From f29509577a78a24eaaebd13d7ff444723ce75a88 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 25 Oct 2019 10:07:07 +1100 Subject: [PATCH] Add logs to Logs page (#151) * Add websocket connection * Add logs to the log page * Make debug color more readable * Remove TODO from front page * Put all log entries in latest first order * Add filtering of log entries by level * Limit log entries and throttle updates * Fix logger not throttling broadcasts * Remove now unnecessary UI-side log throttling * Filter incoming logs by log level * Make log view more terminal-like --- graphql/documents/data/log.graphql | 5 + graphql/documents/queries/misc.graphql | 5 + graphql/documents/subscriptions.graphql | 6 + graphql/schema/schema.graphql | 4 + graphql/schema/types/logging.graphql | 16 ++ pkg/api/resolver_query_logs.go | 23 +++ pkg/api/resolver_subscription_logging.go | 60 ++++++ pkg/logger/logger.go | 94 ++++++++- ui/v2/package.json | 2 + .../components/Settings/SettingsLogsPanel.tsx | 183 +++++++++++++++++- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 2 - ui/v2/src/components/Stats.tsx | 2 - ui/v2/src/core/StashService.ts | 54 +++++- ui/v2/src/index.scss | 31 +++ ui/v2/yarn.lock | 51 ++++- 15 files changed, 519 insertions(+), 19 deletions(-) create mode 100644 graphql/documents/data/log.graphql create mode 100644 graphql/schema/types/logging.graphql create mode 100644 pkg/api/resolver_query_logs.go create mode 100644 pkg/api/resolver_subscription_logging.go diff --git a/graphql/documents/data/log.graphql b/graphql/documents/data/log.graphql new file mode 100644 index 000000000..b0dc92863 --- /dev/null +++ b/graphql/documents/data/log.graphql @@ -0,0 +1,5 @@ +fragment LogEntryData on LogEntry { + time + level + message +} diff --git a/graphql/documents/queries/misc.graphql b/graphql/documents/queries/misc.graphql index 3c1de2639..1c3bccc75 100644 --- a/graphql/documents/queries/misc.graphql +++ b/graphql/documents/queries/misc.graphql @@ -54,6 +54,11 @@ query Stats { } } +query Logs { + logs { + ...LogEntryData + } +} query Version { version { hash, diff --git a/graphql/documents/subscriptions.graphql b/graphql/documents/subscriptions.graphql index 7329b0d92..f9673dd8e 100644 --- a/graphql/documents/subscriptions.graphql +++ b/graphql/documents/subscriptions.graphql @@ -1,3 +1,9 @@ subscription MetadataUpdate { metadataUpdate +} + +subscription LoggingSubscribe { + loggingSubscribe { + ...LogEntryData + } } \ No newline at end of file diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index cfff40f30..b8af1c639 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -37,6 +37,8 @@ type Query { """Organize scene markers by tag for a given scene ID""" sceneMarkerTags(scene_id: ID!): [SceneMarkerTag!]! + logs: [LogEntry!]! + # Scrapers """Scrape a performer using Freeones""" @@ -101,6 +103,8 @@ type Mutation { type Subscription { """Update from the metadata manager""" metadataUpdate: String! + + loggingSubscribe: [LogEntry!]! } schema { diff --git a/graphql/schema/types/logging.graphql b/graphql/schema/types/logging.graphql new file mode 100644 index 000000000..397a27f10 --- /dev/null +++ b/graphql/schema/types/logging.graphql @@ -0,0 +1,16 @@ +"""Log entries""" +scalar Time + +enum LogLevel { + Debug + Info + Progress + Warning + Error +} + +type LogEntry { + time: Time! + level: LogLevel! + message: String! +} \ No newline at end of file diff --git a/pkg/api/resolver_query_logs.go b/pkg/api/resolver_query_logs.go new file mode 100644 index 000000000..2fc0fa36f --- /dev/null +++ b/pkg/api/resolver_query_logs.go @@ -0,0 +1,23 @@ +package api + +import ( + "context" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" +) + +func (r *queryResolver) Logs(ctx context.Context) ([]*models.LogEntry, error) { + logCache := logger.GetLogCache() + ret := make([]*models.LogEntry, len(logCache)) + + for i, entry := range logCache { + ret[i] = &models.LogEntry{ + Time: entry.Time, + Level: getLogLevel(entry.Type), + Message: entry.Message, + } + } + + return ret, nil +} \ No newline at end of file diff --git a/pkg/api/resolver_subscription_logging.go b/pkg/api/resolver_subscription_logging.go new file mode 100644 index 000000000..6ef8e4109 --- /dev/null +++ b/pkg/api/resolver_subscription_logging.go @@ -0,0 +1,60 @@ +package api + +import ( + "context" + + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/models" +) + +func getLogLevel(logType string) models.LogLevel { + if logType == "progress" { + return models.LogLevelProgress + } else if logType == "debug" { + return models.LogLevelDebug + } else if logType == "info" { + return models.LogLevelInfo + } else if logType == "warn" { + return models.LogLevelWarning + } else if logType == "error" { + return models.LogLevelError + } + + // default to debug + return models.LogLevelDebug +} + +func logEntriesFromLogItems(logItems []logger.LogItem) []*models.LogEntry { + ret := make([]*models.LogEntry, len(logItems)) + + for i, entry := range logItems { + ret[i] = &models.LogEntry{ + Time: entry.Time, + Level: getLogLevel(entry.Type), + Message: entry.Message, + } + } + + return ret +} + +func (r *subscriptionResolver) LoggingSubscribe(ctx context.Context) (<-chan []*models.LogEntry, error) { + ret := make(chan []*models.LogEntry, 100) + stop := make(chan int, 1) + logSub := logger.SubscribeToLog(stop) + + go func() { + for { + select { + case logEntries := <-logSub: + ret <- logEntriesFromLogItems(logEntries) + case <-ctx.Done(): + stop <- 0 + close(ret) + return + } + } + }() + + return ret, nil +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 726e87b77..1789c56a5 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -2,13 +2,16 @@ package logger import ( "fmt" - "github.com/sirupsen/logrus" "sync" + "time" + + "github.com/sirupsen/logrus" ) type LogItem struct { - Type string `json:"type"` - Message string `json:"message"` + Time time.Time `json:"time"` + Type string `json:"type"` + Message string `json:"message"` } var logger = logrus.New() @@ -16,14 +19,99 @@ var progressLogger = logrus.New() var LogCache []LogItem var mutex = &sync.Mutex{} +var logSubs []chan []LogItem +var waiting = false +var lastBroadcast = time.Now() +var logBuffer []LogItem func addLogItem(l *LogItem) { mutex.Lock() + l.Time = time.Now() LogCache = append([]LogItem{*l}, LogCache...) if len(LogCache) > 30 { LogCache = LogCache[:len(LogCache)-1] } mutex.Unlock() + go broadcastLogItem(l) +} + +func GetLogCache() []LogItem { + mutex.Lock() + + ret := make([]LogItem, len(LogCache)) + copy(ret, LogCache) + + mutex.Unlock() + + return ret +} + +func SubscribeToLog(stop chan int) <-chan []LogItem { + ret := make(chan []LogItem, 100) + + go func() { + <-stop + unsubscribeFromLog(ret) + }() + + mutex.Lock() + logSubs = append(logSubs, ret) + mutex.Unlock() + + return ret +} + +func unsubscribeFromLog(toRemove chan []LogItem) { + mutex.Lock() + for i, c := range logSubs { + if c == toRemove { + logSubs = append(logSubs[:i], logSubs[i+1:]...) + } + } + close(toRemove) + mutex.Unlock() +} + +func doBroadcastLogItems() { + // assumes mutex held + + for _, c := range logSubs { + // don't block waiting to broadcast + select { + case c <- logBuffer: + default: + } + } + + logBuffer = nil + waiting = false + lastBroadcast = time.Now() +} + +func broadcastLogItem(l *LogItem) { + mutex.Lock() + + logBuffer = append(logBuffer, *l) + + // don't send more than once per second + if !waiting { + // if last broadcast was under a second ago, wait until a second has + // passed + timeSinceBroadcast := time.Since(lastBroadcast) + if timeSinceBroadcast.Seconds() < 1 { + waiting = true + time.AfterFunc(time.Second-timeSinceBroadcast, func() { + mutex.Lock() + doBroadcastLogItems() + mutex.Unlock() + }) + } else { + doBroadcastLogItems() + } + } + // if waiting then adding it to the buffer is sufficient + + mutex.Unlock() } func init() { diff --git a/ui/v2/package.json b/ui/v2/package.json index 4e5c65601..913ef7c37 100644 --- a/ui/v2/package.json +++ b/ui/v2/package.json @@ -14,6 +14,7 @@ "@types/react-router-dom": "4.3.3", "@types/video.js": "^7.2.11", "apollo-boost": "0.4.0", + "apollo-link-ws": "^1.0.19", "axios": "0.18.0", "bulma": "0.7.5", "formik": "1.5.7", @@ -32,6 +33,7 @@ "react-router-dom": "5.0.0", "react-scripts": "3.0.1", "react-use": "9.1.2", + "subscriptions-transport-ws": "^0.9.16", "video.js": "^7.6.0" }, "scripts": { diff --git a/ui/v2/src/components/Settings/SettingsLogsPanel.tsx b/ui/v2/src/components/Settings/SettingsLogsPanel.tsx index 5d13bbd9d..0f3a72337 100644 --- a/ui/v2/src/components/Settings/SettingsLogsPanel.tsx +++ b/ui/v2/src/components/Settings/SettingsLogsPanel.tsx @@ -1,19 +1,188 @@ import { - H1, - H4, - H6, - Tag, + H4, FormGroup, HTMLSelect, } from "@blueprintjs/core"; -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, useState, useEffect, useRef } from "react"; import * as GQL from "../../core/generated-graphql"; -import { TextUtils } from "../../utils/text"; +import { StashService } from "../../core/StashService"; interface IProps {} +function convertTime(logEntry : GQL.LogEntryDataFragment) { + function pad(val : number) { + var ret = val.toString(); + if (val <= 9) { + ret = "0" + ret; + } + + return ret; + } + + var date = new Date(logEntry.time); + var month = date.getMonth() + 1; + var day = date.getDate(); + var dateStr = date.getFullYear() + "-" + pad(month) + "-" + pad(day); + dateStr += " " + pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()); + + return dateStr; +} + +class LogEntry { + public time: string; + public level: string; + public message: string; + public id: string; + + private static nextId: number = 0; + + public constructor(logEntry: GQL.LogEntryDataFragment) { + this.time = convertTime(logEntry); + this.level = logEntry.level; + this.message = logEntry.message; + + var id = LogEntry.nextId++; + this.id = id.toString(); + } +} + export const SettingsLogsPanel: FunctionComponent = (props: IProps) => { + const { data, error } = StashService.useLoggingSubscribe(); + const { data: existingData } = StashService.useLogs(); + + const logEntries = useRef([]); + const [logLevel, setLogLevel] = useState("Info"); + const [filteredLogEntries, setFilteredLogEntries] = useState([]); + const lastUpdate = useRef(0); + const updateTimeout = useRef(); + + // maximum number of log entries to display. Subsequent entries will truncate + // the list, dropping off the oldest entries first. + const MAX_LOG_ENTRIES = 200; + + function truncateLogEntries(entries : LogEntry[]) { + entries.length = Math.min(entries.length, MAX_LOG_ENTRIES); + } + + function prependLogEntries(toPrepend : LogEntry[]) { + var newLogEntries = toPrepend.concat(logEntries.current); + truncateLogEntries(newLogEntries); + logEntries.current = newLogEntries; + } + + function appendLogEntries(toAppend : LogEntry[]) { + var newLogEntries = logEntries.current.concat(toAppend); + truncateLogEntries(newLogEntries); + logEntries.current = newLogEntries; + } + + useEffect(() => { + if (!data) { return; } + + // append data to the logEntries + var convertedData = data.loggingSubscribe.map(convertLogEntry); + + // filter subscribed data as it comes in, otherwise we'll end up + // truncating stuff that wasn't filtered out + convertedData = convertedData.filter(filterByLogLevel) + + // put newest entries at the top + convertedData.reverse(); + prependLogEntries(convertedData); + + updateFilteredEntries(); + }, [data]); + + useEffect(() => { + if (!existingData || !existingData.logs) { return; } + + var convertedData = existingData.logs.map(convertLogEntry); + appendLogEntries(convertedData); + + updateFilteredEntries(); + }, [existingData]); + + function updateFilteredEntries() { + if (!updateTimeout.current) { + console.log("Updating after timeout"); + } + updateTimeout.current = undefined; + + var filteredEntries = logEntries.current.filter(filterByLogLevel); + setFilteredLogEntries(filteredEntries); + + lastUpdate.current = new Date().getTime(); + } + + useEffect(() => { + updateFilteredEntries(); + }, [logLevel]); + + function convertLogEntry(logEntry : GQL.LogEntryDataFragment) { + return new LogEntry(logEntry); + } + + function levelClass(level : string) { + return level.toLowerCase().trim(); + } + + interface ILogElementProps { + logEntry : LogEntry + } + + function LogElement(props : ILogElementProps) { + // pad to maximum length of level enum + var level = props.logEntry.level.padEnd(GQL.LogLevel.Progress.length); + + return ( + <> + {props.logEntry.time}  + {level}  + {props.logEntry.message} +
+ + ); + } + + function maybeRenderError() { + if (error) { + return ( + <> + Error connecting to log server: {error.message}
+ + ); + } + } + + const logLevels = ["Debug", "Info", "Warning", "Error"]; + + function filterByLogLevel(logEntry : LogEntry) { + if (logLevel == "Debug") { + return true; + } + + var logLevelIndex = logLevels.indexOf(logLevel); + var levelIndex = logLevels.indexOf(logEntry.level); + + return levelIndex >= logLevelIndex; + } + return ( <> - Logs +

Logs

+
+ + setLogLevel(event.target.value)} + value={logLevel} + /> + +
+
+ {maybeRenderError()} + {filteredLogEntries.map((logEntry) => + + )} +
); }; diff --git a/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 0e0a15676..2526aa52a 100644 --- a/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -7,10 +7,8 @@ import { H4, } from "@blueprintjs/core"; import React, { FunctionComponent, useState } from "react"; -import * as GQL from "../../../core/generated-graphql"; import { StashService } from "../../../core/StashService"; import { ErrorUtils } from "../../../utils/errors"; -import { TextUtils } from "../../../utils/text"; import { ToastUtils } from "../../../utils/toasts"; import { GenerateButton } from "./GenerateButton"; diff --git a/ui/v2/src/components/Stats.tsx b/ui/v2/src/components/Stats.tsx index a45485546..5ec380933 100644 --- a/ui/v2/src/components/Stats.tsx +++ b/ui/v2/src/components/Stats.tsx @@ -56,8 +56,6 @@ export const Stats: FunctionComponent = () => { * Filters for performers and studios only supports one item, even though it's a multi select. - TODO: - * Websocket connection to display logs in the UI `} diff --git a/ui/v2/src/core/StashService.ts b/ui/v2/src/core/StashService.ts index fcd0c6c8d..2595d62cc 100644 --- a/ui/v2/src/core/StashService.ts +++ b/ui/v2/src/core/StashService.ts @@ -1,24 +1,58 @@ -import ApolloClient from "apollo-boost"; +import ApolloClient from "apollo-client"; +import { WebSocketLink } from 'apollo-link-ws'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { HttpLink, split } from "apollo-boost"; import _ from "lodash"; import { ListFilterModel } from "../models/list-filter/filter"; import * as GQL from "./generated-graphql"; +import { SubscriptionHookOptions } from "react-apollo-hooks"; +import { getMainDefinition } from "apollo-utilities"; +import { platform } from "os"; export class StashService { public static client: ApolloClient; public static initialize() { const platformUrl = new URL(window.location.origin); + const wsPlatformUrl = new URL(window.location.origin); + wsPlatformUrl.protocol = "ws:"; + if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { platformUrl.port = "9999"; // TODO: Hack. Development expects port 9999 + wsPlatformUrl.port = "9999"; if (process.env.REACT_APP_HTTPS === "true") { platformUrl.protocol = "https:"; + wsPlatformUrl.protocol = "wss:"; } } - const url = platformUrl.toString().slice(0, -1); + const url = platformUrl.toString().slice(0, -1) + "/graphql"; + const wsUrl = wsPlatformUrl.toString().slice(0, -1) + "/graphql"; + const httpLink = new HttpLink({ + uri: url, + }); + + const wsLink = new WebSocketLink({ + uri: wsUrl, + options: { + reconnect: true + }, + }); + + const link = split( + ({ query }) => { + const { kind, operation } = getMainDefinition(query); + return kind === 'OperationDefinition' && operation === 'subscription'; + }, + wsLink, + httpLink, + ); + + const cache = new InMemoryCache(); StashService.client = new ApolloClient({ - uri: `${url}/graphql`, + link: link, + cache: cache }); (window as any).StashService = StashService; @@ -174,6 +208,20 @@ export class StashService { return GQL.useConfigureInterface({ variables: { input }, refetchQueries: ["Configuration"] }); } + public static useMetadataUpdate() { + return GQL.useMetadataUpdate(); + } + + public static useLoggingSubscribe() { + return GQL.useLoggingSubscribe(); + } + + public static useLogs() { + return GQL.useLogs({ + fetchPolicy: 'no-cache' + }); + } + public static queryScrapeFreeones(performerName: string) { return StashService.client.query({ query: GQL.ScrapeFreeonesDocument, diff --git a/ui/v2/src/index.scss b/ui/v2/src/index.scss index 8389deba8..2b6c393f9 100755 --- a/ui/v2/src/index.scss +++ b/ui/v2/src/index.scss @@ -172,6 +172,37 @@ video.preview { white-space: pre-line; } +.logs { + white-space: pre-wrap; + word-break: break-all; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + font-size: smaller; + padding-right: 10px; + overflow-y: auto; + max-height: 100vh; + width: 120ch; + + .debug { + color: lightgreen; + font-weight: bold; + } + + .info { + color: white; + font-weight: bold; + } + + .warning { + color: orange; + font-weight: bold; + } + + .error { + color: red; + font-weight: bold; + } +} + span.block { display: block; } diff --git a/ui/v2/yarn.lock b/ui/v2/yarn.lock index 17dee0ece..d5995290b 100755 --- a/ui/v2/yarn.lock +++ b/ui/v2/yarn.lock @@ -2232,6 +2232,14 @@ apollo-link-http@^1.3.1: apollo-link "^1.2.8" apollo-link-http-common "^0.2.10" +apollo-link-ws@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-ws/-/apollo-link-ws-1.0.19.tgz#dfa871d4df883a8777c9556c872fc892e103daa5" + integrity sha512-mRXmeUkc55ixOdYRtfq5rq3o9sboKghKABKroDVhJnkdS56zthBEWMAD+phajujOUbqByxjok0te8ABqByBdeQ== + dependencies: + apollo-link "^1.2.13" + tslib "^1.9.3" + apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.3, apollo-link@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.8.tgz#0f252adefd5047ac1a9f35ba9439d216587dcd84" @@ -2239,6 +2247,16 @@ apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.3, apollo-link@^1.2.8: dependencies: zen-observable-ts "^0.8.15" +apollo-link@^1.2.13: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" + apollo-utilities@1.3.0, apollo-utilities@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.0.tgz#9803724c07ac94ca11dc26397edb58735d2b0211" @@ -2651,6 +2669,11 @@ babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backo2@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + bail@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.3.tgz#63cfb9ddbac829b02a3128cd53224be78e6c21a3" @@ -4754,6 +4777,11 @@ eventemitter3@^3.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + events@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" @@ -6658,7 +6686,7 @@ istanbul-reports@^2.1.1: dependencies: handlebars "^4.1.2" -iterall@^1.1.3, iterall@^1.2.2: +iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== @@ -11395,6 +11423,17 @@ stylis@3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== +subscriptions-transport-ws@^0.9.16: + version "0.9.16" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" + integrity sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -11442,7 +11481,7 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@^1.0.2, symbol-observable@^1.1.0: +symbol-observable@^1.0.2, symbol-observable@^1.0.4, symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -12766,6 +12805,14 @@ zen-observable-ts@^0.8.15: dependencies: zen-observable "^0.8.0" +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable@^0.8.0: version "0.8.13" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.13.tgz#a9f1b9dbdfd2d60a08761ceac6a861427d44ae2e"