@rocicorp/zero 1.10.0-canary.21 → 1.10.0-canary.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/analyze-cli.js +2 -2
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/replicache/src/dag/gc.d.ts.map +1 -1
- package/out/replicache/src/dag/gc.js +2 -1
- package/out/replicache/src/dag/gc.js.map +1 -1
- package/out/replicache/src/dag/invalid-ref-count-error.d.ts +12 -0
- package/out/replicache/src/dag/invalid-ref-count-error.d.ts.map +1 -0
- package/out/replicache/src/dag/invalid-ref-count-error.js +19 -0
- package/out/replicache/src/dag/invalid-ref-count-error.js.map +1 -0
- package/out/replicache/src/dag/store-impl.d.ts +12 -2
- package/out/replicache/src/dag/store-impl.d.ts.map +1 -1
- package/out/replicache/src/dag/store-impl.js +22 -8
- package/out/replicache/src/dag/store-impl.js.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.d.ts.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts +15 -5
- package/out/replicache/src/kv/idb-store-with-mem-fallback.d.ts.map +1 -1
- package/out/replicache/src/kv/idb-store-with-mem-fallback.js +25 -25
- package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +1 -1
- package/out/replicache/src/kv/idb-store.d.ts +9 -0
- package/out/replicache/src/kv/idb-store.d.ts.map +1 -1
- package/out/replicache/src/kv/idb-store.js +14 -2
- package/out/replicache/src/kv/idb-store.js.map +1 -1
- package/out/replicache/src/kv/mem-store.d.ts +1 -0
- package/out/replicache/src/kv/mem-store.d.ts.map +1 -1
- package/out/replicache/src/kv/mem-store.js +3 -0
- package/out/replicache/src/kv/mem-store.js.map +1 -1
- package/out/replicache/src/kv/op-sqlite/store.d.ts.map +1 -1
- package/out/replicache/src/kv/op-sqlite/store.js +1 -1
- package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/sqlite-store.d.ts +2 -1
- package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
- package/out/replicache/src/kv/sqlite-store.js +6 -1
- package/out/replicache/src/kv/sqlite-store.js.map +1 -1
- package/out/replicache/src/kv/store.d.ts +6 -0
- package/out/replicache/src/kv/store.d.ts.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.d.ts +10 -5
- package/out/replicache/src/persist/collect-idb-databases.d.ts.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js +16 -9
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/replicache-impl.d.ts +14 -3
- package/out/replicache/src/replicache-impl.d.ts.map +1 -1
- package/out/replicache/src/replicache-impl.js +104 -9
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/shared/src/arrays.js +1 -15
- package/out/shared/src/arrays.js.map +1 -1
- package/out/shared/src/hash.d.ts +1 -0
- package/out/shared/src/hash.d.ts.map +1 -1
- package/out/shared/src/hash.js +10 -28
- package/out/shared/src/hash.js.map +1 -1
- package/out/shared/src/iterables.d.ts +2 -0
- package/out/shared/src/iterables.d.ts.map +1 -1
- package/out/shared/src/iterables.js +18 -1
- package/out/shared/src/iterables.js.map +1 -1
- package/out/shared/src/json.d.ts.map +1 -1
- package/out/shared/src/json.js +2 -2
- package/out/shared/src/json.js.map +1 -1
- package/out/shared/src/ring-buffer.d.ts +8 -0
- package/out/shared/src/ring-buffer.d.ts.map +1 -1
- package/out/shared/src/ring-buffer.js +22 -1
- package/out/shared/src/ring-buffer.js.map +1 -1
- package/out/shared/src/xxhash32.d.ts +36 -0
- package/out/shared/src/xxhash32.d.ts.map +1 -0
- package/out/shared/src/xxhash32.js +51 -0
- package/out/shared/src/xxhash32.js.map +1 -0
- package/out/z2s/src/compiler.js +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/auth/read-authorizer.js +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js +3 -2
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js +1 -1
- package/out/zero-cache/src/services/run-ast.js +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +6 -10
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-covering.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/query-covering.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js +3 -3
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-client/src/client/connection-manager.d.ts +20 -0
- package/out/zero-client/src/client/connection-manager.d.ts.map +1 -1
- package/out/zero-client/src/client/connection-manager.js +32 -14
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/connection-status-enum.d.ts +2 -0
- package/out/zero-client/src/client/connection-status-enum.d.ts.map +1 -1
- package/out/zero-client/src/client/connection-status-enum.js +3 -1
- package/out/zero-client/src/client/connection-status-enum.js.map +1 -1
- package/out/zero-client/src/client/connection.d.ts +6 -0
- package/out/zero-client/src/client/connection.d.ts.map +1 -1
- package/out/zero-client/src/client/connection.js +2 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/context.d.ts +8 -11
- package/out/zero-client/src/client/context.d.ts.map +1 -1
- package/out/zero-client/src/client/context.js +11 -15
- package/out/zero-client/src/client/context.js.map +1 -1
- package/out/zero-client/src/client/custom.js +2 -2
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/error.js +1 -1
- package/out/zero-client/src/client/mutator-proxy.d.ts.map +1 -1
- package/out/zero-client/src/client/mutator-proxy.js +2 -1
- package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +3 -0
- package/out/zero-client/src/client/options.d.ts.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/query-manager.d.ts +2 -0
- package/out/zero-client/src/client/query-manager.d.ts.map +1 -1
- package/out/zero-client/src/client/query-manager.js +48 -9
- package/out/zero-client/src/client/query-manager.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts +18 -3
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +59 -12
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/types/query-result.d.ts +2 -0
- package/out/zero-client/src/types/query-result.d.ts.map +1 -1
- package/out/zero-protocol/src/ast.d.ts +64 -1
- package/out/zero-protocol/src/ast.d.ts.map +1 -1
- package/out/zero-protocol/src/ast.js +223 -51
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/query-hash-visitor.d.ts +81 -0
- package/out/zero-protocol/src/query-hash-visitor.d.ts.map +1 -0
- package/out/zero-protocol/src/query-hash-visitor.js +324 -0
- package/out/zero-protocol/src/query-hash-visitor.js.map +1 -0
- package/out/zero-protocol/src/query-hash.d.ts +7 -2
- package/out/zero-protocol/src/query-hash.d.ts.map +1 -1
- package/out/zero-protocol/src/query-hash.js +12 -9
- package/out/zero-protocol/src/query-hash.js.map +1 -1
- package/out/zero-react/src/use-query.d.ts +5 -4
- package/out/zero-react/src/use-query.d.ts.map +1 -1
- package/out/zero-react/src/use-query.js +86 -9
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-solid/src/solid-view.d.ts +10 -0
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +27 -0
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zero-solid/src/use-query.js +5 -3
- package/out/zero-solid/src/use-query.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts +10 -0
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +20 -8
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts.map +1 -1
- package/out/zql/src/ivm/cap.js +5 -3
- package/out/zql/src/ivm/cap.js.map +1 -1
- package/out/zql/src/ivm/deferred-input.d.ts.map +1 -1
- package/out/zql/src/ivm/deferred-input.js +3 -2
- package/out/zql/src/ivm/deferred-input.js.map +1 -1
- package/out/zql/src/ivm/exists.d.ts +1 -1
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +1 -1
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.d.ts +2 -2
- package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-in.js +6 -5
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.d.ts +2 -2
- package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +4 -4
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js +10 -8
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter.d.ts +2 -2
- package/out/zql/src/ivm/filter.d.ts.map +1 -1
- package/out/zql/src/ivm/filter.js +7 -4
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +2 -2
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +6 -4
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts +2 -2
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +9 -9
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +3 -4
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +9 -7
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +3 -3
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/mutator-registry.d.ts.map +1 -1
- package/out/zql/src/mutate/mutator-registry.js +2 -2
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.d.ts +2 -0
- package/out/zql/src/query/query-delegate-base.d.ts.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +58 -31
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-delegate.d.ts +11 -1
- package/out/zql/src/query/query-delegate.d.ts.map +1 -1
- package/out/zql/src/query/query-impl.d.ts +25 -14
- package/out/zql/src/query/query-impl.d.ts.map +1 -1
- package/out/zql/src/query/query-impl.js +414 -94
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query-internals.d.ts +21 -11
- package/out/zql/src/query/query-internals.d.ts.map +1 -1
- package/out/zql/src/query/query-internals.js.map +1 -1
- package/out/zql/src/query/query-transitions.d.ts +171 -0
- package/out/zql/src/query/query-transitions.d.ts.map +1 -0
- package/out/zql/src/query/query-transitions.js +207 -0
- package/out/zql/src/query/query-transitions.js.map +1 -0
- package/out/zql/src/query/query.d.ts +7 -1
- package/out/zql/src/query/query.d.ts.map +1 -1
- package/out/zql/src/query/runnable-query-impl.d.ts +3 -2
- package/out/zql/src/query/runnable-query-impl.d.ts.map +1 -1
- package/out/zql/src/query/runnable-query-impl.js +23 -2
- package/out/zql/src/query/runnable-query-impl.js.map +1 -1
- package/out/zql/src/query/typed-view.d.ts +1 -1
- package/out/zql/src/query/typed-view.d.ts.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +7 -5
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ import { getOrCreateCounter } from "../observability/metrics.js";
|
|
|
9
9
|
import { ProtocolErrorWithLevel, getLogLevel, wrapWithProtocolError } from "../types/error-with-level.js";
|
|
10
10
|
import { upstreamSchema } from "../../../zero-protocol/src/up.js";
|
|
11
11
|
import { PokeChunkEncoder } from "../types/poke-chunk.js";
|
|
12
|
-
import WebSocket
|
|
12
|
+
import WebSocket, { createWebSocketStream } from "ws";
|
|
13
13
|
import { Readable, Writable, pipeline } from "node:stream";
|
|
14
14
|
//#region ../zero-cache/src/workers/connection.ts
|
|
15
15
|
var DOWNSTREAM_MSG_INTERVAL_MS = 6e3;
|
|
@@ -198,7 +198,7 @@ var Connection = class {
|
|
|
198
198
|
}
|
|
199
199
|
};
|
|
200
200
|
function send(lc, ws, data, callback, serialized) {
|
|
201
|
-
if (ws.readyState === WebSocket
|
|
201
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
202
202
|
serialized ??= JSON.stringify(data);
|
|
203
203
|
if (callback === "ignore-backpressure") {
|
|
204
204
|
ws.send(serialized);
|
|
@@ -284,7 +284,7 @@ function sendAsync(lc, ws, data) {
|
|
|
284
284
|
}
|
|
285
285
|
function sendBinary(lc, ws, chunk) {
|
|
286
286
|
return new Promise((resolve, reject) => {
|
|
287
|
-
if (ws.readyState !== WebSocket
|
|
287
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
288
288
|
const error = webSocketClosedError();
|
|
289
289
|
lc.debug?.(`Dropping outbound binary message on ws (state: ${ws.readyState})`);
|
|
290
290
|
reject(error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#downstreamSender","#downstreamMsgTimer","#webSocketErrors","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#recordWebSocketError","#lastDownstreamMsgTime","#pokeChunkEncoder","#sendPokePart","#sendPokeEnd"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.ts';\nimport type {Downstream} from '../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n POKE_CHUNK_PROTOCOL_VERSION,\n type PokeChunk,\n type PokeEndMessage,\n type PokePartMessage,\n} from '../../../zero-protocol/src/poke.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {upstreamSchema, type Upstream} from '../../../zero-protocol/src/up.ts';\nimport {getOrCreateCounter} from '../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../types/downstream.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport {PokeChunkEncoder} from '../types/poke-chunk.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult =\n | {\n type: 'stream';\n source: 'viewSyncer';\n stream: Source<ViewSyncerDownstream>;\n }\n | {\n type: 'stream';\n source: 'pusher';\n stream: Source<Downstream>;\n };\n\nexport interface MessageHandler {\n handleMessage(msg: Upstream): Promise<HandlerResult[]>;\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\nexport const WEBSOCKET_SEND_TIMEOUT_MS = 10_000;\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst EVENT_TYPE_ATTRIBUTE = 'event.type';\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #downstreamSender: DownstreamSender;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n readonly #webSocketErrors = getOrCreateCounter(\n 'sync',\n 'websocket.errors',\n 'Client WebSocket error events.',\n );\n\n #viewSyncerOutboundStream: Source<ViewSyncerDownstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#downstreamSender = new DownstreamSender(\n this.#lc,\n ws,\n protocolVersion,\n );\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = valita.parse(value, upstreamSchema);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n if (result.source === 'viewSyncer') {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream.message, callback, downstream.serialized),\n );\n } else {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream, callback),\n );\n }\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n if (!wasClean) {\n this.#recordWebSocketError('unclean_close');\n }\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#recordWebSocketError('error_event');\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\n };\n\n #recordWebSocketError(eventType: string) {\n this.#webSocketErrors.add(1, {\n [PROTOCOL_VERSION_ATTRIBUTE]: this.#protocolVersion,\n [EVENT_TYPE_ATTRIBUTE]: eventType,\n });\n }\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound<T>(\n outboundStream: Source<T>,\n sendMessage: (\n downstream: T,\n callback: (err?: Error | null) => void,\n ) => void,\n ) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: T, _encoding, callback) =>\n sendMessage(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n this.#downstreamSender.send(data, callback, serialized);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string | PokeChunk, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n) {\n if (ws.readyState === WebSocket.OPEN) {\n serialized ??= JSON.stringify(data);\n if (callback === 'ignore-backpressure') {\n ws.send(serialized);\n return;\n }\n\n let completed = false;\n const timer = setTimeout(() => {\n complete(webSocketSendTimeoutError());\n }, WEBSOCKET_SEND_TIMEOUT_MS);\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n callback(error);\n };\n\n ws.send(serialized, complete);\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\n/**\n * Sends downstream messages in the format supported by a connection's sync\n * protocol version. Keep version forks contained here so the view-syncer can\n * continue producing one canonical poke representation.\n *\n * Exported for compatibility testing.\n */\nexport class DownstreamSender {\n readonly #lc: LogContext;\n readonly #ws: WebSocketLike;\n readonly #protocolVersion: number;\n #pokeChunkEncoder: PokeChunkEncoder | undefined;\n\n constructor(lc: LogContext, ws: WebSocketLike, protocolVersion: number) {\n this.#lc = lc;\n this.#ws = ws;\n this.#protocolVersion = protocolVersion;\n }\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n if (this.#protocolVersion >= POKE_CHUNK_PROTOCOL_VERSION) {\n switch (data[0]) {\n case 'pokeStart':\n assert(\n this.#pokeChunkEncoder === undefined,\n 'cannot start a poke while another poke is in progress',\n );\n this.#pokeChunkEncoder = new PokeChunkEncoder();\n break;\n case 'pokePart':\n this.#sendPokePart(data, callback, serialized);\n return;\n case 'pokeEnd':\n this.#sendPokeEnd(data, callback);\n return;\n }\n }\n send(this.#lc, this.#ws, data, callback, serialized);\n }\n\n #sendPokePart(\n pokePart: PokePartMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokePart received without a pokeStart');\n\n serialized ??= JSON.stringify(pokePart);\n assert(\n serialized.startsWith(POKE_PART_PREFIX) && serialized.endsWith(']'),\n 'invalid serialized pokePart',\n );\n void encoder\n .addPatch(serialized.slice(POKE_PART_PREFIX.length, -1), chunk =>\n sendBinary(this.#lc, this.#ws, chunk),\n )\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n\n #sendPokeEnd(\n pokeEnd: PokeEndMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokeEnd received without a pokeStart');\n this.#pokeChunkEncoder = undefined;\n\n if (pokeEnd[1].cancel) {\n encoder.cancel();\n send(this.#lc, this.#ws, pokeEnd, callback);\n return;\n }\n\n void encoder\n .finish(chunk => sendBinary(this.#lc, this.#ws, chunk))\n .then(() => sendAsync(this.#lc, this.#ws, pokeEnd))\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n}\n\nconst POKE_PART_PREFIX = '[\"pokePart\",';\n\nfunction sendAsync(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n send(lc, ws, data, error => (error ? reject(error) : resolve()));\n });\n}\n\nfunction sendBinary(\n lc: LogContext,\n ws: WebSocketLike,\n chunk: PokeChunk,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState !== WebSocket.OPEN) {\n const error = webSocketClosedError();\n lc.debug?.(\n `Dropping outbound binary message on ws (state: ${ws.readyState})`,\n );\n reject(error);\n return;\n }\n let completed = false;\n const timer = setTimeout(\n () => complete(webSocketSendTimeoutError()),\n WEBSOCKET_SEND_TIMEOUT_MS,\n );\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n error ? reject(error) : resolve();\n };\n\n ws.send(chunk, complete);\n });\n}\n\nfunction invokeCallback(\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n error?: Error,\n): void {\n if (callback !== 'ignore-backpressure') {\n callback(error);\n }\n}\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction webSocketClosedError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nfunction webSocketSendTimeoutError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: `WebSocket send timed out after ${WEBSOCKET_SEND_TIMEOUT_MS} ms`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#downstreamSender","#downstreamMsgTimer","#webSocketErrors","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#recordWebSocketError","#lastDownstreamMsgTime","#pokeChunkEncoder","#sendPokePart","#sendPokeEnd"],"sources":["../../../../../zero-cache/src/workers/connection.ts"],"sourcesContent":["import {pipeline, Readable, Writable} from 'node:stream';\nimport type {LogContext, LogLevel} from '@rocicorp/logger';\nimport type {CloseEvent, Data, ErrorEvent} from 'ws';\nimport WebSocket, {createWebSocketStream} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport * as valita from '../../../shared/src/valita.ts';\nimport type {ConnectedMessage} from '../../../zero-protocol/src/connect.ts';\nimport type {Downstream} from '../../../zero-protocol/src/down.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport type {ErrorBody} from '../../../zero-protocol/src/error.ts';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {\n POKE_CHUNK_PROTOCOL_VERSION,\n type PokeChunk,\n type PokeEndMessage,\n type PokePartMessage,\n} from '../../../zero-protocol/src/poke.ts';\nimport {\n MIN_SERVER_SUPPORTED_SYNC_PROTOCOL,\n PROTOCOL_VERSION,\n} from '../../../zero-protocol/src/protocol-version.ts';\nimport {upstreamSchema, type Upstream} from '../../../zero-protocol/src/up.ts';\nimport {getOrCreateCounter} from '../observability/metrics.ts';\nimport type {ViewSyncerDownstream} from '../types/downstream.ts';\nimport {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.ts';\nimport {PokeChunkEncoder} from '../types/poke-chunk.ts';\nimport type {Source} from '../types/streams.ts';\nimport type {ConnectParams} from './connect-params.ts';\n\nexport type HandlerResult =\n | {\n type: 'ok';\n }\n | {\n type: 'fatal';\n error: ErrorBody;\n }\n | {\n type: 'transient';\n errors: ErrorBody[];\n }\n | StreamResult;\n\nexport type StreamResult =\n | {\n type: 'stream';\n source: 'viewSyncer';\n stream: Source<ViewSyncerDownstream>;\n }\n | {\n type: 'stream';\n source: 'pusher';\n stream: Source<Downstream>;\n };\n\nexport interface MessageHandler {\n handleMessage(msg: Upstream): Promise<HandlerResult[]>;\n}\n\n// Ensures that a downstream message is sent at least every interval, sending a\n// 'pong' if necessary. This is set to be slightly longer than the client-side\n// PING_INTERVAL of 5 seconds, so that in the common case, 'pong's are sent in\n// response to client-initiated 'ping's. However, if the inbound stream is\n// backed up because a command is taking a long time to process, the pings\n// will be stuck in the queue (i.e. back-pressured), in which case pongs will\n// be manually sent to notify the client of server liveness.\n//\n// This is equivalent to what is done for Postgres keepalives on the\n// replication stream (which can similarly be back-pressured):\n// https://github.com/rocicorp/mono/blob/f98cb369a2dbb15650328859c732db358f187ef0/packages/zero-cache/src/services/change-source/pg/logical-replication/stream.ts#L21\nconst DOWNSTREAM_MSG_INTERVAL_MS = 6_000;\nexport const WEBSOCKET_SEND_TIMEOUT_MS = 10_000;\nconst PROTOCOL_VERSION_ATTRIBUTE = 'protocol.version';\nconst EVENT_TYPE_ATTRIBUTE = 'event.type';\n\n/**\n * Represents a connection between the client and server.\n *\n * Handles incoming messages on the connection and dispatches\n * them to the correct service.\n *\n * Listens to the ViewSyncer and sends messages to the client.\n */\nexport class Connection {\n readonly #ws: WebSocket;\n readonly #wsID: string;\n readonly #protocolVersion: number;\n readonly #lc: LogContext;\n readonly #onClose: () => void;\n readonly #messageHandler: MessageHandler;\n readonly #downstreamSender: DownstreamSender;\n readonly #downstreamMsgTimer: NodeJS.Timeout | undefined;\n readonly #webSocketErrors = getOrCreateCounter(\n 'sync',\n 'websocket.errors',\n 'Client WebSocket error events.',\n );\n\n #viewSyncerOutboundStream: Source<ViewSyncerDownstream> | undefined;\n #pusherOutboundStream: Source<Downstream> | undefined;\n #closed = false;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n ws: WebSocket,\n messageHandler: MessageHandler,\n onClose: () => void,\n ) {\n const {clientGroupID, clientID, wsID, protocolVersion} = connectParams;\n this.#messageHandler = messageHandler;\n\n this.#ws = ws;\n this.#wsID = wsID;\n this.#protocolVersion = protocolVersion;\n\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#downstreamSender = new DownstreamSender(\n this.#lc,\n ws,\n protocolVersion,\n );\n this.#lc.debug?.('new connection');\n this.#onClose = onClose;\n\n this.#ws.addEventListener('close', this.#handleClose);\n this.#ws.addEventListener('error', this.#handleError);\n\n this.#proxyInbound();\n this.#downstreamMsgTimer = setInterval(\n this.#maybeSendPong,\n DOWNSTREAM_MSG_INTERVAL_MS / 2,\n );\n }\n\n /**\n * Checks the protocol version and errors for unsupported protocols,\n * sending the initial `connected` response on success.\n *\n * This is early in the connection lifecycle because {@link #handleMessage}\n * will only parse messages with schema(s) of supported protocol versions.\n */\n init(): boolean {\n if (\n this.#protocolVersion > PROTOCOL_VERSION ||\n this.#protocolVersion < MIN_SERVER_SUPPORTED_SYNC_PROTOCOL\n ) {\n this.#closeWithError({\n kind: ErrorKind.VersionNotSupported,\n message: `server is at sync protocol v${PROTOCOL_VERSION} and does not support v${\n this.#protocolVersion\n }. The ${\n this.#protocolVersion > PROTOCOL_VERSION ? 'server' : 'client'\n } must be updated to a newer release.`,\n origin: ErrorOrigin.ZeroCache,\n });\n } else {\n const connectedMessage: ConnectedMessage = [\n 'connected',\n {wsid: this.#wsID, timestamp: Date.now()},\n ];\n this.send(connectedMessage, 'ignore-backpressure');\n return true;\n }\n return false;\n }\n\n close(reason: string, ...args: unknown[]) {\n if (this.#closed) {\n return;\n }\n this.#closed = true;\n this.#lc.info?.(`closing connection: ${reason}`, ...args);\n this.#ws.removeEventListener('close', this.#handleClose);\n this.#ws.removeEventListener('error', this.#handleError);\n this.#viewSyncerOutboundStream?.cancel();\n this.#viewSyncerOutboundStream = undefined;\n this.#pusherOutboundStream?.cancel();\n this.#pusherOutboundStream = undefined;\n this.#onClose();\n if (this.#ws.readyState !== this.#ws.CLOSED) {\n this.#ws.close();\n }\n clearTimeout(this.#downstreamMsgTimer);\n\n // spin down services if we have\n // no more client connections for the client group?\n }\n\n handleInitConnection(initConnectionMsg: string) {\n return this.#handleMessage({data: initConnectionMsg});\n }\n\n #handleMessage = async (event: {data: Data}) => {\n const data = event.data.toString();\n if (this.#closed) {\n this.#lc.debug?.('Ignoring message received after closed', data);\n return;\n }\n\n let msg;\n try {\n const value = JSON.parse(data);\n msg = valita.parse(value, upstreamSchema);\n } catch (e) {\n const errorBody = {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n } as const;\n this.#closeWithError(\n errorBody,\n new ProtocolErrorWithLevel(errorBody, 'warn'),\n );\n return;\n }\n\n try {\n const msgType = msg[0];\n if (msgType === 'ping') {\n this.send(['pong', {}], 'ignore-backpressure');\n return;\n }\n\n const result = await this.#messageHandler.handleMessage(msg);\n for (const r of result) {\n this.#handleMessageResult(r);\n }\n } catch (e) {\n this.#closeWithThrown(e);\n }\n };\n\n #handleMessageResult(result: HandlerResult): void {\n switch (result.type) {\n case 'fatal':\n this.#closeWithError(result.error);\n break;\n case 'ok':\n break;\n case 'stream': {\n switch (result.source) {\n case 'viewSyncer':\n assert(\n this.#viewSyncerOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#viewSyncerOutboundStream = result.stream;\n break;\n case 'pusher':\n assert(\n this.#pusherOutboundStream === undefined,\n 'Outbound stream already set for this connection!',\n );\n this.#pusherOutboundStream = result.stream;\n break;\n }\n if (result.source === 'viewSyncer') {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream.message, callback, downstream.serialized),\n );\n } else {\n this.#proxyOutbound(result.stream, (downstream, callback) =>\n this.send(downstream, callback),\n );\n }\n break;\n }\n case 'transient': {\n for (const error of result.errors) {\n this.sendError(error);\n }\n }\n }\n }\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n if (!wasClean) {\n this.#recordWebSocketError('unclean_close');\n }\n this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#recordWebSocketError('error_event');\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\n };\n\n #recordWebSocketError(eventType: string) {\n this.#webSocketErrors.add(1, {\n [PROTOCOL_VERSION_ATTRIBUTE]: this.#protocolVersion,\n [EVENT_TYPE_ATTRIBUTE]: eventType,\n });\n }\n\n #proxyInbound() {\n pipeline(\n createWebSocketStream(this.#ws),\n new Writable({\n write: (data, _encoding, callback) => {\n this.#handleMessage({data}).then(() => callback(), callback);\n },\n }),\n // The done callback is not used, as #handleClose and #handleError,\n // configured on the underlying WebSocket, provide more complete\n // information.\n () => {},\n );\n }\n\n #proxyOutbound<T>(\n outboundStream: Source<T>,\n sendMessage: (\n downstream: T,\n callback: (err?: Error | null) => void,\n ) => void,\n ) {\n // Note: createWebSocketStream() is avoided here in order to control\n // exception handling with #closeWithThrown(). If the Writable\n // from createWebSocketStream() were instead used, exceptions\n // from the outboundStream result in the Writable closing the\n // the websocket before the error message can be sent.\n pipeline(\n Readable.from(outboundStream),\n new Writable({\n objectMode: true,\n write: (downstream: T, _encoding, callback) =>\n sendMessage(downstream, callback),\n }),\n e =>\n e\n ? this.#closeWithThrown(e)\n : this.close(`downstream closed by ViewSyncer`),\n );\n }\n\n #closeWithThrown(e: unknown) {\n const errorBody =\n findProtocolError(e)?.errorBody ?? wrapWithProtocolError(e).errorBody;\n\n this.#closeWithError(errorBody, e);\n }\n\n #closeWithError(errorBody: ErrorBody, thrown?: unknown) {\n this.sendError(errorBody, thrown);\n this.close(\n `${errorBody.kind} (${errorBody.origin}): ${errorBody.message}`,\n errorBody,\n );\n }\n\n #lastDownstreamMsgTime = Date.now();\n\n #maybeSendPong = () => {\n if (Date.now() - this.#lastDownstreamMsgTime > DOWNSTREAM_MSG_INTERVAL_MS) {\n this.#lc.debug?.('manually sending pong');\n this.send(['pong', {}], 'ignore-backpressure');\n }\n };\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ) {\n this.#lastDownstreamMsgTime = Date.now();\n this.#downstreamSender.send(data, callback, serialized);\n }\n\n sendError(errorBody: ErrorBody, thrown?: unknown) {\n sendError(this.#lc, this.#ws, errorBody, thrown);\n }\n}\n\nexport type WebSocketLike = Pick<WebSocket, 'readyState'> & {\n send(data: string | PokeChunk, cb?: (err?: Error) => void): void;\n};\n\n// Exported for testing purposes.\nexport function send(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n) {\n if (ws.readyState === WebSocket.OPEN) {\n serialized ??= JSON.stringify(data);\n if (callback === 'ignore-backpressure') {\n ws.send(serialized);\n return;\n }\n\n let completed = false;\n const timer = setTimeout(() => {\n complete(webSocketSendTimeoutError());\n }, WEBSOCKET_SEND_TIMEOUT_MS);\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n callback(error);\n };\n\n ws.send(serialized, complete);\n } else {\n lc.debug?.(`Dropping outbound message on ws (state: ${ws.readyState})`, {\n dropped: data,\n });\n if (callback !== 'ignore-backpressure') {\n callback(\n new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n ),\n );\n }\n }\n}\n\n/**\n * Sends downstream messages in the format supported by a connection's sync\n * protocol version. Keep version forks contained here so the view-syncer can\n * continue producing one canonical poke representation.\n *\n * Exported for compatibility testing.\n */\nexport class DownstreamSender {\n readonly #lc: LogContext;\n readonly #ws: WebSocketLike;\n readonly #protocolVersion: number;\n #pokeChunkEncoder: PokeChunkEncoder | undefined;\n\n constructor(lc: LogContext, ws: WebSocketLike, protocolVersion: number) {\n this.#lc = lc;\n this.#ws = ws;\n this.#protocolVersion = protocolVersion;\n }\n\n send(\n data: Downstream,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n if (this.#protocolVersion >= POKE_CHUNK_PROTOCOL_VERSION) {\n switch (data[0]) {\n case 'pokeStart':\n assert(\n this.#pokeChunkEncoder === undefined,\n 'cannot start a poke while another poke is in progress',\n );\n this.#pokeChunkEncoder = new PokeChunkEncoder();\n break;\n case 'pokePart':\n this.#sendPokePart(data, callback, serialized);\n return;\n case 'pokeEnd':\n this.#sendPokeEnd(data, callback);\n return;\n }\n }\n send(this.#lc, this.#ws, data, callback, serialized);\n }\n\n #sendPokePart(\n pokePart: PokePartMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n serialized?: string | undefined,\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokePart received without a pokeStart');\n\n serialized ??= JSON.stringify(pokePart);\n assert(\n serialized.startsWith(POKE_PART_PREFIX) && serialized.endsWith(']'),\n 'invalid serialized pokePart',\n );\n void encoder\n .addPatch(serialized.slice(POKE_PART_PREFIX.length, -1), chunk =>\n sendBinary(this.#lc, this.#ws, chunk),\n )\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n\n #sendPokeEnd(\n pokeEnd: PokeEndMessage,\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n ): void {\n const encoder = this.#pokeChunkEncoder;\n assert(encoder, 'pokeEnd received without a pokeStart');\n this.#pokeChunkEncoder = undefined;\n\n if (pokeEnd[1].cancel) {\n encoder.cancel();\n send(this.#lc, this.#ws, pokeEnd, callback);\n return;\n }\n\n void encoder\n .finish(chunk => sendBinary(this.#lc, this.#ws, chunk))\n .then(() => sendAsync(this.#lc, this.#ws, pokeEnd))\n .then(\n () => invokeCallback(callback),\n error => invokeCallback(callback, toError(error)),\n );\n }\n}\n\nconst POKE_PART_PREFIX = '[\"pokePart\",';\n\nfunction sendAsync(\n lc: LogContext,\n ws: WebSocketLike,\n data: Downstream,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n send(lc, ws, data, error => (error ? reject(error) : resolve()));\n });\n}\n\nfunction sendBinary(\n lc: LogContext,\n ws: WebSocketLike,\n chunk: PokeChunk,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n if (ws.readyState !== WebSocket.OPEN) {\n const error = webSocketClosedError();\n lc.debug?.(\n `Dropping outbound binary message on ws (state: ${ws.readyState})`,\n );\n reject(error);\n return;\n }\n let completed = false;\n const timer = setTimeout(\n () => complete(webSocketSendTimeoutError()),\n WEBSOCKET_SEND_TIMEOUT_MS,\n );\n timer.unref();\n\n const complete = (error?: Error | null) => {\n if (completed) {\n return;\n }\n completed = true;\n clearTimeout(timer);\n error ? reject(error) : resolve();\n };\n\n ws.send(chunk, complete);\n });\n}\n\nfunction invokeCallback(\n callback: ((err?: Error | null) => void) | 'ignore-backpressure',\n error?: Error,\n): void {\n if (callback !== 'ignore-backpressure') {\n callback(error);\n }\n}\n\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nfunction webSocketClosedError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: 'WebSocket closed',\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nfunction webSocketSendTimeoutError(): ProtocolErrorWithLevel {\n return new ProtocolErrorWithLevel(\n {\n kind: ErrorKind.Internal,\n message: `WebSocket send timed out after ${WEBSOCKET_SEND_TIMEOUT_MS} ms`,\n origin: ErrorOrigin.ZeroCache,\n },\n 'info',\n );\n}\n\nexport function sendError(\n lc: LogContext,\n ws: WebSocket,\n errorBody: ErrorBody,\n thrown?: unknown,\n) {\n lc = lc.withContext('errorKind', errorBody.kind);\n\n let logLevel: LogLevel;\n\n // If the thrown error is a ProtocolErrorWithLevel, its explicit logLevel takes precedence\n if (thrown instanceof ProtocolErrorWithLevel) {\n logLevel = thrown.logLevel;\n }\n // Errors with errno or transient socket codes are low-level, transient I/O issues\n // (e.g., EPIPE, ECONNRESET) and should be warnings, not errors\n else if (\n hasErrno(thrown) ||\n hasTransientSocketCode(thrown) ||\n isTransientSocketMessage(errorBody.message)\n ) {\n logLevel = 'warn';\n }\n // Fallback: check errorBody.kind for errors that weren't thrown as ProtocolErrorWithLevel\n else if (\n errorBody.kind === ErrorKind.ClientNotFound ||\n errorBody.kind === ErrorKind.TransformFailed\n ) {\n logLevel = 'warn';\n } else {\n logLevel = thrown ? getLogLevel(thrown) : 'info';\n }\n\n lc[logLevel]?.('Sending error on WebSocket', errorBody, thrown ?? '');\n send(lc, ws, ['error', errorBody], 'ignore-backpressure');\n}\n\nexport function findProtocolError(error: unknown): ProtocolError | undefined {\n if (isProtocolError(error)) {\n return error;\n }\n if (error instanceof Error && error.cause) {\n return findProtocolError(error.cause);\n }\n return undefined;\n}\n\nfunction hasErrno(error: unknown): boolean {\n return Boolean(\n error &&\n typeof error === 'object' &&\n 'errno' in error &&\n typeof (error as {errno: unknown}).errno !== 'undefined',\n );\n}\n\n// System error codes that indicate transient socket conditions.\n// These are checked via the `code` property on errors.\nconst TRANSIENT_SOCKET_ERROR_CODES = new Set([\n 'EPIPE',\n 'ECONNRESET',\n 'ECANCELED',\n]);\n\n// Error messages that indicate transient socket conditions but don't have\n// standard error codes (e.g., WebSocket library errors).\nconst TRANSIENT_SOCKET_MESSAGE_PATTERNS = [\n 'socket was closed while data was being compressed',\n];\n\nfunction hasTransientSocketCode(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const maybeCode =\n 'code' in error ? String((error as {code?: unknown}).code) : undefined;\n return Boolean(\n maybeCode && TRANSIENT_SOCKET_ERROR_CODES.has(maybeCode.toUpperCase()),\n );\n}\n\nfunction isTransientSocketMessage(message: string | undefined): boolean {\n if (!message) {\n return false;\n }\n const lower = message.toLowerCase();\n return TRANSIENT_SOCKET_MESSAGE_PATTERNS.some(pattern =>\n lower.includes(pattern),\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@ import { Subscribable } from '../../../shared/src/subscribable.ts';
|
|
|
2
2
|
import { ConnectionStatus } from './connection-status.ts';
|
|
3
3
|
import { type AuthError, type ClosedError, type DisconnectedReason, type ZeroError } from './error.ts';
|
|
4
4
|
export type ConnectionManagerState = {
|
|
5
|
+
name: ConnectionStatus.Initializing;
|
|
6
|
+
} | {
|
|
5
7
|
name: ConnectionStatus.Disconnected;
|
|
6
8
|
reason: DisconnectedReason;
|
|
7
9
|
} | {
|
|
@@ -75,6 +77,24 @@ export declare class ConnectionManager extends Subscribable<ConnectionManagerSta
|
|
|
75
77
|
* @returns true if a pending request was handled.
|
|
76
78
|
*/
|
|
77
79
|
resumeFromConnectRequest(): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Transition from initializing to connecting, once the local store is
|
|
82
|
+
* loaded.
|
|
83
|
+
*
|
|
84
|
+
* This is where the connecting window starts. Loading the local store can
|
|
85
|
+
* take tens of seconds on a slow device with a large replica, and none of
|
|
86
|
+
* that says anything about whether the server is reachable, so it is not
|
|
87
|
+
* charged against the disconnect timeout. No connect attempt has been made
|
|
88
|
+
* yet, so `attempt` is 0.
|
|
89
|
+
*
|
|
90
|
+
* A no-op in any other state: something else, like `close()`, has already
|
|
91
|
+
* moved the state on.
|
|
92
|
+
*
|
|
93
|
+
* @returns An object containing a promise that resolves on the next state change.
|
|
94
|
+
*/
|
|
95
|
+
initialized(): {
|
|
96
|
+
nextStatePromise: Promise<ConnectionManagerState>;
|
|
97
|
+
};
|
|
78
98
|
/**
|
|
79
99
|
* Transition to connecting state.
|
|
80
100
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,YAAY,EAAC,MAAM,qCAAqC,CAAC;AAEjE,OAAO,EAAC,gBAAgB,EAAC,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAIpB,MAAM,MAAM,sBAAsB,GAC9B;IACE,IAAI,EAAE,gBAAgB,CAAC,YAAY,CAAC;IACpC,MAAM,EAAE,kBAAkB,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,gBAAgB,CAAC,UAAU,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAChC,GACD;IACE,IAAI,EAAE,gBAAgB,CAAC,SAAS,CAAC;CAClC,GACD;IACE,IAAI,EAAE,gBAAgB,CAAC,SAAS,CAAC;IACjC,MAAM,EAAE,SAAS,CAAC;CACnB,GACD;IACE,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC;IAC7B,MAAM,EAAE,SAAS,CAAC;CACnB,GACD;IACE,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC;IAC9B,MAAM,EAAE,WAAW,CAAC;CACrB,CAAC;AAEN,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEF,QAAA,MAAM,eAAe,yBAGkB,CAAC;AAExC,KAAK,wBAAwB,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AACjE,KAAK,8BAA8B,GAAG,OAAO,CAC3C,sBAAsB,EACtB;IAAC,IAAI,EAAE,wBAAwB,CAAA;CAAC,CACjC,CAAC;AAEF,qBAAa,iBAAkB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;;gBAuC7D,OAAO,EAAE,wBAAwB;
|
|
1
|
+
{"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Closed, Connected, Connecting, Disconnected, Error, NeedsAuth } from "./connection-status-enum.js";
|
|
1
|
+
import { Closed, Connected, Connecting, Disconnected, Error, Initializing, NeedsAuth } from "./connection-status-enum.js";
|
|
2
2
|
import { Subscribable } from "../../../shared/src/subscribable.js";
|
|
3
3
|
import { ClientClosed, Offline } from "./client-error-kind-enum.js";
|
|
4
4
|
import { ClientError, isClientError } from "./error.js";
|
|
@@ -40,17 +40,10 @@ var ConnectionManager = class ConnectionManager extends Subscribable {
|
|
|
40
40
|
#stateChangeResolver = resolver();
|
|
41
41
|
constructor(options) {
|
|
42
42
|
super();
|
|
43
|
-
const now = Date.now();
|
|
44
43
|
this.#disconnectTimeout = options.disconnectTimeout;
|
|
45
44
|
this.#timeoutCheckIntervalMs = options.timeoutCheckIntervalMs ?? DEFAULT_TIMEOUT_CHECK_INTERVAL_MS;
|
|
46
|
-
this.#state = {
|
|
47
|
-
|
|
48
|
-
attempt: 0,
|
|
49
|
-
disconnectAt: now + this.#disconnectTimeout
|
|
50
|
-
};
|
|
51
|
-
this.#connectingStartedAt = now;
|
|
52
|
-
this.#lastTimeoutCheckAt = now;
|
|
53
|
-
this.#maybeStartTimeoutInterval();
|
|
45
|
+
this.#state = { name: Initializing };
|
|
46
|
+
this.#lastTimeoutCheckAt = Date.now();
|
|
54
47
|
}
|
|
55
48
|
get state() {
|
|
56
49
|
return this.#state;
|
|
@@ -115,6 +108,33 @@ var ConnectionManager = class ConnectionManager extends Subscribable {
|
|
|
115
108
|
return true;
|
|
116
109
|
}
|
|
117
110
|
/**
|
|
111
|
+
* Transition from initializing to connecting, once the local store is
|
|
112
|
+
* loaded.
|
|
113
|
+
*
|
|
114
|
+
* This is where the connecting window starts. Loading the local store can
|
|
115
|
+
* take tens of seconds on a slow device with a large replica, and none of
|
|
116
|
+
* that says anything about whether the server is reachable, so it is not
|
|
117
|
+
* charged against the disconnect timeout. No connect attempt has been made
|
|
118
|
+
* yet, so `attempt` is 0.
|
|
119
|
+
*
|
|
120
|
+
* A no-op in any other state: something else, like `close()`, has already
|
|
121
|
+
* moved the state on.
|
|
122
|
+
*
|
|
123
|
+
* @returns An object containing a promise that resolves on the next state change.
|
|
124
|
+
*/
|
|
125
|
+
initialized() {
|
|
126
|
+
if (this.#state.name !== "initializing") return { nextStatePromise: this.#nextStatePromise() };
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
this.#connectingStartedAt = now;
|
|
129
|
+
this.#state = {
|
|
130
|
+
name: Connecting,
|
|
131
|
+
attempt: 0,
|
|
132
|
+
disconnectAt: now + this.#disconnectTimeout
|
|
133
|
+
};
|
|
134
|
+
this.#maybeStartTimeoutInterval();
|
|
135
|
+
return { nextStatePromise: this.#publishStateAndGetPromise() };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
118
138
|
* Transition to connecting state.
|
|
119
139
|
*
|
|
120
140
|
* This starts the timeout timer, but if we've entered disconnected state,
|
|
@@ -137,9 +157,8 @@ var ConnectionManager = class ConnectionManager extends Subscribable {
|
|
|
137
157
|
attempt: this.#state.attempt + 1,
|
|
138
158
|
reason
|
|
139
159
|
};
|
|
140
|
-
const nextStatePromise = this.#publishStateAndGetPromise();
|
|
141
160
|
this.#maybeStartTimeoutInterval();
|
|
142
|
-
return { nextStatePromise };
|
|
161
|
+
return { nextStatePromise: this.#publishStateAndGetPromise() };
|
|
143
162
|
}
|
|
144
163
|
if (this.#connectingStartedAt === void 0) this.#connectingStartedAt = now;
|
|
145
164
|
const disconnectAt = this.#connectingStartedAt + this.#disconnectTimeout;
|
|
@@ -149,9 +168,8 @@ var ConnectionManager = class ConnectionManager extends Subscribable {
|
|
|
149
168
|
disconnectAt,
|
|
150
169
|
reason
|
|
151
170
|
};
|
|
152
|
-
const nextStatePromise = this.#publishStateAndGetPromise();
|
|
153
171
|
this.#maybeStartTimeoutInterval();
|
|
154
|
-
return { nextStatePromise };
|
|
172
|
+
return { nextStatePromise: this.#publishStateAndGetPromise() };
|
|
155
173
|
}
|
|
156
174
|
/**
|
|
157
175
|
* Transition to connected state.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-manager.js","names":["#disconnectTimeout","#timeoutCheckIntervalMs","#state","#connectingStartedAt","#lastTimeoutCheckAt","#maybeStartTimeoutInterval","#nextStatePromise","#connectRequestResolver","#consumeConnectRequest","#publishStateAndGetPromise","#maybeStopTimeoutInterval","#publishState","#resolveNextStateWaiters","#stateChangeResolver","#timeoutInterval","#creditFrozenTime","#checkTimeout"],"sources":["../../../../../zero-client/src/client/connection-manager.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport {ConnectionStatus} from './connection-status.ts';\nimport {\n ClientError,\n isClientError,\n type AuthError,\n type ClosedError,\n type DisconnectedReason,\n type ZeroError,\n} from './error.ts';\n\nconst DEFAULT_TIMEOUT_CHECK_INTERVAL_MS = 1_000;\n\nexport type ConnectionManagerState =\n | {\n name: ConnectionStatus.Disconnected;\n reason: DisconnectedReason;\n }\n | {\n name: ConnectionStatus.Connecting;\n attempt: number;\n disconnectAt: number;\n reason?: ZeroError | undefined;\n }\n | {\n name: ConnectionStatus.Connected;\n }\n | {\n name: ConnectionStatus.NeedsAuth;\n reason: AuthError;\n }\n | {\n name: ConnectionStatus.Error;\n reason: ZeroError;\n }\n | {\n name: ConnectionStatus.Closed;\n reason: ClosedError;\n };\n\nexport type ConnectionManagerOptions = {\n /**\n * The amount of milliseconds we allow for continuous connecting attempts before\n * transitioning to disconnected state.\n */\n disconnectTimeout: number;\n /**\n * How frequently we check whether the connecting timeout has elapsed.\n * Defaults to 1 second.\n */\n timeoutCheckIntervalMs?: number | undefined;\n};\n\nconst TERMINAL_STATES = [\n ConnectionStatus.NeedsAuth,\n ConnectionStatus.Error,\n] as const satisfies ConnectionStatus[];\n\ntype TerminalConnectionStatus = (typeof TERMINAL_STATES)[number];\ntype TerminalConnectionManagerState = Extract<\n ConnectionManagerState,\n {name: TerminalConnectionStatus}\n>;\n\nexport class ConnectionManager extends Subscribable<ConnectionManagerState> {\n #state: ConnectionManagerState;\n #connectRequestResolver: Resolver<void> | undefined = resolver();\n\n /**\n * The timestamp when we first started trying to connect.\n * This is used to track the retry window.\n * Reset to undefined when we successfully connect or when we transition to disconnected.\n */\n #connectingStartedAt: number | undefined;\n\n /**\n * The amount of milliseconds we allow for continuous connecting attempts before\n * transitioning to disconnected state.\n */\n #disconnectTimeout: number;\n\n /**\n * Handle for the timeout interval that periodically checks whether we've\n * exceeded the allowed connecting window.\n */\n #timeoutInterval: ReturnType<typeof setInterval> | undefined;\n\n /**\n * Interval duration for checking whether the connecting timeout has elapsed.\n */\n #timeoutCheckIntervalMs: number;\n\n /**\n * `Date.now()` at the last timeout-interval tick, used to notice that the\n * JavaScript event loop was not running. See {@linkcode #creditFrozenTime}.\n */\n #lastTimeoutCheckAt: number;\n\n /**\n * Resolver used to signal waiting callers when the state changes.\n */\n #stateChangeResolver: Resolver<ConnectionManagerState> = resolver();\n\n constructor(options: ConnectionManagerOptions) {\n super();\n\n const now = Date.now();\n\n this.#disconnectTimeout = options.disconnectTimeout;\n this.#timeoutCheckIntervalMs =\n options.timeoutCheckIntervalMs ?? DEFAULT_TIMEOUT_CHECK_INTERVAL_MS;\n this.#state = {\n name: ConnectionStatus.Connecting,\n attempt: 0,\n disconnectAt: now + this.#disconnectTimeout,\n };\n this.#connectingStartedAt = now;\n this.#lastTimeoutCheckAt = now;\n this.#maybeStartTimeoutInterval();\n }\n\n get state(): ConnectionManagerState {\n return this.#state;\n }\n\n /**\n * Returns true if the current state is equal to the given status.\n */\n is(status: ConnectionStatus): boolean {\n return this.#state.name === status;\n }\n\n /**\n * Returns true if the current state is a terminal state\n * that can be recovered from by calling connect().\n */\n isInTerminalState(): boolean {\n return ConnectionManager.isTerminalState(this.#state);\n }\n\n /**\n * Returns true if the given status is a terminal state\n * that can be recovered from by calling connect().\n */\n static isTerminalState(\n state: ConnectionManagerState,\n ): state is TerminalConnectionManagerState {\n return (TERMINAL_STATES as readonly ConnectionStatus[]).includes(\n state.name,\n );\n }\n\n /**\n * Returns true if the run loop should continue.\n * The run loop continues in all states except closed.\n * In needs-auth and error states, the run loop pauses and waits for connect() to be called.\n */\n shouldContinueRunLoop(): boolean {\n return this.#state.name !== ConnectionStatus.Closed;\n }\n\n /**\n * Waits for the next state change.\n * @returns A promise that resolves when the next state change occurs.\n */\n waitForStateChange(): Promise<ConnectionManagerState> {\n return this.#nextStatePromise();\n }\n\n requestConnect(): void {\n if (this.#connectRequestResolver === undefined) {\n return;\n }\n this.#connectRequestResolver.resolve();\n this.#connectRequestResolver = undefined;\n }\n\n waitForConnectRequest(): Promise<void> {\n return this.#connectRequestResolver === undefined\n ? Promise.resolve()\n : this.#connectRequestResolver.promise;\n }\n\n #consumeConnectRequest(): boolean {\n if (this.#connectRequestResolver !== undefined) {\n return false;\n }\n this.#connectRequestResolver = resolver();\n return true;\n }\n\n /**\n * Consume a pending connect request and resume connecting.\n *\n * @returns true if a pending request was handled.\n */\n resumeFromConnectRequest(): boolean {\n if (!this.isInTerminalState()) {\n return false;\n }\n if (!this.#consumeConnectRequest()) {\n return false;\n }\n this.connecting();\n return true;\n }\n\n /**\n * Transition to connecting state.\n *\n * This starts the timeout timer, but if we've entered disconnected state,\n * we stay there and continue retrying.\n *\n * If we're disconnected due to a hidden tab, allow a transition back to\n * connecting when visibility returns.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n connecting(reason?: ZeroError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n const isHiddenDisconnect =\n this.#state.name === ConnectionStatus.Disconnected &&\n this.#state.reason.kind === ClientErrorKind.Hidden;\n\n // we cannot intentionally transition from disconnected to connecting\n // unless the disconnection was due to a hidden tab\n if (\n this.#state.name === ConnectionStatus.Disconnected &&\n !isHiddenDisconnect\n ) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n const now = Date.now();\n\n if (isHiddenDisconnect) {\n // Reset the retry window after a hidden-tab disconnect.\n this.#connectingStartedAt = undefined;\n }\n\n // If we're already connecting, increment the attempt counter\n if (this.#state.name === ConnectionStatus.Connecting) {\n this.#state = {\n ...this.#state,\n attempt: this.#state.attempt + 1,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n this.#maybeStartTimeoutInterval();\n return {nextStatePromise};\n }\n\n // Starting a new connecting session\n // If #connectingStartedAt is undefined, this is a fresh start - set it to now\n // If it's already set, we're retrying within the same retry window, so keep it\n if (this.#connectingStartedAt === undefined) {\n this.#connectingStartedAt = now;\n }\n\n const disconnectAt = this.#connectingStartedAt + this.#disconnectTimeout;\n\n this.#state = {\n name: ConnectionStatus.Connecting,\n attempt: 1,\n disconnectAt,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n this.#maybeStartTimeoutInterval();\n return {nextStatePromise};\n }\n\n /**\n * Transition to connected state.\n * This resets the connecting timeout timer.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n connected(): {nextStatePromise: Promise<ConnectionManagerState>} {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already connected, no-op\n if (this.#state.name === ConnectionStatus.Connected) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer on successful connection\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Connected,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to disconnected state.\n * This is called when the timeout expires.\n * The run loop will continue trying to reconnect.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n disconnected(reason: DisconnectedReason): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already disconnected, no-op\n if (this.#state.name === ConnectionStatus.Disconnected) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // When transitioning from connected to disconnected, we've lost a connection\n // we previously had. Clear the timeout timer so we can start a fresh timeout window.\n if (this.#state.name === ConnectionStatus.Connected) {\n this.#connectingStartedAt = undefined;\n }\n // When transitioning from connecting to disconnected (e.g., due to timeout),\n // we keep the start time to maintain the context that we've been trying for a while.\n\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Disconnected,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to needs-auth state.\n * This pauses the run loop until connect() is called with new credentials.\n * Resets the retry window and attempt counter.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n needsAuth(reason: AuthError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already in needs-auth state, no-op\n if (this.#state.name === ConnectionStatus.NeedsAuth) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer and connecting start time\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.NeedsAuth,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to error state.\n * This pauses the run loop until connect() is called.\n * Resets the retry window and attempt counter.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n error(reason: ZeroError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already in error state, no-op\n if (this.#state.name === ConnectionStatus.Error) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer and connecting start time\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Error,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to closed state.\n * This is terminal - no further transitions are allowed.\n */\n closed() {\n // Already closed, no-op\n if (this.#state.name === ConnectionStatus.Closed) {\n return;\n }\n\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Closed,\n reason: new ClientError({\n kind: ClientErrorKind.ClientClosed,\n message: 'Zero was explicitly closed by calling zero.close()',\n }),\n };\n this.#publishState();\n this.cleanup();\n return;\n }\n\n override cleanup = (): void => {\n this._listeners.clear();\n this.#resolveNextStateWaiters();\n };\n\n #resolveNextStateWaiters(): void {\n this.#stateChangeResolver.resolve(this.#state);\n this.#stateChangeResolver = resolver();\n }\n\n #publishState(): void {\n this.notify(this.#state);\n this.#resolveNextStateWaiters();\n }\n\n #nextStatePromise(): Promise<ConnectionManagerState> {\n return this.#stateChangeResolver.promise;\n }\n\n #publishStateAndGetPromise(): Promise<ConnectionManagerState> {\n this.#publishState();\n return this.#nextStatePromise();\n }\n\n /**\n * Pushes the disconnect deadline forward by however long the event loop was\n * not running.\n *\n * `disconnectAt` is an absolute wall-clock instant, but what it is meant to\n * bound is time spent *actually retrying*. Whenever the loop is frozen --- a\n * suspended React Native app, a sleeping laptop, a throttled background tab\n * --- wall-clock keeps advancing while no reconnect attempt can run, so\n * without this the first tick after resume reports `Offline` having given the\n * client zero live seconds to connect.\n *\n * This interval is its own detector: a tick that lands late by more than a\n * full period means we were not running in between. A merely busy JS thread\n * can also delay a tick, and we cannot tell the two apart from lateness\n * alone, but the costs are asymmetric --- crediting a busy period back just\n * grants a bit more time to connect, whereas failing to credit a real freeze\n * produces a user-visible bogus disconnect --- so we credit.\n *\n * The whole gap is credited, not `elapsed` minus a period. An overdue\n * `setInterval` callback runs as soon as the loop resumes rather than waiting\n * out another period, so `elapsed` is already about the frozen duration;\n * netting off a period would charge up to a full period of frozen time\n * against the deadline. Freezing just after a tick would then leave the\n * deadline exactly at `now` on resume and disconnect immediately, which is\n * the very thing this is here to prevent.\n */\n #creditFrozenTime(): void {\n const now = Date.now();\n const elapsed = now - this.#lastTimeoutCheckAt;\n this.#lastTimeoutCheckAt = now;\n\n // Ordinary scheduling jitter, not a freeze.\n if (elapsed <= 2 * this.#timeoutCheckIntervalMs) {\n return;\n }\n const frozenMs = elapsed;\n\n // Advance the window's origin so that a later `connecting()` that starts a\n // fresh session recomputes a deadline that also excludes the frozen time.\n if (this.#connectingStartedAt !== undefined) {\n this.#connectingStartedAt += frozenMs;\n }\n\n if (this.#state.name === ConnectionStatus.Connecting) {\n // Deliberately not published: the status has not changed, and waking the\n // run loop's `waitForStateChange` racers here would interrupt an\n // in-flight connect attempt for no reason. Readers of `state` still see\n // the corrected deadline.\n this.#state = {\n ...this.#state,\n disconnectAt: this.#state.disconnectAt + frozenMs,\n };\n }\n }\n\n /**\n * Check if we should transition from connecting to disconnected due to timeout.\n * Returns true if the transition happened.\n */\n #checkTimeout(): boolean {\n if (this.#state.name !== ConnectionStatus.Connecting) {\n return false;\n }\n\n const now = Date.now();\n if (now >= this.#state.disconnectAt) {\n this.disconnected(\n new ClientError({\n kind: ClientErrorKind.Offline,\n message: `Zero was unable to connect for ${Math.floor(this.#disconnectTimeout / 1_000)} seconds and was disconnected`,\n }),\n );\n return true;\n }\n\n return false;\n }\n\n #maybeStartTimeoutInterval(): void {\n if (this.#timeoutInterval !== undefined) {\n return;\n }\n this.#lastTimeoutCheckAt = Date.now();\n this.#timeoutInterval = setInterval(() => {\n this.#creditFrozenTime();\n this.#checkTimeout();\n }, this.#timeoutCheckIntervalMs);\n }\n\n #maybeStopTimeoutInterval(): void {\n if (this.#timeoutInterval === undefined) {\n return;\n }\n clearInterval(this.#timeoutInterval);\n this.#timeoutInterval = undefined;\n }\n}\n\n/**\n * Used to trigger the catch block when a terminal state is reached.\n *\n * @param state - The current connection state.\n */\nexport const throwIfConnectionError = (state: ConnectionManagerState) => {\n if (\n ConnectionManager.isTerminalState(state) ||\n state.name === ConnectionStatus.Closed ||\n ((state.name === ConnectionStatus.Connecting ||\n state.name === ConnectionStatus.Disconnected) &&\n state.reason)\n ) {\n if (\n isClientError(state.reason) &&\n (state.reason.kind === ClientErrorKind.ConnectTimeout ||\n state.reason.kind === ClientErrorKind.AbruptClose ||\n state.reason.kind === ClientErrorKind.CleanClose ||\n state.reason.kind === ClientErrorKind.Hidden)\n ) {\n return;\n }\n throw state.reason;\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection-manager.js","names":["#disconnectTimeout","#timeoutCheckIntervalMs","#state","#lastTimeoutCheckAt","#nextStatePromise","#connectRequestResolver","#consumeConnectRequest","#connectingStartedAt","#maybeStartTimeoutInterval","#publishStateAndGetPromise","#maybeStopTimeoutInterval","#publishState","#resolveNextStateWaiters","#stateChangeResolver","#timeoutInterval","#creditFrozenTime","#checkTimeout"],"sources":["../../../../../zero-client/src/client/connection-manager.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport {ConnectionStatus} from './connection-status.ts';\nimport {\n ClientError,\n isClientError,\n type AuthError,\n type ClosedError,\n type DisconnectedReason,\n type ZeroError,\n} from './error.ts';\n\nconst DEFAULT_TIMEOUT_CHECK_INTERVAL_MS = 1_000;\n\nexport type ConnectionManagerState =\n | {\n name: ConnectionStatus.Initializing;\n }\n | {\n name: ConnectionStatus.Disconnected;\n reason: DisconnectedReason;\n }\n | {\n name: ConnectionStatus.Connecting;\n attempt: number;\n disconnectAt: number;\n reason?: ZeroError | undefined;\n }\n | {\n name: ConnectionStatus.Connected;\n }\n | {\n name: ConnectionStatus.NeedsAuth;\n reason: AuthError;\n }\n | {\n name: ConnectionStatus.Error;\n reason: ZeroError;\n }\n | {\n name: ConnectionStatus.Closed;\n reason: ClosedError;\n };\n\nexport type ConnectionManagerOptions = {\n /**\n * The amount of milliseconds we allow for continuous connecting attempts before\n * transitioning to disconnected state.\n */\n disconnectTimeout: number;\n /**\n * How frequently we check whether the connecting timeout has elapsed.\n * Defaults to 1 second.\n */\n timeoutCheckIntervalMs?: number | undefined;\n};\n\nconst TERMINAL_STATES = [\n ConnectionStatus.NeedsAuth,\n ConnectionStatus.Error,\n] as const satisfies ConnectionStatus[];\n\ntype TerminalConnectionStatus = (typeof TERMINAL_STATES)[number];\ntype TerminalConnectionManagerState = Extract<\n ConnectionManagerState,\n {name: TerminalConnectionStatus}\n>;\n\nexport class ConnectionManager extends Subscribable<ConnectionManagerState> {\n #state: ConnectionManagerState;\n #connectRequestResolver: Resolver<void> | undefined = resolver();\n\n /**\n * The timestamp when we first started trying to connect.\n * This is used to track the retry window.\n * Reset to undefined when we successfully connect or when we transition to disconnected.\n */\n #connectingStartedAt: number | undefined;\n\n /**\n * The amount of milliseconds we allow for continuous connecting attempts before\n * transitioning to disconnected state.\n */\n #disconnectTimeout: number;\n\n /**\n * Handle for the timeout interval that periodically checks whether we've\n * exceeded the allowed connecting window.\n */\n #timeoutInterval: ReturnType<typeof setInterval> | undefined;\n\n /**\n * Interval duration for checking whether the connecting timeout has elapsed.\n */\n #timeoutCheckIntervalMs: number;\n\n /**\n * `Date.now()` at the last timeout-interval tick, used to notice that the\n * JavaScript event loop was not running. See {@linkcode #creditFrozenTime}.\n */\n #lastTimeoutCheckAt: number;\n\n /**\n * Resolver used to signal waiting callers when the state changes.\n */\n #stateChangeResolver: Resolver<ConnectionManagerState> = resolver();\n\n constructor(options: ConnectionManagerOptions) {\n super();\n\n this.#disconnectTimeout = options.disconnectTimeout;\n this.#timeoutCheckIntervalMs =\n options.timeoutCheckIntervalMs ?? DEFAULT_TIMEOUT_CHECK_INTERVAL_MS;\n // The connecting window, and the timer that enforces it, start in\n // initialized(), once the local store is loaded.\n this.#state = {name: ConnectionStatus.Initializing};\n this.#lastTimeoutCheckAt = Date.now();\n }\n\n get state(): ConnectionManagerState {\n return this.#state;\n }\n\n /**\n * Returns true if the current state is equal to the given status.\n */\n is(status: ConnectionStatus): boolean {\n return this.#state.name === status;\n }\n\n /**\n * Returns true if the current state is a terminal state\n * that can be recovered from by calling connect().\n */\n isInTerminalState(): boolean {\n return ConnectionManager.isTerminalState(this.#state);\n }\n\n /**\n * Returns true if the given status is a terminal state\n * that can be recovered from by calling connect().\n */\n static isTerminalState(\n state: ConnectionManagerState,\n ): state is TerminalConnectionManagerState {\n return (TERMINAL_STATES as readonly ConnectionStatus[]).includes(\n state.name,\n );\n }\n\n /**\n * Returns true if the run loop should continue.\n * The run loop continues in all states except closed.\n * In needs-auth and error states, the run loop pauses and waits for connect() to be called.\n */\n shouldContinueRunLoop(): boolean {\n return this.#state.name !== ConnectionStatus.Closed;\n }\n\n /**\n * Waits for the next state change.\n * @returns A promise that resolves when the next state change occurs.\n */\n waitForStateChange(): Promise<ConnectionManagerState> {\n return this.#nextStatePromise();\n }\n\n requestConnect(): void {\n if (this.#connectRequestResolver === undefined) {\n return;\n }\n this.#connectRequestResolver.resolve();\n this.#connectRequestResolver = undefined;\n }\n\n waitForConnectRequest(): Promise<void> {\n return this.#connectRequestResolver === undefined\n ? Promise.resolve()\n : this.#connectRequestResolver.promise;\n }\n\n #consumeConnectRequest(): boolean {\n if (this.#connectRequestResolver !== undefined) {\n return false;\n }\n this.#connectRequestResolver = resolver();\n return true;\n }\n\n /**\n * Consume a pending connect request and resume connecting.\n *\n * @returns true if a pending request was handled.\n */\n resumeFromConnectRequest(): boolean {\n if (!this.isInTerminalState()) {\n return false;\n }\n if (!this.#consumeConnectRequest()) {\n return false;\n }\n this.connecting();\n return true;\n }\n\n /**\n * Transition from initializing to connecting, once the local store is\n * loaded.\n *\n * This is where the connecting window starts. Loading the local store can\n * take tens of seconds on a slow device with a large replica, and none of\n * that says anything about whether the server is reachable, so it is not\n * charged against the disconnect timeout. No connect attempt has been made\n * yet, so `attempt` is 0.\n *\n * A no-op in any other state: something else, like `close()`, has already\n * moved the state on.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n initialized(): {nextStatePromise: Promise<ConnectionManagerState>} {\n if (this.#state.name !== ConnectionStatus.Initializing) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n const now = Date.now();\n this.#connectingStartedAt = now;\n this.#state = {\n name: ConnectionStatus.Connecting,\n attempt: 0,\n disconnectAt: now + this.#disconnectTimeout,\n };\n // Start the interval before publishing: a subscriber may close the manager\n // synchronously, and closed() can only stop an interval that exists.\n this.#maybeStartTimeoutInterval();\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to connecting state.\n *\n * This starts the timeout timer, but if we've entered disconnected state,\n * we stay there and continue retrying.\n *\n * If we're disconnected due to a hidden tab, allow a transition back to\n * connecting when visibility returns.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n connecting(reason?: ZeroError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n const isHiddenDisconnect =\n this.#state.name === ConnectionStatus.Disconnected &&\n this.#state.reason.kind === ClientErrorKind.Hidden;\n\n // we cannot intentionally transition from disconnected to connecting\n // unless the disconnection was due to a hidden tab\n if (\n this.#state.name === ConnectionStatus.Disconnected &&\n !isHiddenDisconnect\n ) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n const now = Date.now();\n\n if (isHiddenDisconnect) {\n // Reset the retry window after a hidden-tab disconnect.\n this.#connectingStartedAt = undefined;\n }\n\n // If we're already connecting, increment the attempt counter\n if (this.#state.name === ConnectionStatus.Connecting) {\n this.#state = {\n ...this.#state,\n attempt: this.#state.attempt + 1,\n reason,\n };\n // See initialized() for why the interval starts before publishing.\n this.#maybeStartTimeoutInterval();\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n // Starting a new connecting session\n // If #connectingStartedAt is undefined, this is a fresh start - set it to now\n // If it's already set, we're retrying within the same retry window, so keep it\n if (this.#connectingStartedAt === undefined) {\n this.#connectingStartedAt = now;\n }\n\n const disconnectAt = this.#connectingStartedAt + this.#disconnectTimeout;\n\n this.#state = {\n name: ConnectionStatus.Connecting,\n attempt: 1,\n disconnectAt,\n reason,\n };\n // Start the interval before publishing: a subscriber may close the manager\n // synchronously, and closed() can only stop an interval that exists.\n this.#maybeStartTimeoutInterval();\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to connected state.\n * This resets the connecting timeout timer.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n connected(): {nextStatePromise: Promise<ConnectionManagerState>} {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already connected, no-op\n if (this.#state.name === ConnectionStatus.Connected) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer on successful connection\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Connected,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to disconnected state.\n * This is called when the timeout expires.\n * The run loop will continue trying to reconnect.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n disconnected(reason: DisconnectedReason): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already disconnected, no-op\n if (this.#state.name === ConnectionStatus.Disconnected) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // When transitioning from connected to disconnected, we've lost a connection\n // we previously had. Clear the timeout timer so we can start a fresh timeout window.\n if (this.#state.name === ConnectionStatus.Connected) {\n this.#connectingStartedAt = undefined;\n }\n // When transitioning from connecting to disconnected (e.g., due to timeout),\n // we keep the start time to maintain the context that we've been trying for a while.\n\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Disconnected,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to needs-auth state.\n * This pauses the run loop until connect() is called with new credentials.\n * Resets the retry window and attempt counter.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n needsAuth(reason: AuthError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already in needs-auth state, no-op\n if (this.#state.name === ConnectionStatus.NeedsAuth) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer and connecting start time\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.NeedsAuth,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to error state.\n * This pauses the run loop until connect() is called.\n * Resets the retry window and attempt counter.\n *\n * @returns An object containing a promise that resolves on the next state change.\n */\n error(reason: ZeroError): {\n nextStatePromise: Promise<ConnectionManagerState>;\n } {\n // cannot transition from closed to any other status\n if (this.#state.name === ConnectionStatus.Closed) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Already in error state, no-op\n if (this.#state.name === ConnectionStatus.Error) {\n return {nextStatePromise: this.#nextStatePromise()};\n }\n\n // Reset the timeout timer and connecting start time\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Error,\n reason,\n };\n const nextStatePromise = this.#publishStateAndGetPromise();\n return {nextStatePromise};\n }\n\n /**\n * Transition to closed state.\n * This is terminal - no further transitions are allowed.\n */\n closed() {\n // Already closed, no-op\n if (this.#state.name === ConnectionStatus.Closed) {\n return;\n }\n\n this.#connectingStartedAt = undefined;\n this.#maybeStopTimeoutInterval();\n\n this.#state = {\n name: ConnectionStatus.Closed,\n reason: new ClientError({\n kind: ClientErrorKind.ClientClosed,\n message: 'Zero was explicitly closed by calling zero.close()',\n }),\n };\n this.#publishState();\n this.cleanup();\n return;\n }\n\n override cleanup = (): void => {\n this._listeners.clear();\n this.#resolveNextStateWaiters();\n };\n\n #resolveNextStateWaiters(): void {\n this.#stateChangeResolver.resolve(this.#state);\n this.#stateChangeResolver = resolver();\n }\n\n #publishState(): void {\n this.notify(this.#state);\n this.#resolveNextStateWaiters();\n }\n\n #nextStatePromise(): Promise<ConnectionManagerState> {\n return this.#stateChangeResolver.promise;\n }\n\n #publishStateAndGetPromise(): Promise<ConnectionManagerState> {\n this.#publishState();\n return this.#nextStatePromise();\n }\n\n /**\n * Pushes the disconnect deadline forward by however long the event loop was\n * not running.\n *\n * `disconnectAt` is an absolute wall-clock instant, but what it is meant to\n * bound is time spent *actually retrying*. Whenever the loop is frozen --- a\n * suspended React Native app, a sleeping laptop, a throttled background tab\n * --- wall-clock keeps advancing while no reconnect attempt can run, so\n * without this the first tick after resume reports `Offline` having given the\n * client zero live seconds to connect.\n *\n * This interval is its own detector: a tick that lands late by more than a\n * full period means we were not running in between. A merely busy JS thread\n * can also delay a tick, and we cannot tell the two apart from lateness\n * alone, but the costs are asymmetric --- crediting a busy period back just\n * grants a bit more time to connect, whereas failing to credit a real freeze\n * produces a user-visible bogus disconnect --- so we credit.\n *\n * The whole gap is credited, not `elapsed` minus a period. An overdue\n * `setInterval` callback runs as soon as the loop resumes rather than waiting\n * out another period, so `elapsed` is already about the frozen duration;\n * netting off a period would charge up to a full period of frozen time\n * against the deadline. Freezing just after a tick would then leave the\n * deadline exactly at `now` on resume and disconnect immediately, which is\n * the very thing this is here to prevent.\n */\n #creditFrozenTime(): void {\n const now = Date.now();\n const elapsed = now - this.#lastTimeoutCheckAt;\n this.#lastTimeoutCheckAt = now;\n\n // Ordinary scheduling jitter, not a freeze.\n if (elapsed <= 2 * this.#timeoutCheckIntervalMs) {\n return;\n }\n const frozenMs = elapsed;\n\n // Advance the window's origin so that a later `connecting()` that starts a\n // fresh session recomputes a deadline that also excludes the frozen time.\n if (this.#connectingStartedAt !== undefined) {\n this.#connectingStartedAt += frozenMs;\n }\n\n if (this.#state.name === ConnectionStatus.Connecting) {\n // Deliberately not published: the status has not changed, and waking the\n // run loop's `waitForStateChange` racers here would interrupt an\n // in-flight connect attempt for no reason. Readers of `state` still see\n // the corrected deadline.\n this.#state = {\n ...this.#state,\n disconnectAt: this.#state.disconnectAt + frozenMs,\n };\n }\n }\n\n /**\n * Check if we should transition from connecting to disconnected due to timeout.\n * Returns true if the transition happened.\n */\n #checkTimeout(): boolean {\n if (this.#state.name !== ConnectionStatus.Connecting) {\n return false;\n }\n\n const now = Date.now();\n if (now >= this.#state.disconnectAt) {\n this.disconnected(\n new ClientError({\n kind: ClientErrorKind.Offline,\n message: `Zero was unable to connect for ${Math.floor(this.#disconnectTimeout / 1_000)} seconds and was disconnected`,\n }),\n );\n return true;\n }\n\n return false;\n }\n\n #maybeStartTimeoutInterval(): void {\n if (this.#timeoutInterval !== undefined) {\n return;\n }\n this.#lastTimeoutCheckAt = Date.now();\n this.#timeoutInterval = setInterval(() => {\n this.#creditFrozenTime();\n this.#checkTimeout();\n }, this.#timeoutCheckIntervalMs);\n }\n\n #maybeStopTimeoutInterval(): void {\n if (this.#timeoutInterval === undefined) {\n return;\n }\n clearInterval(this.#timeoutInterval);\n this.#timeoutInterval = undefined;\n }\n}\n\n/**\n * Used to trigger the catch block when a terminal state is reached.\n *\n * @param state - The current connection state.\n */\nexport const throwIfConnectionError = (state: ConnectionManagerState) => {\n if (\n ConnectionManager.isTerminalState(state) ||\n state.name === ConnectionStatus.Closed ||\n ((state.name === ConnectionStatus.Connecting ||\n state.name === ConnectionStatus.Disconnected) &&\n state.reason)\n ) {\n if (\n isClientError(state.reason) &&\n (state.reason.kind === ClientErrorKind.ConnectTimeout ||\n state.reason.kind === ClientErrorKind.AbruptClose ||\n state.reason.kind === ClientErrorKind.CleanClose ||\n state.reason.kind === ClientErrorKind.Hidden)\n ) {\n return;\n }\n throw state.reason;\n }\n};\n"],"mappings":"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"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
export declare const Initializing = "initializing";
|
|
1
2
|
export declare const Disconnected = "disconnected";
|
|
2
3
|
export declare const Connecting = "connecting";
|
|
3
4
|
export declare const Connected = "connected";
|
|
4
5
|
export declare const NeedsAuth = "needs-auth";
|
|
5
6
|
export declare const Error = "error";
|
|
6
7
|
export declare const Closed = "closed";
|
|
8
|
+
export type Initializing = typeof Initializing;
|
|
7
9
|
export type Disconnected = typeof Disconnected;
|
|
8
10
|
export type Connecting = typeof Connecting;
|
|
9
11
|
export type Connected = typeof Connected;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-status-enum.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection-status-enum.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,UAAU,eAAe,CAAC;AACvC,eAAO,MAAM,SAAS,cAAc,CAAC;AACrC,eAAO,MAAM,SAAS,eAAe,CAAC;AACtC,eAAO,MAAM,KAAK,UAAU,CAAC;AAC7B,eAAO,MAAM,MAAM,WAAW,CAAC;AAE/B,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC;AAC/C,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC;AAC3C,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC;AACjC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC"}
|
|
1
|
+
{"version":3,"file":"connection-status-enum.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection-status-enum.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAC3C,eAAO,MAAM,UAAU,eAAe,CAAC;AACvC,eAAO,MAAM,SAAS,cAAc,CAAC;AACrC,eAAO,MAAM,SAAS,eAAe,CAAC;AACtC,eAAO,MAAM,KAAK,UAAU,CAAC;AAC7B,eAAO,MAAM,MAAM,WAAW,CAAC;AAE/B,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC;AAC/C,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC;AAC/C,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC;AAC3C,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AACzC,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC;AACjC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC"}
|
|
@@ -6,8 +6,10 @@ var connection_status_enum_exports = /* @__PURE__ */ __exportAll({
|
|
|
6
6
|
Connecting: () => Connecting,
|
|
7
7
|
Disconnected: () => Disconnected,
|
|
8
8
|
Error: () => Error,
|
|
9
|
+
Initializing: () => Initializing,
|
|
9
10
|
NeedsAuth: () => NeedsAuth
|
|
10
11
|
});
|
|
12
|
+
var Initializing = "initializing";
|
|
11
13
|
var Disconnected = "disconnected";
|
|
12
14
|
var Connecting = "connecting";
|
|
13
15
|
var Connected = "connected";
|
|
@@ -15,6 +17,6 @@ var NeedsAuth = "needs-auth";
|
|
|
15
17
|
var Error = "error";
|
|
16
18
|
var Closed = "closed";
|
|
17
19
|
//#endregion
|
|
18
|
-
export { Closed, Connected, Connecting, Disconnected, Error, NeedsAuth, connection_status_enum_exports };
|
|
20
|
+
export { Closed, Connected, Connecting, Disconnected, Error, Initializing, NeedsAuth, connection_status_enum_exports };
|
|
19
21
|
|
|
20
22
|
//# sourceMappingURL=connection-status-enum.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-status-enum.js","names":[],"sources":["../../../../../zero-client/src/client/connection-status-enum.ts"],"sourcesContent":["export const Disconnected = 'disconnected';\nexport const Connecting = 'connecting';\nexport const Connected = 'connected';\nexport const NeedsAuth = 'needs-auth';\nexport const Error = 'error';\nexport const Closed = 'closed';\n\nexport type Disconnected = typeof Disconnected;\nexport type Connecting = typeof Connecting;\nexport type Connected = typeof Connected;\nexport type NeedsAuth = typeof NeedsAuth;\nexport type Error = typeof Error;\nexport type Closed = typeof Closed;\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"connection-status-enum.js","names":[],"sources":["../../../../../zero-client/src/client/connection-status-enum.ts"],"sourcesContent":["export const Initializing = 'initializing';\nexport const Disconnected = 'disconnected';\nexport const Connecting = 'connecting';\nexport const Connected = 'connected';\nexport const NeedsAuth = 'needs-auth';\nexport const Error = 'error';\nexport const Closed = 'closed';\n\nexport type Initializing = typeof Initializing;\nexport type Disconnected = typeof Disconnected;\nexport type Connecting = typeof Connecting;\nexport type Connected = typeof Connected;\nexport type NeedsAuth = typeof NeedsAuth;\nexport type Error = typeof Error;\nexport type Closed = typeof Closed;\n"],"mappings":";;;;;;;;;;;AAAA,IAAa,eAAe;AAC5B,IAAa,eAAe;AAC5B,IAAa,aAAa;AAC1B,IAAa,YAAY;AACzB,IAAa,YAAY;AACzB,IAAa,QAAQ;AACrB,IAAa,SAAS"}
|
|
@@ -4,6 +4,10 @@ import type { ConnectionManager } from './connection-manager.ts';
|
|
|
4
4
|
/**
|
|
5
5
|
* The current connection state of the Zero instance. One of the following states:
|
|
6
6
|
*
|
|
7
|
+
* - `initializing`: The client is loading its local store. Nothing has been
|
|
8
|
+
* sent to the server yet, so time spent here depends on the device and the
|
|
9
|
+
* amount of local data, not on the network. Entered once, when the Zero
|
|
10
|
+
* instance is created, and followed by `connecting`.
|
|
7
11
|
* - `connecting`: The client is actively trying to connect every 5 seconds.
|
|
8
12
|
* - `disconnected`: The client is now in an "offline" state. It will continue
|
|
9
13
|
* to try to connect every 5 seconds.
|
|
@@ -16,6 +20,8 @@ import type { ConnectionManager } from './connection-manager.ts';
|
|
|
16
20
|
* a terminal state, and a new Zero instance must be created to reconnect.
|
|
17
21
|
*/
|
|
18
22
|
export type ConnectionState = {
|
|
23
|
+
name: 'initializing';
|
|
24
|
+
} | {
|
|
19
25
|
name: 'disconnected';
|
|
20
26
|
reason: string;
|
|
21
27
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAC,YAAY,EAAC,MAAM,qCAAqC,CAAC;AAGjE,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AAGjC
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAC,YAAY,EAAC,MAAM,qCAAqC,CAAC;AAGjE,OAAO,KAAK,EACV,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AAGjC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,cAAc,CAAC;CACtB,GACD;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;CACnB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EACF;QACE,IAAI,EAAE,QAAQ,CAAC;QACf,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,OAAO,CAAC;QACd,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GACD;QACE,IAAI,EAAE,YAAY,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACP,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAEpB;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACrD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;IAExC;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,IAAI,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,qBAAa,cAAe,YAAW,UAAU;;gBAO7C,iBAAiB,EAAE,iBAAiB,EACpC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAQjC,IAAI,KAAK,IAAI,MAAM,CAAC,eAAe,CAAC,CAEnC;IAEK,OAAO,CAAC,IAAI,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAwCpD;AAED,qBAAa,gBACX,SAAQ,YAAY,CAAC,eAAe,CACpC,YAAW,MAAM,CAAC,eAAe,CAAC;;gBAItB,iBAAiB,EAAE,iBAAiB;IAchD,IAAI,OAAO,IAAI,eAAe,CAE7B;CA8DF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { unreachable } from "../../../shared/src/asserts.js";
|
|
2
2
|
import "../../../zero-protocol/src/error-kind-enum.js";
|
|
3
|
-
import { Closed, Connected, Connecting, Disconnected, Error, NeedsAuth } from "./connection-status-enum.js";
|
|
3
|
+
import { Closed, Connected, Connecting, Disconnected, Error, Initializing, NeedsAuth } from "./connection-status-enum.js";
|
|
4
4
|
import { Subscribable } from "../../../shared/src/subscribable.js";
|
|
5
5
|
import "./client-error-kind-enum.js";
|
|
6
6
|
//#region ../zero-client/src/client/connection.ts
|
|
@@ -53,6 +53,7 @@ var ConnectionSource = class extends Subscribable {
|
|
|
53
53
|
}
|
|
54
54
|
#mapConnectionManagerState(state) {
|
|
55
55
|
switch (state.name) {
|
|
56
|
+
case Initializing: return { name: "initializing" };
|
|
56
57
|
case Closed: return {
|
|
57
58
|
name: "closed",
|
|
58
59
|
reason: state.reason.message
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["#connectionManager","#lc","#source","#setAuth","#state","#mapConnectionManagerState"],"sources":["../../../../../zero-client/src/client/connection.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../shared/src/asserts.ts';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport type {\n ConnectionManager,\n ConnectionManagerState,\n} from './connection-manager.ts';\nimport {ConnectionStatus} from './connection-status.ts';\n\n/**\n * The current connection state of the Zero instance. One of the following states:\n *\n * - `connecting`: The client is actively trying to connect every 5 seconds.\n * - `disconnected`: The client is now in an \"offline\" state. It will continue\n * to try to connect every 5 seconds.\n * - `connected`: The client has opened a successful connection to the server.\n * - `needs-auth`: Authentication is invalid or expired. No connection retries will be made\n * until the host application calls `connect()`.\n * - `error`: A fatal error occurred. No connection retries will be made until the host\n * application calls `connect()` again.\n * - `closed`: The client was shut down (for example via `zero.close()`). This is\n * a terminal state, and a new Zero instance must be created to reconnect.\n */\nexport type ConnectionState =\n | {\n name: 'disconnected';\n reason: string;\n }\n | {\n name: 'connecting';\n reason?: string;\n }\n | {\n name: 'connected';\n }\n | {\n name: 'needs-auth';\n reason:\n | {\n type: 'mutate';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'query';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'zero-cache';\n reason: string;\n };\n }\n | {\n name: 'error';\n reason: string;\n }\n | {\n name: 'closed';\n reason: string;\n };\n\nexport interface Source<T> {\n /**\n * The current state value.\n */\n readonly current: T;\n\n /**\n * Subscribe to state changes.\n *\n * @param listener - Called when the state changes with the new state value.\n * @returns A function to unsubscribe from state changes.\n */\n subscribe(listener: (state: T) => void): () => void;\n}\n\n/**\n * Connection API for managing Zero's connection lifecycle.\n */\nexport interface Connection {\n /**\n * The current connection state as a subscribable value.\n */\n readonly state: Source<ConnectionState>;\n\n /**\n * Updates the auth token and, when Zero is paused in `needs-auth` or `error`,\n * resumes connecting.\n *\n * Calling `connect()` without `auth` preserves the current auth token.\n * If Zero is already `connected`, it sends an auth update to the server\n * _without_ reconnecting. In other states, the new token is used the next time\n * Zero connects.\n *\n * This method does not reconnect from `disconnected` or `closed`. To switch\n * to logged-out, create a new Zero instance with `auth` omitted.\n *\n * @param opts - Optional connection options.\n * @param opts.auth - Optional new auth token to store and use for auth refreshes or\n * the next connection.\n * @returns A promise that resolves immediately unless Zero is paused in\n * `needs-auth` or `error`, in which case it resolves after the next\n * connection state change.\n */\n connect(opts?: {auth: string}): Promise<void>;\n}\n\nexport class ConnectionImpl implements Connection {\n readonly #connectionManager: ConnectionManager;\n readonly #lc: LogContext;\n readonly #source: ConnectionSource;\n readonly #setAuth: (auth: string) => void;\n\n constructor(\n connectionManager: ConnectionManager,\n lc: LogContext,\n setAuth: (auth: string) => void,\n ) {\n this.#connectionManager = connectionManager;\n this.#lc = lc;\n this.#source = new ConnectionSource(connectionManager);\n this.#setAuth = setAuth;\n }\n\n get state(): Source<ConnectionState> {\n return this.#source;\n }\n\n async connect(opts?: {auth: string}): Promise<void> {\n const lc = this.#lc.withContext('connect');\n\n if (opts && 'auth' in opts) {\n lc.debug?.('Updating auth credential from connect()');\n this.#setAuth(opts.auth);\n }\n\n // if the connection is disconnected due to a missing cacheURL, we don't allow a reconnect\n if (\n this.#connectionManager.state.name === ConnectionStatus.Disconnected &&\n this.#connectionManager.state.reason.kind ===\n ClientErrorKind.NoSocketOrigin\n ) {\n lc.error?.(\n 'connect() called but the connection is disconnected due to a missing cacheURL. No reconnect will be attempted.',\n );\n return;\n }\n\n // only allow connect() to be called from a terminal state\n if (!this.#connectionManager.isInTerminalState()) {\n lc.debug?.(\n 'connect() called but not in a terminal state. Current state:',\n this.#connectionManager.state.name,\n );\n return;\n }\n\n lc.info?.(\n `Resuming connection from state: ${this.#connectionManager.state.name}`,\n );\n\n this.#connectionManager.requestConnect();\n if (this.#connectionManager.state.name === ConnectionStatus.Connecting) {\n return;\n }\n\n await this.#connectionManager.waitForStateChange();\n }\n}\n\nexport class ConnectionSource\n extends Subscribable<ConnectionState>\n implements Source<ConnectionState>\n{\n #state: ConnectionState;\n\n constructor(connectionManager: ConnectionManager) {\n super();\n this.#state = this.#mapConnectionManagerState(connectionManager.state);\n\n // Subscribe to ConnectionManager immediately to keep #state in sync.\n // This ensures `current` always returns the correct state, even if\n // external code hasn't subscribed yet (fixes race condition where\n // connection completes before React subscribes).\n connectionManager.subscribe(state => {\n this.#state = this.#mapConnectionManagerState(state);\n this.notify(this.#state);\n });\n }\n\n get current(): ConnectionState {\n return this.#state;\n }\n\n #mapConnectionManagerState(state: ConnectionManagerState): ConnectionState {\n switch (state.name) {\n case ConnectionStatus.Closed:\n return {\n name: 'closed',\n reason: state.reason.message,\n };\n case ConnectionStatus.Connected:\n return {\n name: 'connected',\n };\n case ConnectionStatus.Connecting:\n return {\n name: 'connecting',\n ...(state.reason?.message ? {reason: state.reason.message} : {}),\n };\n case ConnectionStatus.Disconnected:\n return {\n name: 'disconnected',\n reason: state.reason.message,\n };\n case ConnectionStatus.Error:\n return {\n name: 'error',\n reason: state.reason.message,\n };\n case ConnectionStatus.NeedsAuth:\n return {\n name: 'needs-auth',\n reason:\n state.reason.errorBody.kind === ErrorKind.PushFailed\n ? {\n type: 'mutate',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : state.reason.errorBody.kind === ErrorKind.TransformFailed\n ? {\n type: 'query',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : {\n type: 'zero-cache',\n reason: state.reason.message,\n },\n };\n\n default:\n unreachable(state);\n }\n }\n}\n"],"mappings":";;;;;;AA8GA,IAAa,iBAAb,MAAkD;CAChD;CACA;CACA;CACA;CAEA,YACE,mBACA,IACA,SACA;EACA,KAAKA,qBAAqB;EAC1B,KAAKC,MAAM;EACX,KAAKC,UAAU,IAAI,iBAAiB,iBAAiB;EACrD,KAAKC,WAAW;CAClB;CAEA,IAAI,QAAiC;EACnC,OAAO,KAAKD;CACd;CAEA,MAAM,QAAQ,MAAsC;EAClD,MAAM,KAAK,KAAKD,IAAI,YAAY,SAAS;EAEzC,IAAI,QAAQ,UAAU,MAAM;GAC1B,GAAG,QAAQ,yCAAyC;GACpD,KAAKE,SAAS,KAAK,IAAI;EACzB;EAGA,IACE,KAAKH,mBAAmB,MAAM,SAAS,kBACvC,KAAKA,mBAAmB,MAAM,OAAO,SACnC,kBACF;GACA,GAAG,QACD,gHACF;GACA;EACF;EAGA,IAAI,CAAC,KAAKA,mBAAmB,kBAAkB,GAAG;GAChD,GAAG,QACD,gEACA,KAAKA,mBAAmB,MAAM,IAChC;GACA;EACF;EAEA,GAAG,OACD,mCAAmC,KAAKA,mBAAmB,MAAM,MACnE;EAEA,KAAKA,mBAAmB,eAAe;EACvC,IAAI,KAAKA,mBAAmB,MAAM,SAAS,cACzC;EAGF,MAAM,KAAKA,mBAAmB,mBAAmB;CACnD;AACF;AAEA,IAAa,mBAAb,cACU,aAEV;CACE;CAEA,YAAY,mBAAsC;EAChD,MAAM;EACN,KAAKI,SAAS,KAAKC,2BAA2B,kBAAkB,KAAK;EAMrE,kBAAkB,WAAU,UAAS;GACnC,KAAKD,SAAS,KAAKC,2BAA2B,KAAK;GACnD,KAAK,OAAO,KAAKD,MAAM;EACzB,CAAC;CACH;CAEA,IAAI,UAA2B;EAC7B,OAAO,KAAKA;CACd;CAEA,2BAA2B,OAAgD;EACzE,QAAQ,MAAM,MAAd;GACE,KAAK,QACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,WACH,OAAO,EACL,MAAM,YACR;GACF,KAAK,YACH,OAAO;IACL,MAAM;IACN,GAAI,MAAM,QAAQ,UAAU,EAAC,QAAQ,MAAM,OAAO,QAAO,IAAI,CAAC;GAChE;GACF,KAAK,cACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,OACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,WACH,OAAO;IACL,MAAM;IACN,QACE,MAAM,OAAO,UAAU,SAAS,eAC5B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,YAAW,IACzC,CAAC;IACP,IACA,MAAM,OAAO,UAAU,SAAS,oBAC9B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,YAAW,IACzC,CAAC;IACP,IACA;KACE,MAAM;KACN,QAAQ,MAAM,OAAO;IACvB;GACV;GAEF,SACE,YAAY,KAAK;EACrB;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"connection.js","names":["#connectionManager","#lc","#source","#setAuth","#state","#mapConnectionManagerState"],"sources":["../../../../../zero-client/src/client/connection.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {unreachable} from '../../../shared/src/asserts.ts';\nimport {Subscribable} from '../../../shared/src/subscribable.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ClientErrorKind} from './client-error-kind.ts';\nimport type {\n ConnectionManager,\n ConnectionManagerState,\n} from './connection-manager.ts';\nimport {ConnectionStatus} from './connection-status.ts';\n\n/**\n * The current connection state of the Zero instance. One of the following states:\n *\n * - `initializing`: The client is loading its local store. Nothing has been\n * sent to the server yet, so time spent here depends on the device and the\n * amount of local data, not on the network. Entered once, when the Zero\n * instance is created, and followed by `connecting`.\n * - `connecting`: The client is actively trying to connect every 5 seconds.\n * - `disconnected`: The client is now in an \"offline\" state. It will continue\n * to try to connect every 5 seconds.\n * - `connected`: The client has opened a successful connection to the server.\n * - `needs-auth`: Authentication is invalid or expired. No connection retries will be made\n * until the host application calls `connect()`.\n * - `error`: A fatal error occurred. No connection retries will be made until the host\n * application calls `connect()` again.\n * - `closed`: The client was shut down (for example via `zero.close()`). This is\n * a terminal state, and a new Zero instance must be created to reconnect.\n */\nexport type ConnectionState =\n | {\n name: 'initializing';\n }\n | {\n name: 'disconnected';\n reason: string;\n }\n | {\n name: 'connecting';\n reason?: string;\n }\n | {\n name: 'connected';\n }\n | {\n name: 'needs-auth';\n reason:\n | {\n type: 'mutate';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'query';\n status: 401 | 403;\n body?: string;\n }\n | {\n type: 'zero-cache';\n reason: string;\n };\n }\n | {\n name: 'error';\n reason: string;\n }\n | {\n name: 'closed';\n reason: string;\n };\n\nexport interface Source<T> {\n /**\n * The current state value.\n */\n readonly current: T;\n\n /**\n * Subscribe to state changes.\n *\n * @param listener - Called when the state changes with the new state value.\n * @returns A function to unsubscribe from state changes.\n */\n subscribe(listener: (state: T) => void): () => void;\n}\n\n/**\n * Connection API for managing Zero's connection lifecycle.\n */\nexport interface Connection {\n /**\n * The current connection state as a subscribable value.\n */\n readonly state: Source<ConnectionState>;\n\n /**\n * Updates the auth token and, when Zero is paused in `needs-auth` or `error`,\n * resumes connecting.\n *\n * Calling `connect()` without `auth` preserves the current auth token.\n * If Zero is already `connected`, it sends an auth update to the server\n * _without_ reconnecting. In other states, the new token is used the next time\n * Zero connects.\n *\n * This method does not reconnect from `disconnected` or `closed`. To switch\n * to logged-out, create a new Zero instance with `auth` omitted.\n *\n * @param opts - Optional connection options.\n * @param opts.auth - Optional new auth token to store and use for auth refreshes or\n * the next connection.\n * @returns A promise that resolves immediately unless Zero is paused in\n * `needs-auth` or `error`, in which case it resolves after the next\n * connection state change.\n */\n connect(opts?: {auth: string}): Promise<void>;\n}\n\nexport class ConnectionImpl implements Connection {\n readonly #connectionManager: ConnectionManager;\n readonly #lc: LogContext;\n readonly #source: ConnectionSource;\n readonly #setAuth: (auth: string) => void;\n\n constructor(\n connectionManager: ConnectionManager,\n lc: LogContext,\n setAuth: (auth: string) => void,\n ) {\n this.#connectionManager = connectionManager;\n this.#lc = lc;\n this.#source = new ConnectionSource(connectionManager);\n this.#setAuth = setAuth;\n }\n\n get state(): Source<ConnectionState> {\n return this.#source;\n }\n\n async connect(opts?: {auth: string}): Promise<void> {\n const lc = this.#lc.withContext('connect');\n\n if (opts && 'auth' in opts) {\n lc.debug?.('Updating auth credential from connect()');\n this.#setAuth(opts.auth);\n }\n\n // if the connection is disconnected due to a missing cacheURL, we don't allow a reconnect\n if (\n this.#connectionManager.state.name === ConnectionStatus.Disconnected &&\n this.#connectionManager.state.reason.kind ===\n ClientErrorKind.NoSocketOrigin\n ) {\n lc.error?.(\n 'connect() called but the connection is disconnected due to a missing cacheURL. No reconnect will be attempted.',\n );\n return;\n }\n\n // only allow connect() to be called from a terminal state\n if (!this.#connectionManager.isInTerminalState()) {\n lc.debug?.(\n 'connect() called but not in a terminal state. Current state:',\n this.#connectionManager.state.name,\n );\n return;\n }\n\n lc.info?.(\n `Resuming connection from state: ${this.#connectionManager.state.name}`,\n );\n\n this.#connectionManager.requestConnect();\n if (this.#connectionManager.state.name === ConnectionStatus.Connecting) {\n return;\n }\n\n await this.#connectionManager.waitForStateChange();\n }\n}\n\nexport class ConnectionSource\n extends Subscribable<ConnectionState>\n implements Source<ConnectionState>\n{\n #state: ConnectionState;\n\n constructor(connectionManager: ConnectionManager) {\n super();\n this.#state = this.#mapConnectionManagerState(connectionManager.state);\n\n // Subscribe to ConnectionManager immediately to keep #state in sync.\n // This ensures `current` always returns the correct state, even if\n // external code hasn't subscribed yet (fixes race condition where\n // connection completes before React subscribes).\n connectionManager.subscribe(state => {\n this.#state = this.#mapConnectionManagerState(state);\n this.notify(this.#state);\n });\n }\n\n get current(): ConnectionState {\n return this.#state;\n }\n\n #mapConnectionManagerState(state: ConnectionManagerState): ConnectionState {\n switch (state.name) {\n case ConnectionStatus.Initializing:\n return {\n name: 'initializing',\n };\n case ConnectionStatus.Closed:\n return {\n name: 'closed',\n reason: state.reason.message,\n };\n case ConnectionStatus.Connected:\n return {\n name: 'connected',\n };\n case ConnectionStatus.Connecting:\n return {\n name: 'connecting',\n ...(state.reason?.message ? {reason: state.reason.message} : {}),\n };\n case ConnectionStatus.Disconnected:\n return {\n name: 'disconnected',\n reason: state.reason.message,\n };\n case ConnectionStatus.Error:\n return {\n name: 'error',\n reason: state.reason.message,\n };\n case ConnectionStatus.NeedsAuth:\n return {\n name: 'needs-auth',\n reason:\n state.reason.errorBody.kind === ErrorKind.PushFailed\n ? {\n type: 'mutate',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : state.reason.errorBody.kind === ErrorKind.TransformFailed\n ? {\n type: 'query',\n status: state.reason.errorBody.status,\n ...(state.reason.errorBody.bodyPreview\n ? {body: state.reason.errorBody.bodyPreview}\n : {}),\n }\n : {\n type: 'zero-cache',\n reason: state.reason.message,\n },\n };\n\n default:\n unreachable(state);\n }\n }\n}\n"],"mappings":";;;;;;AAqHA,IAAa,iBAAb,MAAkD;CAChD;CACA;CACA;CACA;CAEA,YACE,mBACA,IACA,SACA;EACA,KAAKA,qBAAqB;EAC1B,KAAKC,MAAM;EACX,KAAKC,UAAU,IAAI,iBAAiB,iBAAiB;EACrD,KAAKC,WAAW;CAClB;CAEA,IAAI,QAAiC;EACnC,OAAO,KAAKD;CACd;CAEA,MAAM,QAAQ,MAAsC;EAClD,MAAM,KAAK,KAAKD,IAAI,YAAY,SAAS;EAEzC,IAAI,QAAQ,UAAU,MAAM;GAC1B,GAAG,QAAQ,yCAAyC;GACpD,KAAKE,SAAS,KAAK,IAAI;EACzB;EAGA,IACE,KAAKH,mBAAmB,MAAM,SAAS,kBACvC,KAAKA,mBAAmB,MAAM,OAAO,SACnC,kBACF;GACA,GAAG,QACD,gHACF;GACA;EACF;EAGA,IAAI,CAAC,KAAKA,mBAAmB,kBAAkB,GAAG;GAChD,GAAG,QACD,gEACA,KAAKA,mBAAmB,MAAM,IAChC;GACA;EACF;EAEA,GAAG,OACD,mCAAmC,KAAKA,mBAAmB,MAAM,MACnE;EAEA,KAAKA,mBAAmB,eAAe;EACvC,IAAI,KAAKA,mBAAmB,MAAM,SAAS,cACzC;EAGF,MAAM,KAAKA,mBAAmB,mBAAmB;CACnD;AACF;AAEA,IAAa,mBAAb,cACU,aAEV;CACE;CAEA,YAAY,mBAAsC;EAChD,MAAM;EACN,KAAKI,SAAS,KAAKC,2BAA2B,kBAAkB,KAAK;EAMrE,kBAAkB,WAAU,UAAS;GACnC,KAAKD,SAAS,KAAKC,2BAA2B,KAAK;GACnD,KAAK,OAAO,KAAKD,MAAM;EACzB,CAAC;CACH;CAEA,IAAI,UAA2B;EAC7B,OAAO,KAAKA;CACd;CAEA,2BAA2B,OAAgD;EACzE,QAAQ,MAAM,MAAd;GACE,KAAK,cACH,OAAO,EACL,MAAM,eACR;GACF,KAAK,QACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,WACH,OAAO,EACL,MAAM,YACR;GACF,KAAK,YACH,OAAO;IACL,MAAM;IACN,GAAI,MAAM,QAAQ,UAAU,EAAC,QAAQ,MAAM,OAAO,QAAO,IAAI,CAAC;GAChE;GACF,KAAK,cACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,OACH,OAAO;IACL,MAAM;IACN,QAAQ,MAAM,OAAO;GACvB;GACF,KAAK,WACH,OAAO;IACL,MAAM;IACN,QACE,MAAM,OAAO,UAAU,SAAS,eAC5B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,YAAW,IACzC,CAAC;IACP,IACA,MAAM,OAAO,UAAU,SAAS,oBAC9B;KACE,MAAM;KACN,QAAQ,MAAM,OAAO,UAAU;KAC/B,GAAI,MAAM,OAAO,UAAU,cACvB,EAAC,MAAM,MAAM,OAAO,UAAU,YAAW,IACzC,CAAC;IACP,IACA;KACE,MAAM;KACN,QAAQ,MAAM,OAAO;IACvB;GACV;GAEF,SACE,YAAY,KAAK;EACrB;CACF;AACF"}
|
|
@@ -42,18 +42,15 @@ export declare class ZeroContext extends QueryDelegateBase {
|
|
|
42
42
|
get pipelinesReady(): boolean;
|
|
43
43
|
onPipelinesReady(cb: () => void): () => void;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* are empty and `unknown
|
|
45
|
+
* Build and hydrate every pipeline deferred since construction, in
|
|
46
|
+
* materialization order, as a single view-update batch. Views materialized
|
|
47
|
+
* before this is called are empty and `unknown` in the meantime, exactly as
|
|
48
|
+
* they would be over empty sources. A pipeline that fails to build is
|
|
49
|
+
* logged and its view reports an error; the rest are still built.
|
|
50
|
+
*
|
|
51
|
+
* Returns `this` so it can be chained onto the constructor call.
|
|
48
52
|
*/
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Build and hydrate every pipeline deferred since {@link deferPipelines},
|
|
52
|
-
* in materialization order, as a single view-update batch. A pipeline that
|
|
53
|
-
* fails to build is logged and its view reports an error; the rest are
|
|
54
|
-
* still built.
|
|
55
|
-
*/
|
|
56
|
-
markPipelinesReady(): void;
|
|
53
|
+
markPipelinesReady(): this;
|
|
57
54
|
mapAst(ast: AST): AST;
|
|
58
55
|
decorateSourceInput(input: SourceInput, queryID: string): Input;
|
|
59
56
|
onTransactionCommit(cb: CommitListener): () => void;
|