@rocicorp/zero 1.6.2 → 1.7.0-canary.2
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/README.md +28 -3
- package/out/_virtual/{_@oxc-project_runtime@0.122.0 → _@oxc-project_runtime@0.130.0}/helpers/usingCtx.js +1 -1
- package/out/analyze-query/src/analyze-cli.js +3 -3
- package/out/analyze-query/src/analyze-cli.js.map +1 -1
- package/out/analyze-query/src/bin-analyze.js +7 -2
- package/out/analyze-query/src/bin-analyze.js.map +1 -1
- package/out/analyze-query/src/bin-transform.js.map +1 -1
- package/out/ast-to-zql/src/ast-to-zql.js.map +1 -1
- package/out/ast-to-zql/src/bin.js.map +1 -1
- package/out/ast-to-zql/src/format.js.map +1 -1
- package/out/datadog/src/datadog-log-sink.js.map +1 -1
- package/out/otel/src/enabled.js.map +1 -1
- package/out/otel/src/log-options.js.map +1 -1
- package/out/otel/src/maybe-time.js.map +1 -1
- package/out/otel/src/span.js.map +1 -1
- package/out/replicache/src/async-iterable-to-array.js.map +1 -1
- package/out/replicache/src/bg-interval.js.map +1 -1
- package/out/replicache/src/btree/diff.js.map +1 -1
- package/out/replicache/src/btree/node.js.map +1 -1
- package/out/replicache/src/btree/read.js.map +1 -1
- package/out/replicache/src/btree/splice.js.map +1 -1
- package/out/replicache/src/btree/write.js +6 -3
- package/out/replicache/src/btree/write.js.map +1 -1
- package/out/replicache/src/call-default-fetch.js.map +1 -1
- package/out/replicache/src/connection-loop-delegates.js.map +1 -1
- package/out/replicache/src/connection-loop.js +13 -13
- package/out/replicache/src/connection-loop.js.map +1 -1
- package/out/replicache/src/cookies.js.map +1 -1
- package/out/replicache/src/dag/chunk.js.map +1 -1
- package/out/replicache/src/dag/gc.js.map +1 -1
- package/out/replicache/src/dag/key.js.map +1 -1
- package/out/replicache/src/dag/lazy-store.js.map +1 -1
- package/out/replicache/src/dag/store-impl.js.map +1 -1
- package/out/replicache/src/dag/store.js.map +1 -1
- package/out/replicache/src/dag/visitor.js.map +1 -1
- package/out/replicache/src/db/commit.js.map +1 -1
- package/out/replicache/src/db/index.js.map +1 -1
- package/out/replicache/src/db/read.js.map +1 -1
- package/out/replicache/src/db/rebase.js.map +1 -1
- package/out/replicache/src/db/write.js.map +1 -1
- package/out/replicache/src/deleted-clients.js.map +1 -1
- package/out/replicache/src/error-responses.js.map +1 -1
- package/out/replicache/src/frozen-json.js.map +1 -1
- package/out/replicache/src/get-default-puller.js.map +1 -1
- package/out/replicache/src/get-default-pusher.js.map +1 -1
- package/out/replicache/src/get-kv-store-provider.js.map +1 -1
- package/out/replicache/src/hash.js.map +1 -1
- package/out/replicache/src/http-request-info.js.map +1 -1
- package/out/replicache/src/index-defs.js.map +1 -1
- package/out/replicache/src/kv/expo-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/idb-store-with-mem-fallback.js.map +1 -1
- package/out/replicache/src/kv/idb-store.js.map +1 -1
- package/out/replicache/src/kv/mem-store.js.map +1 -1
- package/out/replicache/src/kv/op-sqlite/store.js.map +1 -1
- package/out/replicache/src/kv/read-impl.js.map +1 -1
- package/out/replicache/src/kv/sqlite-store.d.ts.map +1 -1
- package/out/replicache/src/kv/sqlite-store.js +4 -1
- package/out/replicache/src/kv/sqlite-store.js.map +1 -1
- package/out/replicache/src/kv/throw-if-closed.js.map +1 -1
- package/out/replicache/src/kv/write-impl-base.js.map +1 -1
- package/out/replicache/src/kv/write-impl.js.map +1 -1
- package/out/replicache/src/lazy.js.map +1 -1
- package/out/replicache/src/log-options.js.map +1 -1
- package/out/replicache/src/make-idb-name.js.map +1 -1
- package/out/replicache/src/new-client-channel.js.map +1 -1
- package/out/replicache/src/on-persist-channel.js.map +1 -1
- package/out/replicache/src/patch-operation.js.map +1 -1
- package/out/replicache/src/pending-mutations.js.map +1 -1
- package/out/replicache/src/persist/client-gc.js.map +1 -1
- package/out/replicache/src/persist/client-group-gc.js.map +1 -1
- package/out/replicache/src/persist/client-groups.js +40 -0
- package/out/replicache/src/persist/client-groups.js.map +1 -1
- package/out/replicache/src/persist/clients.js +28 -0
- package/out/replicache/src/persist/clients.js.map +1 -1
- package/out/replicache/src/persist/collect-idb-databases.js.map +1 -1
- package/out/replicache/src/persist/gather-mem-only-visitor.js.map +1 -1
- package/out/replicache/src/persist/gather-not-cached-visitor.js.map +1 -1
- package/out/replicache/src/persist/heartbeat.js.map +1 -1
- package/out/replicache/src/persist/idb-databases-store-db-name.js.map +1 -1
- package/out/replicache/src/persist/idb-databases-store.js.map +1 -1
- package/out/replicache/src/persist/make-client-id.js.map +1 -1
- package/out/replicache/src/persist/persist.js.map +1 -1
- package/out/replicache/src/persist/refresh.js.map +1 -1
- package/out/replicache/src/process-scheduler.js.map +1 -1
- package/out/replicache/src/pusher.js.map +1 -1
- package/out/replicache/src/replicache-impl.js.map +1 -1
- package/out/replicache/src/report-error.js.map +1 -1
- package/out/replicache/src/request-idle.js.map +1 -1
- package/out/replicache/src/scan-iterator.js.map +1 -1
- package/out/replicache/src/scan-options.js.map +1 -1
- package/out/replicache/src/set-interval-with-signal.js.map +1 -1
- package/out/replicache/src/subscriptions.js.map +1 -1
- package/out/replicache/src/sync/diff.js.map +1 -1
- package/out/replicache/src/sync/ids.js.map +1 -1
- package/out/replicache/src/sync/patch.js.map +1 -1
- package/out/replicache/src/sync/pull-error.js.map +1 -1
- package/out/replicache/src/sync/pull.js.map +1 -1
- package/out/replicache/src/sync/push.js.map +1 -1
- package/out/replicache/src/sync/request-id.js.map +1 -1
- package/out/replicache/src/to-error.js.map +1 -1
- package/out/replicache/src/transaction-closed-error.js.map +1 -1
- package/out/replicache/src/transactions.js.map +1 -1
- package/out/replicache/src/with-transactions.js.map +1 -1
- package/out/shared/src/abort-error.js.map +1 -1
- package/out/shared/src/arrays.js.map +1 -1
- package/out/shared/src/asserts.js.map +1 -1
- package/out/shared/src/bigint-json.js.map +1 -1
- package/out/shared/src/binary-search.js.map +1 -1
- package/out/shared/src/broadcast-channel.js.map +1 -1
- package/out/shared/src/browser-env.js.map +1 -1
- package/out/shared/src/btree-set.js.map +1 -1
- package/out/shared/src/cache.js.map +1 -1
- package/out/shared/src/centroid.js.map +1 -1
- package/out/shared/src/custom-key-map.js.map +1 -1
- package/out/shared/src/custom-key-set.js.map +1 -1
- package/out/shared/src/deep-clone.js.map +1 -1
- package/out/shared/src/deep-merge.js.map +1 -1
- package/out/shared/src/document-visible.js.map +1 -1
- package/out/shared/src/dotenv.js.map +1 -1
- package/out/shared/src/enum.d.ts +1 -1
- package/out/shared/src/error.js.map +1 -1
- 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 +9 -1
- package/out/shared/src/iterables.js.map +1 -1
- package/out/shared/src/json-schema.js.map +1 -1
- package/out/shared/src/json.js.map +1 -1
- package/out/shared/src/logging-test-utils.js.map +1 -1
- package/out/shared/src/logging.js.map +1 -1
- package/out/shared/src/map.js.map +1 -1
- package/out/shared/src/must.js.map +1 -1
- package/out/shared/src/object-traversal.js.map +1 -1
- package/out/shared/src/objects.js.map +1 -1
- package/out/shared/src/options.js.map +1 -1
- package/out/shared/src/parse-big-int.js.map +1 -1
- package/out/shared/src/promise-race.js.map +1 -1
- package/out/shared/src/queue.d.ts.map +1 -1
- package/out/shared/src/queue.js +15 -21
- package/out/shared/src/queue.js.map +1 -1
- package/out/shared/src/rand.js.map +1 -1
- package/out/shared/src/random-uint64.js.map +1 -1
- package/out/shared/src/random-values.js.map +1 -1
- package/out/shared/src/record-proxy.js.map +1 -1
- package/out/shared/src/resolved-promises.js.map +1 -1
- package/out/shared/src/ring-buffer.d.ts +32 -0
- package/out/shared/src/ring-buffer.d.ts.map +1 -0
- package/out/shared/src/ring-buffer.js +109 -0
- package/out/shared/src/ring-buffer.js.map +1 -0
- package/out/shared/src/sentinels.js.map +1 -1
- package/out/shared/src/set-utils.js.map +1 -1
- package/out/shared/src/size-of-value.js.map +1 -1
- package/out/shared/src/sleep.js.map +1 -1
- package/out/shared/src/sorted-entries.js.map +1 -1
- package/out/shared/src/string-compare.js.map +1 -1
- package/out/shared/src/subscribable.js.map +1 -1
- package/out/shared/src/tdigest-schema.js.map +1 -1
- package/out/shared/src/tdigest.js.map +1 -1
- package/out/shared/src/valita.js.map +1 -1
- package/out/z2s/src/compiler.js.map +1 -1
- package/out/z2s/src/sql.d.ts.map +1 -1
- package/out/z2s/src/sql.js +3 -2
- package/out/z2s/src/sql.js.map +1 -1
- package/out/zero/package.js +37 -28
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/build-schema.js.map +1 -1
- package/out/zero/src/zero-cache-dev.js.map +1 -1
- package/out/zero/src/zero-out.js.map +1 -1
- package/out/zero/src/zero.js +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/jwt.js.map +1 -1
- package/out/zero-cache/src/auth/load-permissions.js.map +1 -1
- package/out/zero-cache/src/auth/read-authorizer.js.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/network.js.map +1 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/server-context.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +5 -0
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/custom/fetch.js.map +1 -1
- package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
- package/out/zero-cache/src/db/create.js.map +1 -1
- package/out/zero-cache/src/db/delete-lite-db.js.map +1 -1
- package/out/zero-cache/src/db/lite-tables.js.map +1 -1
- package/out/zero-cache/src/db/migration-lite.js +19 -0
- package/out/zero-cache/src/db/migration-lite.js.map +1 -1
- package/out/zero-cache/src/db/migration.js +19 -0
- package/out/zero-cache/src/db/migration.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy-binary.js.map +1 -1
- package/out/zero-cache/src/db/pg-copy.js.map +1 -1
- package/out/zero-cache/src/db/pg-to-lite.js.map +1 -1
- package/out/zero-cache/src/db/pg-type-parser.js.map +1 -1
- package/out/zero-cache/src/db/run-transaction.js.map +1 -1
- package/out/zero-cache/src/db/specs.js.map +1 -1
- package/out/zero-cache/src/db/statements.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/db/warmup.js.map +1 -1
- package/out/zero-cache/src/observability/events.js.map +1 -1
- package/out/zero-cache/src/observability/metrics.js.map +1 -1
- package/out/zero-cache/src/scripts/decommission.js.map +1 -1
- package/out/zero-cache/src/scripts/deploy-permissions.js.map +1 -1
- package/out/zero-cache/src/scripts/permissions.d.ts.map +1 -1
- package/out/zero-cache/src/scripts/permissions.js +1 -2
- package/out/zero-cache/src/scripts/permissions.js.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +8 -7
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +2 -2
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/inspector-delegate.js.map +1 -1
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/otel-diag-logger.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/server/priority-op.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/main.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/runner/runtime.js.map +1 -1
- package/out/zero-cache/src/server/runner/zero-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/shadow-syncer.js.map +1 -1
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js.map +1 -1
- package/out/zero-cache/src/server/worker-urls.js.map +1 -1
- package/out/zero-cache/src/services/analyze.d.ts.map +1 -1
- package/out/zero-cache/src/services/analyze.js +5 -2
- package/out/zero-cache/src/services/analyze.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/change-stream-multiplexer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-metadata.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/decommission.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/binary-reader.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/logical-replication/stream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/lsn.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/replication-slots.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/validation.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/control.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js +2 -0
- package/out/zero-cache/src/services/change-source/protocol/current/data.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/downstream.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/json.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/status.js.map +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/upstream.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts +46 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +109 -12
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/broadcast.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/forwarder.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/replica-monitor.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/init.js +21 -25
- package/out/zero-cache/src/services/change-streamer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.js +15 -0
- package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +12 -2
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscriber.js.map +1 -1
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/http-service.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/limiter/sliding-window-limiter.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +23 -0
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +89 -1
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/error.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
- package/out/zero-cache/src/services/replicator/change-processor.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/notifier.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts +1 -0
- package/out/zero-cache/src/services/replicator/reporter/recorder.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/recorder.js +8 -0
- package/out/zero-cache/src/services/replicator/reporter/recorder.js.map +1 -1
- package/out/zero-cache/src/services/replicator/reporter/report-schema.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/change-log.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/column-metadata.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.js.map +1 -1
- package/out/zero-cache/src/services/replicator/schema/table-metadata.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker-client.js.map +1 -1
- package/out/zero-cache/src/services/replicator/write-worker.js.map +1 -1
- package/out/zero-cache/src/services/run-ast.d.ts.map +1 -1
- package/out/zero-cache/src/services/run-ast.js +1 -0
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/runner.js.map +1 -1
- package/out/zero-cache/src/services/running-state.js.map +1 -1
- package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/active-users-gauge.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-schema.js +10 -2
- package/out/zero-cache/src/services/view-syncer/client-schema.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-purger.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +2 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/drain-coordinator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +2 -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 +81 -7
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-set-signature.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/init.js +97 -113
- package/out/zero-cache/src/services/view-syncer/schema/init.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js +103 -1
- package/out/zero-cache/src/services/view-syncer/schema/types.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/tracer.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/ttl-clock.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +13 -6
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/configuration-error.js.map +1 -1
- package/out/zero-cache/src/types/error-with-level.js.map +1 -1
- package/out/zero-cache/src/types/http.js.map +1 -1
- package/out/zero-cache/src/types/lexi-version.js.map +1 -1
- package/out/zero-cache/src/types/lite.js.map +1 -1
- package/out/zero-cache/src/types/names.js.map +1 -1
- package/out/zero-cache/src/types/pg-data-type.d.ts +53 -2
- package/out/zero-cache/src/types/pg-data-type.d.ts.map +1 -1
- package/out/zero-cache/src/types/pg-data-type.js +28 -3
- package/out/zero-cache/src/types/pg-data-type.js.map +1 -1
- package/out/zero-cache/src/types/pg.js.map +1 -1
- package/out/zero-cache/src/types/processes.js.map +1 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- package/out/zero-cache/src/types/row-key.js.map +1 -1
- package/out/zero-cache/src/types/shards.js.map +1 -1
- package/out/zero-cache/src/types/sql.js.map +1 -1
- package/out/zero-cache/src/types/state-version.js.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/strings.js.map +1 -1
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/types/timeout.js.map +1 -1
- package/out/zero-cache/src/types/url-params.js.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
- package/out/zero-cache/src/types/ws.js.map +1 -1
- package/out/zero-cache/src/workers/connect-params.js.map +1 -1
- package/out/zero-cache/src/workers/connection.js.map +1 -1
- package/out/zero-cache/src/workers/mutator.js.map +1 -1
- package/out/zero-cache/src/workers/replicator.js.map +1 -1
- package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/active-clients-manager.js.map +1 -1
- package/out/zero-client/src/client/connection-manager.js +2 -1
- package/out/zero-client/src/client/connection-manager.js.map +1 -1
- package/out/zero-client/src/client/connection.js.map +1 -1
- package/out/zero-client/src/client/context.js.map +1 -1
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/crud.js.map +1 -1
- package/out/zero-client/src/client/custom.js +2 -1
- package/out/zero-client/src/client/custom.js.map +1 -1
- package/out/zero-client/src/client/delete-clients-manager.js.map +1 -1
- package/out/zero-client/src/client/enable-analytics.js.map +1 -1
- package/out/zero-client/src/client/error.js.map +1 -1
- package/out/zero-client/src/client/http-string.js.map +1 -1
- package/out/zero-client/src/client/inspector/client-group.js.map +1 -1
- package/out/zero-client/src/client/inspector/client.js.map +1 -1
- package/out/zero-client/src/client/inspector/html-dialog-prompt.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/query.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/keys.js.map +1 -1
- package/out/zero-client/src/client/log-options.js.map +1 -1
- package/out/zero-client/src/client/make-mutate-property.js.map +1 -1
- package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
- package/out/zero-client/src/client/metrics.js.map +1 -1
- package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
- package/out/zero-client/src/client/mutator-proxy.js.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/query-manager.js +1 -1
- package/out/zero-client/src/client/query-manager.js.map +1 -1
- package/out/zero-client/src/client/reload-error-handler.js.map +1 -1
- package/out/zero-client/src/client/server-option.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero-poke-handler.js.map +1 -1
- package/out/zero-client/src/client/zero-rep.js.map +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +58 -32
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-client/src/mod.d.ts +2 -2
- package/out/zero-client/src/mod.d.ts.map +1 -1
- package/out/zero-client/src/mod.js +1 -1
- package/out/zero-client/src/util/nanoid.js.map +1 -1
- package/out/{zero-schema → zero-permissions}/src/permissions.d.ts +1 -1
- package/out/zero-permissions/src/permissions.d.ts.map +1 -0
- package/out/{zero-schema → zero-permissions}/src/permissions.js +2 -2
- package/out/zero-permissions/src/permissions.js.map +1 -0
- package/out/zero-protocol/src/analyze-query-result.js +3 -0
- package/out/zero-protocol/src/analyze-query-result.js.map +1 -1
- package/out/zero-protocol/src/application-error.js.map +1 -1
- package/out/zero-protocol/src/ast.js.map +1 -1
- package/out/zero-protocol/src/change-desired-queries.js +1 -0
- package/out/zero-protocol/src/change-desired-queries.js.map +1 -1
- package/out/zero-protocol/src/client-schema.js.map +1 -1
- package/out/zero-protocol/src/close-connection.js.map +1 -1
- package/out/zero-protocol/src/connect.js +7 -0
- package/out/zero-protocol/src/connect.js.map +1 -1
- package/out/zero-protocol/src/custom-queries.js.map +1 -1
- package/out/zero-protocol/src/data.js.map +1 -1
- package/out/zero-protocol/src/delete-clients.js.map +1 -1
- package/out/zero-protocol/src/down.js.map +1 -1
- package/out/zero-protocol/src/error.js +7 -0
- package/out/zero-protocol/src/error.js.map +1 -1
- package/out/zero-protocol/src/inspect-down.js.map +1 -1
- package/out/zero-protocol/src/inspect-up.js +1 -0
- package/out/zero-protocol/src/inspect-up.js.map +1 -1
- package/out/zero-protocol/src/mutate-server.js.map +1 -1
- package/out/zero-protocol/src/mutation-id.js.map +1 -1
- package/out/zero-protocol/src/mutation.js.map +1 -1
- package/out/zero-protocol/src/mutations-patch.js.map +1 -1
- package/out/zero-protocol/src/ping.js.map +1 -1
- package/out/zero-protocol/src/poke.js +4 -0
- package/out/zero-protocol/src/poke.js.map +1 -1
- package/out/zero-protocol/src/pong.js.map +1 -1
- package/out/zero-protocol/src/primary-key.js.map +1 -1
- package/out/zero-protocol/src/protocol-version.js.map +1 -1
- package/out/zero-protocol/src/pull.js.map +1 -1
- package/out/zero-protocol/src/push.js +16 -0
- package/out/zero-protocol/src/push.js.map +1 -1
- package/out/zero-protocol/src/queries-patch.js.map +1 -1
- package/out/zero-protocol/src/query-hash.js.map +1 -1
- package/out/zero-protocol/src/query-server.js.map +1 -1
- package/out/zero-protocol/src/row-patch.js.map +1 -1
- package/out/zero-protocol/src/up.js.map +1 -1
- package/out/zero-protocol/src/update-auth.js.map +1 -1
- package/out/zero-protocol/src/version.js.map +1 -1
- package/out/zero-react/src/use-connection-state.js.map +1 -1
- package/out/zero-react/src/use-query.d.ts.map +1 -1
- package/out/zero-react/src/use-query.js +2 -3
- package/out/zero-react/src/use-query.js.map +1 -1
- package/out/zero-react/src/use-zero-online.js.map +1 -1
- package/out/zero-react/src/zero-provider.js.map +1 -1
- package/out/zero-react/src/zero.js +1 -1
- package/out/zero-schema/src/builder/relationship-builder.js.map +1 -1
- package/out/zero-schema/src/builder/schema-builder.js.map +1 -1
- package/out/zero-schema/src/builder/table-builder.js.map +1 -1
- package/out/zero-schema/src/compiled-permissions.js.map +1 -1
- package/out/zero-schema/src/name-mapper.js.map +1 -1
- package/out/zero-schema/src/schema-config.js.map +1 -1
- package/out/zero-server/src/adapters/drizzle.js.map +1 -1
- package/out/zero-server/src/adapters/kysely.js.map +1 -1
- package/out/zero-server/src/adapters/pg.js.map +1 -1
- package/out/zero-server/src/adapters/postgresjs.js.map +1 -1
- package/out/zero-server/src/adapters/prisma.js.map +1 -1
- package/out/zero-server/src/custom.js +2 -1
- package/out/zero-server/src/custom.js.map +1 -1
- package/out/zero-server/src/logging.js.map +1 -1
- package/out/zero-server/src/pg-query-executor.js.map +1 -1
- package/out/zero-server/src/process-mutations.js.map +1 -1
- package/out/zero-server/src/push-processor.js.map +1 -1
- package/out/zero-server/src/queries/process-queries.js +1 -1
- package/out/zero-server/src/queries/process-queries.js.map +1 -1
- package/out/zero-server/src/schema.d.ts.map +1 -1
- package/out/zero-server/src/schema.js +1 -0
- package/out/zero-server/src/schema.js.map +1 -1
- package/out/zero-server/src/zql-database.js.map +1 -1
- package/out/zero-solid/src/solid-view.d.ts +42 -0
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +43 -2
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zero-solid/src/use-connection-state.js.map +1 -1
- package/out/zero-solid/src/use-query.js +1 -1
- package/out/zero-solid/src/use-query.js.map +1 -1
- package/out/zero-solid/src/use-zero-online.js.map +1 -1
- package/out/zero-solid/src/use-zero.js.map +1 -1
- package/out/zero-solid/src/zero.js +1 -1
- package/out/zero-types/src/format.js.map +1 -1
- package/out/zero-types/src/name-mapper.js.map +1 -1
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/builder/debug-delegate.d.ts +5 -0
- package/out/zql/src/builder/debug-delegate.d.ts.map +1 -1
- package/out/zql/src/builder/debug-delegate.js +10 -1
- package/out/zql/src/builder/debug-delegate.js.map +1 -1
- package/out/zql/src/builder/filter.d.ts.map +1 -1
- package/out/zql/src/builder/filter.js +5 -4
- package/out/zql/src/builder/filter.js.map +1 -1
- package/out/zql/src/builder/like.js +1 -1
- package/out/zql/src/builder/like.js.map +1 -1
- package/out/zql/src/error.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +5 -3
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/cap.js.map +1 -1
- package/out/zql/src/ivm/change.js.map +1 -1
- package/out/zql/src/ivm/constraint.js +1 -1
- package/out/zql/src/ivm/constraint.js.map +1 -1
- package/out/zql/src/ivm/data.js.map +1 -1
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter-push.js.map +1 -1
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts +4 -8
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +59 -63
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join-utils.js.map +1 -1
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/memory-storage.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +1 -1
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/schema.d.ts +0 -8
- package/out/zql/src/ivm/schema.d.ts.map +1 -1
- package/out/zql/src/ivm/skip-yields.js.map +1 -1
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/source.js.map +1 -1
- package/out/zql/src/ivm/stream.js.map +1 -1
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.d.ts +57 -3
- package/out/zql/src/ivm/view-apply-change.d.ts.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +296 -123
- package/out/zql/src/ivm/view-apply-change.js.map +1 -1
- package/out/zql/src/mutate/crud.js.map +1 -1
- package/out/zql/src/mutate/custom.js.map +1 -1
- package/out/zql/src/mutate/mutator-registry.js.map +1 -1
- package/out/zql/src/mutate/mutator.js.map +1 -1
- package/out/zql/src/planner/planner-builder.js.map +1 -1
- package/out/zql/src/planner/planner-connection.js.map +1 -1
- package/out/zql/src/planner/planner-constraint.js.map +1 -1
- package/out/zql/src/planner/planner-debug.js.map +1 -1
- package/out/zql/src/planner/planner-fan-in.js.map +1 -1
- package/out/zql/src/planner/planner-fan-out.js.map +1 -1
- package/out/zql/src/planner/planner-graph.js.map +1 -1
- package/out/zql/src/planner/planner-join.d.ts.map +1 -1
- package/out/zql/src/planner/planner-join.js +2 -1
- package/out/zql/src/planner/planner-join.js.map +1 -1
- package/out/zql/src/planner/planner-node.js.map +1 -1
- package/out/zql/src/planner/planner-source.js.map +1 -1
- package/out/zql/src/planner/planner-terminus.js.map +1 -1
- package/out/zql/src/query/complete-ordering.js.map +1 -1
- package/out/zql/src/query/create-builder.js.map +1 -1
- package/out/zql/src/query/error.js.map +1 -1
- package/out/zql/src/query/escape-like.js.map +1 -1
- package/out/zql/src/query/expression.js.map +1 -1
- package/out/zql/src/query/measure-push-operator.js.map +1 -1
- package/out/zql/src/query/metrics-delegate.js.map +1 -1
- package/out/zql/src/query/named.js.map +1 -1
- package/out/zql/src/query/query-delegate-base.js +1 -3
- package/out/zql/src/query/query-delegate-base.js.map +1 -1
- package/out/zql/src/query/query-impl.js.map +1 -1
- package/out/zql/src/query/query-internals.js.map +1 -1
- package/out/zql/src/query/query-registry.js.map +1 -1
- package/out/zql/src/query/runnable-query-impl.js.map +1 -1
- package/out/zql/src/query/static-query.js.map +1 -1
- package/out/zql/src/query/ttl.js.map +1 -1
- package/out/zql/src/query/validate-input.js.map +1 -1
- package/out/zqlite/src/database-storage.js.map +1 -1
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +1 -0
- package/out/zqlite/src/db.js.map +1 -1
- package/out/zqlite/src/explain-queries.js.map +1 -1
- package/out/zqlite/src/internal/sql-inline.js.map +1 -1
- package/out/zqlite/src/internal/sql.js.map +1 -1
- package/out/zqlite/src/internal/statement-cache.js.map +1 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js +12 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/query-delegate.js.map +1 -1
- package/out/zqlite/src/resolve-scalar-subqueries.js.map +1 -1
- package/out/zqlite/src/sqlite-cost-model.js.map +1 -1
- package/out/zqlite/src/sqlite-stat-fanout.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +6 -6
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +45 -28
- package/out/zero-client/src/util/socket.d.ts +0 -3
- package/out/zero-client/src/util/socket.d.ts.map +0 -1
- package/out/zero-client/src/util/socket.js +0 -8
- package/out/zero-client/src/util/socket.js.map +0 -1
- package/out/zero-schema/src/permissions.d.ts.map +0 -1
- package/out/zero-schema/src/permissions.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.js","names":["#lc","#ws","#closeStream","#messageHandler","#connected","#handleOpen","#handleClose","#handleError","#conn"],"sources":["../../../../../zero-cache/src/types/streams.ts"],"sourcesContent":["import {\n pipeline,\n Readable,\n Transform,\n Writable,\n type DuplexOptions,\n} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {\n createWebSocketStream,\n type CloseEvent,\n type ErrorEvent,\n type MessageEvent,\n type WebSocket,\n} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {BigIntJSON, type JSONValue} from '../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../shared/src/queue.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Subscription, type Options} from './subscription.ts';\nimport {\n closeWithError,\n expectPingsForLiveness,\n sendPingsForLiveness,\n} from './ws.ts';\n\n// Consistent with Postgres keepalives, and shorter than the\n// commonly used default idle timeout of 1 minute.\nconst PING_INTERVAL_MS = 30_000;\n\nexport type Source<T> = AsyncIterable<T> & {\n /**\n * Immediately terminates all current iterations (i.e. {@link AsyncIterator.next next()})\n * will return `{value: undefined, done: true}`), and prevents any subsequent iterations\n * from yielding any values.\n *\n * @param err Terminate the iteration by throwing the `err` instead.\n */\n cancel: (err?: Error) => void;\n\n /**\n * The presence of a `pipeline` iterable allows the usual \"consumed-on-iterate\" semantics\n * to be overridden.\n *\n * This is suitable for transport layers that serialize messages across processes, such\n * as the {@link streamOut()} method; pipelining allows the transport to send messages\n * as they arrive without waiting for the previous message to be acked, streaming\n * them to the receiving process where they are presumably queued and processed without\n * a per-message ack delay. The receiving end of the transport then responds with acks\n * asynchronously as the receiving end processes the messages.\n */\n pipeline?: AsyncIterable<{value: T; consumed: () => void}> | undefined;\n};\n\nexport type Sink<T> = {\n push(message: T): void;\n};\n\n/**\n * Back-pressure-aware transformation of a WebSocket into\n * upstream and downstream {@link Subscription} objects.\n */\n// TODO: Change {@link streamIn} and {@link streamOut} to use this\n// under the covers so that internal communication is also\n// responsive to backpressure.\nexport function stream<In extends JSONValue, Out extends JSONValue>(\n lc: LogContext,\n ws: WebSocket,\n inSchema: v.Type<In>,\n outOptions: Options<Out> = {},\n inOptions: Options<In> = {},\n streamOptions: DuplexOptions = {},\n): {outstream: Sink<Out>; instream: Source<In>} {\n const endpoint = ws.url ?? 'client';\n function close(err?: unknown) {\n if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {\n if (err) {\n closeWithError(lc, ws, err);\n } else {\n lc.info?.(`closing connection to ${endpoint}`);\n ws.close();\n }\n }\n }\n\n const instream = Subscription.create<In>({\n ...inOptions,\n cleanup: (unconsumed, err) => {\n inOptions.cleanup?.(unconsumed, err);\n close(err);\n },\n });\n const outstream = Subscription.create<Out>({\n ...outOptions,\n cleanup: (unconsumed, err) => {\n outOptions.cleanup?.(unconsumed, err);\n close(err);\n },\n });\n\n const duplex = createWebSocketStream(ws, {\n ...streamOptions,\n decodeStrings: false,\n });\n\n // Outgoing transform.\n function streamOut() {\n // Mainly used for verifying that back-pressure kicks in tests.\n duplex.on('drain', () => lc.debug?.(`drained messages to ${endpoint}`));\n\n pipeline(\n Readable.from(outstream),\n new Transform({\n objectMode: true,\n transform: (msg, _encoding, callback) =>\n callback(null, BigIntJSON.stringify(msg)),\n }),\n duplex,\n err => (err ? outstream.fail(err) : outstream.cancel()),\n );\n }\n\n if (ws.readyState === ws.CONNECTING) {\n ws.on('open', () => {\n lc.info?.(`connected to ${endpoint}`);\n streamOut();\n });\n } else {\n streamOut();\n }\n\n // Incoming transform.\n pipe({\n source: duplex,\n sink: instream,\n parse: chunk => {\n const json = BigIntJSON.parse(chunk.toString());\n return v.parse(json, inSchema, 'passthrough');\n },\n });\n\n sendPingsForLiveness(lc, ws, PING_INTERVAL_MS);\n\n return {outstream, instream};\n}\n\ntype PipeOptions<T> = {\n source: Readable;\n sink: Subscription<T>;\n parse: (buffer: Buffer) => T | null;\n bufferMessages?: number;\n};\n\nexport function pipe<T>({source, sink, parse, bufferMessages}: PipeOptions<T>) {\n bufferMessages ??= 0;\n assert(bufferMessages >= 0, 'bufferMessages must be non-negative');\n const pending: Promise<unknown>[] = [];\n\n pipeline(\n source,\n new Writable({\n decodeStrings: false,\n write: (chunk, _encoding, callback) => {\n let msg: T | null;\n try {\n if ((msg = parse(chunk)) === null) {\n callback();\n return;\n }\n } catch (err) {\n callback(ensureError(err));\n return;\n }\n // Inbound backpressure is exerted by unconsumed messages in the\n // subscription. A buffer can be used to allow messages to queue up in\n // in the Subscription object, which allows the consumer to \"peek\" at\n // whether there are more messages immediately available\n // (via {@link Subscription.queued}.\n const {result} = sink.push(msg);\n pending.push(result);\n void result.then(() => pending.shift());\n\n if (pending.length <= bufferMessages) {\n // immediately allow more messages\n callback();\n } else {\n // wait for the oldest result in the pending queue\n pending[0].then(\n () => callback(),\n err => callback(ensureError(err)),\n );\n }\n },\n destroy: (err, callback) => {\n if (err) {\n sink.fail(ensureError(err));\n }\n // Otherwise, final will handle the cancel.\n callback();\n },\n final: callback => {\n sink.cancel();\n callback();\n },\n }),\n err => (err ? sink.fail(err) : sink.cancel()),\n );\n}\n\nfunction ensureError(err: unknown) {\n return err instanceof Error ? err : new Error(String(err));\n}\n\nconst ackSchema = v.object({ack: v.number()});\n\ntype Ack = v.Infer<typeof ackSchema>;\n\ntype Streamed<T> = {\n /** Application-level message. */\n msg: T;\n\n /** ID used for the Ack message. */\n id: number;\n};\n\nexport function streamOut<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\n): Promise<void> {\n return streamOutInternal(lc, source, sink, BigIntJSON.stringify);\n}\n\n/**\n * Streams out a `Source` for which messages are already stringified JSON.\n */\nexport function streamOutStringified(\n lc: LogContext,\n source: Source<string>,\n sink: WebSocket,\n): Promise<void> {\n return streamOutInternal(lc, source, sink, json => json);\n}\n\nasync function streamOutInternal<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\n stringify: (payload: T) => string,\n): Promise<void> {\n sendPingsForLiveness(lc, sink, PING_INTERVAL_MS);\n\n const closer = WebSocketCloser.forSource(lc, sink, source);\n\n const acks = new Queue<Ack>();\n sink.addEventListener('message', ({data}) => {\n try {\n if (typeof data !== 'string') {\n throw new Error('Expected string message');\n }\n acks.enqueue(v.parse(JSON.parse(data), ackSchema));\n } catch (e) {\n lc.error?.(`error parsing ack`, e);\n closer.close(e);\n }\n });\n\n try {\n let nextID = 0;\n const {pipeline} = source;\n if (pipeline) {\n lc.debug?.(`started pipelined outbound stream`);\n for await (const {value: msg, consumed} of pipeline) {\n const id = ++nextID;\n const data = `{\"id\":${id},\"msg\":${stringify(msg)}}`;\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`pipelining`, data);\n sink.send(data);\n\n void (async () => {\n const {ack} = await acks.dequeue();\n // lc.debug?.(`received ack`, ack);\n if (ack !== id) {\n throw new Error(`Unexpected ack for ${id}: ${ack}`);\n }\n consumed();\n })();\n }\n } else {\n lc.debug?.(`started synchronous outbound stream`);\n for await (const msg of source) {\n const id = ++nextID;\n const data = `{\"id\":${id},\"msg\":${stringify(msg)}}`;\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`sending`, data);\n sink.send(data);\n\n const {ack} = await acks.dequeue();\n if (ack !== id) {\n throw new Error(`Unexpected ack for ${id}: ${ack}`);\n }\n }\n }\n closer.close();\n } catch (e) {\n closer.close(e);\n }\n}\n\nexport async function streamIn<T extends JSONValue>(\n lc: LogContext,\n source: WebSocket,\n schema: v.Type<T>,\n): Promise<Source<T>> {\n expectPingsForLiveness(lc, source, PING_INTERVAL_MS);\n\n const streamedSchema = v.object({\n msg: schema,\n id: v.number(),\n });\n\n const sink: Subscription<T, Streamed<T>> = new Subscription<T, Streamed<T>>(\n {\n consumed: ({id}) => source.send(JSON.stringify({ack: id} satisfies Ack)),\n cleanup: () => closer.close(),\n },\n ({msg}) => msg,\n );\n\n const closer = WebSocketCloser.forSink(lc, source, sink, handleMessage);\n\n function handleMessage(event: MessageEvent) {\n const data = event.data.toString();\n if (!sink.active) {\n lc.warn?.('dropping ws message received after close', data);\n return;\n }\n try {\n const value = BigIntJSON.parse(data);\n const msg = v.parse(value, streamedSchema, 'passthrough');\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`received`, data);\n sink.push(msg);\n } catch (e) {\n closer.close(e);\n }\n }\n\n await closer.connected;\n return sink;\n}\n\nclass WebSocketCloser {\n readonly #lc: LogContext;\n readonly #ws: WebSocket;\n readonly #closeStream: () => void;\n readonly #messageHandler: ((e: MessageEvent) => void | undefined) | null;\n readonly #connected = resolver();\n\n get connected(): Promise<void> {\n return this.#connected.promise;\n }\n\n static forSource<T>(lc: LogContext, ws: WebSocket, stream: Source<T>) {\n // If the websocket is closed, call cancel() to notify the Source of\n // any unconsumed messages.\n return new WebSocketCloser(lc, ws, () => stream.cancel());\n }\n\n static forSink<T>(\n lc: LogContext,\n ws: WebSocket,\n stream: Subscription<T, Streamed<T>>,\n messageHandler: (e: MessageEvent) => void | undefined,\n ) {\n // If the websocket is closed, call end() to allow the downstream Sink\n // to process any pending messages before closing the stream.\n return new WebSocketCloser(lc, ws, () => stream.end(), messageHandler);\n }\n\n private constructor(\n lc: LogContext,\n ws: WebSocket,\n closeStream: () => void,\n messageHandler?: (e: MessageEvent) => void | undefined,\n ) {\n this.#lc = lc;\n this.#ws = ws;\n this.#closeStream = closeStream;\n this.#messageHandler = messageHandler ?? null;\n\n ws.addEventListener('open', this.#handleOpen);\n ws.addEventListener('close', this.#handleClose);\n ws.addEventListener('error', this.#handleError);\n if (this.#messageHandler) {\n ws.addEventListener('message', this.#messageHandler);\n }\n\n switch (ws.readyState) {\n case ws.CONNECTING:\n break; // expected for new connections. resolve or reject in handlers.\n case ws.OPEN:\n this.#connected.resolve();\n break;\n default:\n this.#connected.reject(\n new Error(`websocket already in state ${ws.readyState}`),\n );\n break;\n }\n }\n\n get #conn(): string {\n return 'connection' + (this.#ws.url ? ` to ${this.#ws.url}` : '');\n }\n\n #handleOpen = () => {\n this.#lc.info?.(`${this.#conn} established`);\n this.#connected.resolve();\n };\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n this.#lc.info?.(`${this.#conn} closed`, {\n code,\n reason,\n wasClean,\n });\n this.close();\n this.#connected.reject(`${this.#conn} closed with code ${code}`);\n };\n\n #handleError = ({message, error}: ErrorEvent) => {\n if (this.#ws.readyState === this.#ws.OPEN) {\n this.#lc.error?.(`error in ${this.#conn}`, message, error);\n }\n this.#connected.reject(error);\n };\n\n close(err?: unknown) {\n if (err) {\n this.#lc.error?.(`closing stream with error`, err);\n }\n this.#closeStream();\n if (!this.closed()) {\n this.#ws.close();\n }\n }\n\n closed() {\n return (\n this.#ws.readyState === this.#ws.CLOSED ||\n this.#ws.readyState === this.#ws.CLOSING\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"streams.js","names":["#lc","#ws","#closeStream","#messageHandler","#connected","#handleOpen","#handleClose","#handleError","#conn"],"sources":["../../../../../zero-cache/src/types/streams.ts"],"sourcesContent":["import {\n pipeline,\n Readable,\n Transform,\n Writable,\n type DuplexOptions,\n} from 'node:stream';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {\n createWebSocketStream,\n type CloseEvent,\n type ErrorEvent,\n type MessageEvent,\n type WebSocket,\n} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {BigIntJSON, type JSONValue} from '../../../shared/src/bigint-json.ts';\nimport {Queue} from '../../../shared/src/queue.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Subscription, type Options} from './subscription.ts';\nimport {\n closeWithError,\n expectPingsForLiveness,\n sendPingsForLiveness,\n} from './ws.ts';\n\n// Consistent with Postgres keepalives, and shorter than the\n// commonly used default idle timeout of 1 minute.\nconst PING_INTERVAL_MS = 30_000;\n\nexport type Source<T> = AsyncIterable<T> & {\n /**\n * Immediately terminates all current iterations (i.e. {@link AsyncIterator.next next()})\n * will return `{value: undefined, done: true}`), and prevents any subsequent iterations\n * from yielding any values.\n *\n * @param err Terminate the iteration by throwing the `err` instead.\n */\n cancel: (err?: Error) => void;\n\n /**\n * The presence of a `pipeline` iterable allows the usual \"consumed-on-iterate\" semantics\n * to be overridden.\n *\n * This is suitable for transport layers that serialize messages across processes, such\n * as the {@link streamOut()} method; pipelining allows the transport to send messages\n * as they arrive without waiting for the previous message to be acked, streaming\n * them to the receiving process where they are presumably queued and processed without\n * a per-message ack delay. The receiving end of the transport then responds with acks\n * asynchronously as the receiving end processes the messages.\n */\n pipeline?: AsyncIterable<{value: T; consumed: () => void}> | undefined;\n};\n\nexport type Sink<T> = {\n push(message: T): void;\n};\n\n/**\n * Back-pressure-aware transformation of a WebSocket into\n * upstream and downstream {@link Subscription} objects.\n */\n// TODO: Change {@link streamIn} and {@link streamOut} to use this\n// under the covers so that internal communication is also\n// responsive to backpressure.\nexport function stream<In extends JSONValue, Out extends JSONValue>(\n lc: LogContext,\n ws: WebSocket,\n inSchema: v.Type<In>,\n outOptions: Options<Out> = {},\n inOptions: Options<In> = {},\n streamOptions: DuplexOptions = {},\n): {outstream: Sink<Out>; instream: Source<In>} {\n const endpoint = ws.url ?? 'client';\n function close(err?: unknown) {\n if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {\n if (err) {\n closeWithError(lc, ws, err);\n } else {\n lc.info?.(`closing connection to ${endpoint}`);\n ws.close();\n }\n }\n }\n\n const instream = Subscription.create<In>({\n ...inOptions,\n cleanup: (unconsumed, err) => {\n inOptions.cleanup?.(unconsumed, err);\n close(err);\n },\n });\n const outstream = Subscription.create<Out>({\n ...outOptions,\n cleanup: (unconsumed, err) => {\n outOptions.cleanup?.(unconsumed, err);\n close(err);\n },\n });\n\n const duplex = createWebSocketStream(ws, {\n ...streamOptions,\n decodeStrings: false,\n });\n\n // Outgoing transform.\n function streamOut() {\n // Mainly used for verifying that back-pressure kicks in tests.\n duplex.on('drain', () => lc.debug?.(`drained messages to ${endpoint}`));\n\n pipeline(\n Readable.from(outstream),\n new Transform({\n objectMode: true,\n transform: (msg, _encoding, callback) =>\n callback(null, BigIntJSON.stringify(msg)),\n }),\n duplex,\n err => (err ? outstream.fail(err) : outstream.cancel()),\n );\n }\n\n if (ws.readyState === ws.CONNECTING) {\n ws.on('open', () => {\n lc.info?.(`connected to ${endpoint}`);\n streamOut();\n });\n } else {\n streamOut();\n }\n\n // Incoming transform.\n pipe({\n source: duplex,\n sink: instream,\n parse: chunk => {\n const json = BigIntJSON.parse(chunk.toString());\n return v.parse(json, inSchema, 'passthrough');\n },\n });\n\n sendPingsForLiveness(lc, ws, PING_INTERVAL_MS);\n\n return {outstream, instream};\n}\n\ntype PipeOptions<T> = {\n source: Readable;\n sink: Subscription<T>;\n parse: (buffer: Buffer) => T | null;\n bufferMessages?: number;\n};\n\nexport function pipe<T>({source, sink, parse, bufferMessages}: PipeOptions<T>) {\n bufferMessages ??= 0;\n assert(bufferMessages >= 0, 'bufferMessages must be non-negative');\n const pending: Promise<unknown>[] = [];\n\n pipeline(\n source,\n new Writable({\n decodeStrings: false,\n write: (chunk, _encoding, callback) => {\n let msg: T | null;\n try {\n if ((msg = parse(chunk)) === null) {\n callback();\n return;\n }\n } catch (err) {\n callback(ensureError(err));\n return;\n }\n // Inbound backpressure is exerted by unconsumed messages in the\n // subscription. A buffer can be used to allow messages to queue up in\n // in the Subscription object, which allows the consumer to \"peek\" at\n // whether there are more messages immediately available\n // (via {@link Subscription.queued}.\n const {result} = sink.push(msg);\n pending.push(result);\n void result.then(() => pending.shift());\n\n if (pending.length <= bufferMessages) {\n // immediately allow more messages\n callback();\n } else {\n // wait for the oldest result in the pending queue\n pending[0].then(\n () => callback(),\n err => callback(ensureError(err)),\n );\n }\n },\n destroy: (err, callback) => {\n if (err) {\n sink.fail(ensureError(err));\n }\n // Otherwise, final will handle the cancel.\n callback();\n },\n final: callback => {\n sink.cancel();\n callback();\n },\n }),\n err => (err ? sink.fail(err) : sink.cancel()),\n );\n}\n\nfunction ensureError(err: unknown) {\n return err instanceof Error ? err : new Error(String(err));\n}\n\nconst ackSchema = v.object({ack: v.number()});\n\ntype Ack = v.Infer<typeof ackSchema>;\n\ntype Streamed<T> = {\n /** Application-level message. */\n msg: T;\n\n /** ID used for the Ack message. */\n id: number;\n};\n\nexport function streamOut<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\n): Promise<void> {\n return streamOutInternal(lc, source, sink, BigIntJSON.stringify);\n}\n\n/**\n * Streams out a `Source` for which messages are already stringified JSON.\n */\nexport function streamOutStringified(\n lc: LogContext,\n source: Source<string>,\n sink: WebSocket,\n): Promise<void> {\n return streamOutInternal(lc, source, sink, json => json);\n}\n\nasync function streamOutInternal<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\n stringify: (payload: T) => string,\n): Promise<void> {\n sendPingsForLiveness(lc, sink, PING_INTERVAL_MS);\n\n const closer = WebSocketCloser.forSource(lc, sink, source);\n\n const acks = new Queue<Ack>();\n sink.addEventListener('message', ({data}) => {\n try {\n if (typeof data !== 'string') {\n throw new Error('Expected string message');\n }\n acks.enqueue(v.parse(JSON.parse(data), ackSchema));\n } catch (e) {\n lc.error?.(`error parsing ack`, e);\n closer.close(e);\n }\n });\n\n try {\n let nextID = 0;\n const {pipeline} = source;\n if (pipeline) {\n lc.debug?.(`started pipelined outbound stream`);\n for await (const {value: msg, consumed} of pipeline) {\n const id = ++nextID;\n const data = `{\"id\":${id},\"msg\":${stringify(msg)}}`;\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`pipelining`, data);\n sink.send(data);\n\n void (async () => {\n const {ack} = await acks.dequeue();\n // lc.debug?.(`received ack`, ack);\n if (ack !== id) {\n throw new Error(`Unexpected ack for ${id}: ${ack}`);\n }\n consumed();\n })();\n }\n } else {\n lc.debug?.(`started synchronous outbound stream`);\n for await (const msg of source) {\n const id = ++nextID;\n const data = `{\"id\":${id},\"msg\":${stringify(msg)}}`;\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`sending`, data);\n sink.send(data);\n\n const {ack} = await acks.dequeue();\n if (ack !== id) {\n throw new Error(`Unexpected ack for ${id}: ${ack}`);\n }\n }\n }\n closer.close();\n } catch (e) {\n closer.close(e);\n }\n}\n\nexport async function streamIn<T extends JSONValue>(\n lc: LogContext,\n source: WebSocket,\n schema: v.Type<T>,\n): Promise<Source<T>> {\n expectPingsForLiveness(lc, source, PING_INTERVAL_MS);\n\n const streamedSchema = v.object({\n msg: schema,\n id: v.number(),\n });\n\n const sink: Subscription<T, Streamed<T>> = new Subscription<T, Streamed<T>>(\n {\n consumed: ({id}) => source.send(JSON.stringify({ack: id} satisfies Ack)),\n cleanup: () => closer.close(),\n },\n ({msg}) => msg,\n );\n\n const closer = WebSocketCloser.forSink(lc, source, sink, handleMessage);\n\n function handleMessage(event: MessageEvent) {\n const data = event.data.toString();\n if (!sink.active) {\n lc.warn?.('dropping ws message received after close', data);\n return;\n }\n try {\n const value = BigIntJSON.parse(data);\n const msg = v.parse(value, streamedSchema, 'passthrough');\n // Enable for debugging. Otherwise too verbose.\n // lc.debug?.(`received`, data);\n sink.push(msg);\n } catch (e) {\n closer.close(e);\n }\n }\n\n await closer.connected;\n return sink;\n}\n\nclass WebSocketCloser {\n readonly #lc: LogContext;\n readonly #ws: WebSocket;\n readonly #closeStream: () => void;\n readonly #messageHandler: ((e: MessageEvent) => void | undefined) | null;\n readonly #connected = resolver();\n\n get connected(): Promise<void> {\n return this.#connected.promise;\n }\n\n static forSource<T>(lc: LogContext, ws: WebSocket, stream: Source<T>) {\n // If the websocket is closed, call cancel() to notify the Source of\n // any unconsumed messages.\n return new WebSocketCloser(lc, ws, () => stream.cancel());\n }\n\n static forSink<T>(\n lc: LogContext,\n ws: WebSocket,\n stream: Subscription<T, Streamed<T>>,\n messageHandler: (e: MessageEvent) => void | undefined,\n ) {\n // If the websocket is closed, call end() to allow the downstream Sink\n // to process any pending messages before closing the stream.\n return new WebSocketCloser(lc, ws, () => stream.end(), messageHandler);\n }\n\n private constructor(\n lc: LogContext,\n ws: WebSocket,\n closeStream: () => void,\n messageHandler?: (e: MessageEvent) => void | undefined,\n ) {\n this.#lc = lc;\n this.#ws = ws;\n this.#closeStream = closeStream;\n this.#messageHandler = messageHandler ?? null;\n\n ws.addEventListener('open', this.#handleOpen);\n ws.addEventListener('close', this.#handleClose);\n ws.addEventListener('error', this.#handleError);\n if (this.#messageHandler) {\n ws.addEventListener('message', this.#messageHandler);\n }\n\n switch (ws.readyState) {\n case ws.CONNECTING:\n break; // expected for new connections. resolve or reject in handlers.\n case ws.OPEN:\n this.#connected.resolve();\n break;\n default:\n this.#connected.reject(\n new Error(`websocket already in state ${ws.readyState}`),\n );\n break;\n }\n }\n\n get #conn(): string {\n return 'connection' + (this.#ws.url ? ` to ${this.#ws.url}` : '');\n }\n\n #handleOpen = () => {\n this.#lc.info?.(`${this.#conn} established`);\n this.#connected.resolve();\n };\n\n #handleClose = (e: CloseEvent) => {\n const {code, reason, wasClean} = e;\n this.#lc.info?.(`${this.#conn} closed`, {\n code,\n reason,\n wasClean,\n });\n this.close();\n this.#connected.reject(`${this.#conn} closed with code ${code}`);\n };\n\n #handleError = ({message, error}: ErrorEvent) => {\n if (this.#ws.readyState === this.#ws.OPEN) {\n this.#lc.error?.(`error in ${this.#conn}`, message, error);\n }\n this.#connected.reject(error);\n };\n\n close(err?: unknown) {\n if (err) {\n this.#lc.error?.(`closing stream with error`, err);\n }\n this.#closeStream();\n if (!this.closed()) {\n this.#ws.close();\n }\n }\n\n closed() {\n return (\n this.#ws.readyState === this.#ws.CLOSED ||\n this.#ws.readyState === this.#ws.CLOSING\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strings.js","names":[],"sources":["../../../../../zero-cache/src/types/strings.ts"],"sourcesContent":["export function elide(val: string, maxBytes: number) {\n const encoder = new TextEncoder();\n if (encoder.encode(val).length <= maxBytes) {\n return val;\n }\n val = val.substring(0, maxBytes - 3);\n while (encoder.encode(val + '...').length > maxBytes) {\n val = val.substring(0, val.length - 1);\n }\n return val + '...';\n}\n"],"mappings":";AAAA,SAAgB,MAAM,KAAa,UAAkB;CACnD,MAAM,UAAU,IAAI,
|
|
1
|
+
{"version":3,"file":"strings.js","names":[],"sources":["../../../../../zero-cache/src/types/strings.ts"],"sourcesContent":["export function elide(val: string, maxBytes: number) {\n const encoder = new TextEncoder();\n if (encoder.encode(val).length <= maxBytes) {\n return val;\n }\n val = val.substring(0, maxBytes - 3);\n while (encoder.encode(val + '...').length > maxBytes) {\n val = val.substring(0, val.length - 1);\n }\n return val + '...';\n}\n"],"mappings":";AAAA,SAAgB,MAAM,KAAa,UAAkB;CACnD,MAAM,UAAU,IAAI,YAAY;CAChC,IAAI,QAAQ,OAAO,GAAG,EAAE,UAAU,UAChC,OAAO;CAET,MAAM,IAAI,UAAU,GAAG,WAAW,CAAC;CACnC,OAAO,QAAQ,OAAO,MAAM,KAAK,EAAE,SAAS,UAC1C,MAAM,IAAI,UAAU,GAAG,IAAI,SAAS,CAAC;CAEvC,OAAO,MAAM;AACf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.js","names":["#consumers","#messages","#consuming","#pipelineEnabled","#coalesce","#consumed","#cleanup","#publish","#sentinel","#terminate","#pipeline"],"sources":["../../../../../zero-cache/src/types/subscription.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport type {Sink, Source} from './streams.ts';\n\n/**\n * A Subscription abstracts a continuous, logically infinite stream of messages intended\n * for serial processing. Unlike the more general Node `Stream` API, a Subscription has\n * a limited API with specific semantics:\n *\n * * **Serial processing**: Messages must be consumed via the {@link AsyncIterable}\n * interface, e.g.\n * ```ts\n * const subscription = server.subscribe(parameters);\n *\n * for await (const message of subscription) {\n * await process(message); // fully process the message before consuming the next\n * }\n * ```\n *\n * Moreover, the consumer is expected to completely process each message before\n * requesting the next. This is important for cleanup semantics (explained later).\n *\n * * **cancel()**, not close(): The underlying data in a subscription is logically infinite\n * and only terminated when the consumer is no longer interested in receiving the messages\n * (or requires a Subscription with a different configuration). As such, there is no API\n * for gracefully closing the subscription after pending messages are consumed; rather,\n * cancellation is immediate, and upon cancellation, pending messages are dropped. A\n * Subscription can also be terminated with exceptional (i.e. `Error`) circumstances,\n * for which the behavior is equivalent.\n *\n * * **Coalescing** (optional): A producer can configure pending messages in the Subscription\n * to be merged together with a {@link Options.coalesce coalesce} function. This is useful\n * for semantics in which the consumer is not necessarily interested in every incremental\n * change, but rather the cumulative change since the last processed message. A\n * Subscription with coalescing is guaranteed to have at most one outstanding message,\n * regardless of how quickly messages are produced and consumed. This effectively constrains\n * the amount of outstanding work in the system.\n *\n * ### Resource Tracking and Cleanup\n *\n * Because message consumption is constrained to the async iteration API, standard\n * control flow mechanisms allow the producer to perform bookkeeping without any\n * explicit cleanup actions on the part of the consumer. This includes:\n *\n * * **Per-message cleanup**: Each request for the {@link AsyncIterator.next next}\n * message, or the termination of the iteration, signals that the consumer has\n * finished processing the previous message. The producer of a Subscription can\n * supply a {@link Options.consumed consumed} callback to receive these processed\n * messages, allowing it to clean up attached resources (e.g. TransactionPools, etc.).\n *\n * * **Per-subscription cleanup**: The producer of a Subscription can supply a\n * {@link Options.cleanup cleanup} callback that is invoked when the Subscription\n * is terminated, either explicitly via {@link Subscription.cancel cancel()} /\n * {@link Subscription.fail fail()}, or implicitly when an iteration is exited via a\n * `break`, `return`, or `throw` statement. All unconsumed messages are passed to the\n * call back to facilitate bookkeeping.\n *\n * @param T The external message type, published to the AsyncIterable\n * @param M The internal message type used in the producer-side interfaces\n * (e.g. {@link push}, {@link Options.consumed}, {@link Options.coalesce},\n * and {@link Options.cleanup}). This is often the same as the external type\n * T, but may be diverged to facilitate internal bookkeeping.\n */\nexport class Subscription<T, M = T> implements Source<T>, Sink<M> {\n /**\n * Convenience factory method for creating a {@link Subscription} with internal message type\n * `M` as a subtype of `T`, defaulting to the same type. The default `publish` method publishes\n * the message of type `M` directly to the AsyncIterable.\n */\n static create<T, M extends T = T>(\n options: Options<M> = {},\n publish: (m: M) => T = m => m,\n ) {\n return new Subscription(options, publish);\n }\n\n // Consumers waiting to consume messages (i.e. an async iteration awaiting the next message).\n readonly #consumers: Resolver<Entry<M> | null>[] = [];\n // Messages waiting to be dequeued.\n readonly #messages: (Entry<M> | 'terminus')[] = [];\n // Messages dequeued but not yet consumed.\n readonly #consuming = new Set<Entry<M>>();\n readonly #pipelineEnabled: boolean;\n // Sentinel value signaling that the subscription is \"done\" and no more\n // messages can be added.\n #sentinel: 'canceled' | Error | undefined = undefined;\n\n #coalesce: ((curr: Entry<M>, prev: Entry<M>) => M) | undefined;\n #consumed: (prev: Entry<M>) => void;\n #cleanup: (unconsumed: Entry<M>[], err?: Error) => void;\n #publish: (internal: M) => T;\n\n /**\n * @param publish function for converting the internally pushed / coalesced message\n * of type `M` to the external type `T` exposed via async iteration.\n */\n constructor(options: Options<M> = {}, publish: (m: M) => T) {\n const {\n coalesce,\n consumed = () => {},\n cleanup = () => {},\n pipeline = coalesce === undefined,\n } = options;\n\n this.#coalesce = !coalesce\n ? undefined\n : (curr, prev) => {\n try {\n return coalesce(curr.value, prev.value);\n } finally {\n prev.resolve('coalesced');\n }\n };\n\n this.#consumed = entry => {\n consumed(entry.value);\n this.#consuming.delete(entry);\n entry.resolve('consumed');\n };\n\n this.#cleanup = (entries, err) => {\n cleanup(\n entries.map(e => e.value),\n err,\n );\n entries.forEach(e => e.resolve('unconsumed'));\n };\n\n this.#publish = publish;\n\n this.#pipelineEnabled = pipeline;\n }\n\n /**\n * Pushes the next message to be consumed, and returns a `result` that resolves to the\n * eventual {@link Result} of the `value`.\n *\n * If there is an existing unconsumed message and the Subscription has a\n * {@link Options#coalesce coalesce} function, the specified `value` will be coalesced\n * with the pending message. In this case, the result of the pending message\n * is resolved to `coalesced`, regardless of the `coalesce` function implementation.\n *\n * If the subscription is in a terminal state, the message is dropped and the\n * result resolves to `unconsumed`.\n */\n push(value: M): PendingResult {\n const {promise: result, resolve} = resolver<Result>();\n const entry = {value, resolve};\n\n if (this.#sentinel) {\n entry.resolve('unconsumed');\n return {result};\n }\n const consumer = this.#consumers.shift();\n if (consumer) {\n consumer.resolve(entry);\n } else if (\n this.#coalesce &&\n this.#messages.length &&\n this.#messages.at(-1) !== 'terminus'\n ) {\n // oxlint-disable-next-line typescript/no-non-null-assertion\n const prev = this.#messages.at(-1)!;\n assert(prev !== 'terminus', 'prev should not be terminus after check');\n this.#messages[this.#messages.length - 1] = {\n value: this.#coalesce(entry, prev),\n resolve,\n };\n } else {\n this.#messages.push(entry);\n }\n return {result};\n }\n\n /** False if the subscription has been canceled or has failed. */\n get active(): boolean {\n return this.#sentinel === undefined;\n }\n\n /** The number of messages waiting to be dequeued. */\n get queued(): number {\n return this.#messages.length;\n }\n\n /** The number of messages dequeued but not yet \"consumed\" */\n get consuming(): number {\n return this.#consuming.size;\n }\n\n /**\n * Cancels the subscription after any queued messages are consumed. This is\n * meant for the producer-side code.\n *\n * Any messages pushed after calling `end()` will be unconsumed as if\n * `cancel()` were called (once the first set of pending messages is\n * consumed). In particular, if a coalesce function is defined, the new\n * messages will not be coalesced with the messages enqueued before `end()`\n * was called. However, to effect the intent of memory efficiency, multiple\n * messages pushed after calling `end()` will be coalesced together.\n *\n */\n end() {\n if (this.#sentinel) {\n // already terminated\n } else if (this.#messages.length === 0) {\n this.cancel();\n } else {\n this.#messages.push('terminus');\n }\n }\n\n /**\n * Cancels the subscription immediately, cleans up, and terminates any iteration.\n * This is intended for the consumer to call when it is no longer interested\n * in the subscription.\n *\n * @param err If an `err` is specified, an iteration over the Subscription /\n * Sink will throw the `err` (equivalent to the producer calling\n * {@link fail()}). If undefined, the iteration will exit gracefully.\n */\n cancel(err?: Error) {\n this.#terminate(err ?? 'canceled');\n }\n\n /** Fails the subscription, cleans up, and throws from any iteration. */\n fail(err: Error) {\n this.#terminate(err);\n }\n\n #terminate(sentinel: 'canceled' | Error) {\n if (!this.#sentinel) {\n this.#sentinel = sentinel;\n this.#cleanup(\n [...this.#consuming, ...this.#messages.filter(m => m !== 'terminus')],\n sentinel instanceof Error ? sentinel : undefined,\n );\n this.#messages.splice(0);\n\n for (\n let consumer = this.#consumers.shift();\n consumer;\n consumer = this.#consumers.shift()\n ) {\n sentinel === 'canceled'\n ? consumer.resolve(null)\n : consumer.reject(sentinel);\n }\n }\n }\n\n get pipeline(): AsyncIterable<{value: T; consumed: () => void}> | undefined {\n return this.#pipelineEnabled\n ? {[Symbol.asyncIterator]: () => this.#pipeline()}\n : undefined;\n }\n\n #pipeline(): AsyncIterator<{value: T; consumed: () => void}> {\n return {\n next: async () => {\n const entry = this.#messages.shift();\n if (entry === 'terminus') {\n this.cancel();\n return {value: undefined, done: true};\n }\n if (entry !== undefined) {\n this.#consuming.add(entry);\n return {\n value: {\n value: this.#publish(entry.value),\n consumed: () => this.#consumed(entry),\n },\n };\n }\n if (this.#sentinel === 'canceled') {\n return {value: undefined, done: true};\n }\n if (this.#sentinel) {\n return Promise.reject(this.#sentinel);\n }\n const consumer = resolver<Entry<M> | null>();\n this.#consumers.push(consumer);\n\n // Wait for push() (or termination) to resolve the consumer.\n const result = await consumer.promise;\n if (result !== null) {\n this.#consuming.add(result);\n return {\n value: {\n value: this.#publish(result.value),\n consumed: () => this.#consumed(result),\n },\n };\n }\n return {value: undefined, done: true};\n },\n\n return: value => {\n this.cancel();\n return Promise.resolve({value, done: true});\n },\n };\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n const delegate = this.#pipeline();\n\n let prevConsumed = () => {};\n return {\n next: async () => {\n prevConsumed();\n\n const entry = await delegate.next();\n if (entry.done) {\n return entry;\n }\n\n const {value, consumed} = entry.value;\n prevConsumed = consumed;\n return {value};\n },\n\n return: value => {\n prevConsumed();\n\n this.cancel();\n return Promise.resolve({value, done: true});\n },\n };\n }\n}\n\nexport type Options<M> = {\n /**\n * Coalesces messages waiting to be consumed. This is useful for \"watermark\" type\n * subscriptions in which the consumer is only interested in the cumulative state\n * change since the last processed message. When a `coalesce` function is specified,\n * there is guaranteed to be at most one message waiting to be consumed.\n *\n * Note that the `curr` argument comes before `prev`. This facilitates a common\n * scenario in which coalescing just means using the newest value; in such a case,\n * `coalesce` can simply be the identity function (e.g. `msg => msg`).\n */\n coalesce?: (curr: M, prev: M) => M;\n\n /**\n * Called on the previous message in an iteration (1) when the next message is requested,\n * or (2) when the iteration is terminated. This allows the producer to perform\n * per-message cleanup.\n *\n * Note that when a {@link Options.coalesce coalesce} function is defined,\n * `consumed` is _not_ called on the `prev` message; it is the responsibility of\n * producers requiring both coalescing and consumption notification to perform any\n * necessary cleanup of `prev` messages when coalescing.\n */\n consumed?: (prev: M) => void;\n\n /**\n * `cleanup` is called exactly once when the subscription is terminated via a failure or\n * cancelation (whichever happens first), which includes implicit cancelation when\n * the consumer exits an iteration via a `break`, `return`, or `throw` statement.\n *\n * Note that the `err` argument will only reflect an explicit cancelation via a call\n * to {@link Subscription.fail()}. On the other hand, if the iteration is canceled via\n * a `throw` statement, the thrown reason is not reflected in the `err` parameter, as that\n * information is not made available to the AsyncIterator implementation.\n */\n cleanup?: (unconsumed: M[], err?: Error) => void;\n\n /**\n * Enable or disable pipelining when streaming messages over a websocket.\n *\n * If unspecified, pipelining is enabled if there is no {@link Options.coalesce coalesce}\n * method, as pipelining is counter to the semantics of coalescing. However, the\n * application can explicitly enable pipelining even if there is a coalesce method\n * by specifying `true` for this option. This assumes that coalescing is either\n * not important for websocket semantics, or that the receiving end of the websocket\n * transport performs the desired coalescing.\n */\n pipeline?: boolean;\n};\n\n/** Post-queueing results of messages. */\nexport type Result = 'consumed' | 'coalesced' | 'unconsumed';\n\n/**\n * {@link Subscription.subscribe()} wraps the `Promise<Result>` in a `PendingResult`\n * object to avoid forcing all callers to handle the Promise, as most logic does not\n * need to.\n */\nexport type PendingResult = {result: Promise<Result>};\n\ntype Entry<M> = {\n readonly value: M;\n readonly resolve: (r: Result) => void;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,IAAa,eAAb,MAAa,aAAqD;;;;;;CAMhE,OAAO,OACL,UAAsB,EAAE,EACxB,WAAuB,MAAK,GAC5B;AACA,SAAO,IAAI,aAAa,SAAS,QAAQ;;CAI3C,aAAmD,EAAE;CAErD,YAAgD,EAAE;CAElD,6BAAsB,IAAI,KAAe;CACzC;CAGA,YAA4C,KAAA;CAE5C;CACA;CACA;CACA;;;;;CAMA,YAAY,UAAsB,EAAE,EAAE,SAAsB;EAC1D,MAAM,EACJ,UACA,iBAAiB,IACjB,gBAAgB,IAChB,WAAW,aAAa,KAAA,MACtB;AAEJ,QAAA,WAAiB,CAAC,WACd,KAAA,KACC,MAAM,SAAS;AACd,OAAI;AACF,WAAO,SAAS,KAAK,OAAO,KAAK,MAAM;aAC/B;AACR,SAAK,QAAQ,YAAY;;;AAIjC,QAAA,YAAiB,UAAS;AACxB,YAAS,MAAM,MAAM;AACrB,SAAA,UAAgB,OAAO,MAAM;AAC7B,SAAM,QAAQ,WAAW;;AAG3B,QAAA,WAAiB,SAAS,QAAQ;AAChC,WACE,QAAQ,KAAI,MAAK,EAAE,MAAM,EACzB,IACD;AACD,WAAQ,SAAQ,MAAK,EAAE,QAAQ,aAAa,CAAC;;AAG/C,QAAA,UAAgB;AAEhB,QAAA,kBAAwB;;;;;;;;;;;;;;CAe1B,KAAK,OAAyB;EAC5B,MAAM,EAAC,SAAS,QAAQ,YAAW,UAAkB;EACrD,MAAM,QAAQ;GAAC;GAAO;GAAQ;AAE9B,MAAI,MAAA,UAAgB;AAClB,SAAM,QAAQ,aAAa;AAC3B,UAAO,EAAC,QAAO;;EAEjB,MAAM,WAAW,MAAA,UAAgB,OAAO;AACxC,MAAI,SACF,UAAS,QAAQ,MAAM;WAEvB,MAAA,YACA,MAAA,SAAe,UACf,MAAA,SAAe,GAAG,GAAG,KAAK,YAC1B;GAEA,MAAM,OAAO,MAAA,SAAe,GAAG,GAAG;AAClC,UAAO,SAAS,YAAY,0CAA0C;AACtE,SAAA,SAAe,MAAA,SAAe,SAAS,KAAK;IAC1C,OAAO,MAAA,SAAe,OAAO,KAAK;IAClC;IACD;QAED,OAAA,SAAe,KAAK,MAAM;AAE5B,SAAO,EAAC,QAAO;;;CAIjB,IAAI,SAAkB;AACpB,SAAO,MAAA,aAAmB,KAAA;;;CAI5B,IAAI,SAAiB;AACnB,SAAO,MAAA,SAAe;;;CAIxB,IAAI,YAAoB;AACtB,SAAO,MAAA,UAAgB;;;;;;;;;;;;;;CAezB,MAAM;AACJ,MAAI,MAAA,UAAgB,YAET,MAAA,SAAe,WAAW,EACnC,MAAK,QAAQ;MAEb,OAAA,SAAe,KAAK,WAAW;;;;;;;;;;;CAanC,OAAO,KAAa;AAClB,QAAA,UAAgB,OAAO,WAAW;;;CAIpC,KAAK,KAAY;AACf,QAAA,UAAgB,IAAI;;CAGtB,WAAW,UAA8B;AACvC,MAAI,CAAC,MAAA,UAAgB;AACnB,SAAA,WAAiB;AACjB,SAAA,QACE,CAAC,GAAG,MAAA,WAAiB,GAAG,MAAA,SAAe,QAAO,MAAK,MAAM,WAAW,CAAC,EACrE,oBAAoB,QAAQ,WAAW,KAAA,EACxC;AACD,SAAA,SAAe,OAAO,EAAE;AAExB,QACE,IAAI,WAAW,MAAA,UAAgB,OAAO,EACtC,UACA,WAAW,MAAA,UAAgB,OAAO,CAElC,cAAa,aACT,SAAS,QAAQ,KAAK,GACtB,SAAS,OAAO,SAAS;;;CAKnC,IAAI,WAAwE;AAC1E,SAAO,MAAA,kBACH,GAAE,OAAO,sBAAsB,MAAA,UAAgB,EAAC,GAChD,KAAA;;CAGN,YAA6D;AAC3D,SAAO;GACL,MAAM,YAAY;IAChB,MAAM,QAAQ,MAAA,SAAe,OAAO;AACpC,QAAI,UAAU,YAAY;AACxB,UAAK,QAAQ;AACb,YAAO;MAAC,OAAO,KAAA;MAAW,MAAM;MAAK;;AAEvC,QAAI,UAAU,KAAA,GAAW;AACvB,WAAA,UAAgB,IAAI,MAAM;AAC1B,YAAO,EACL,OAAO;MACL,OAAO,MAAA,QAAc,MAAM,MAAM;MACjC,gBAAgB,MAAA,SAAe,MAAM;MACtC,EACF;;AAEH,QAAI,MAAA,aAAmB,WACrB,QAAO;KAAC,OAAO,KAAA;KAAW,MAAM;KAAK;AAEvC,QAAI,MAAA,SACF,QAAO,QAAQ,OAAO,MAAA,SAAe;IAEvC,MAAM,WAAW,UAA2B;AAC5C,UAAA,UAAgB,KAAK,SAAS;IAG9B,MAAM,SAAS,MAAM,SAAS;AAC9B,QAAI,WAAW,MAAM;AACnB,WAAA,UAAgB,IAAI,OAAO;AAC3B,YAAO,EACL,OAAO;MACL,OAAO,MAAA,QAAc,OAAO,MAAM;MAClC,gBAAgB,MAAA,SAAe,OAAO;MACvC,EACF;;AAEH,WAAO;KAAC,OAAO,KAAA;KAAW,MAAM;KAAK;;GAGvC,SAAQ,UAAS;AACf,SAAK,QAAQ;AACb,WAAO,QAAQ,QAAQ;KAAC;KAAO,MAAM;KAAK,CAAC;;GAE9C;;CAGH,CAAC,OAAO,iBAAmC;EACzC,MAAM,WAAW,MAAA,UAAgB;EAEjC,IAAI,qBAAqB;AACzB,SAAO;GACL,MAAM,YAAY;AAChB,kBAAc;IAEd,MAAM,QAAQ,MAAM,SAAS,MAAM;AACnC,QAAI,MAAM,KACR,QAAO;IAGT,MAAM,EAAC,OAAO,aAAY,MAAM;AAChC,mBAAe;AACf,WAAO,EAAC,OAAM;;GAGhB,SAAQ,UAAS;AACf,kBAAc;AAEd,SAAK,QAAQ;AACb,WAAO,QAAQ,QAAQ;KAAC;KAAO,MAAM;KAAK,CAAC;;GAE9C"}
|
|
1
|
+
{"version":3,"file":"subscription.js","names":["#consumers","#messages","#consuming","#pipelineEnabled","#coalesce","#consumed","#cleanup","#publish","#sentinel","#terminate","#pipeline"],"sources":["../../../../../zero-cache/src/types/subscription.ts"],"sourcesContent":["import {resolver, type Resolver} from '@rocicorp/resolver';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport type {Sink, Source} from './streams.ts';\n\n/**\n * A Subscription abstracts a continuous, logically infinite stream of messages intended\n * for serial processing. Unlike the more general Node `Stream` API, a Subscription has\n * a limited API with specific semantics:\n *\n * * **Serial processing**: Messages must be consumed via the {@link AsyncIterable}\n * interface, e.g.\n * ```ts\n * const subscription = server.subscribe(parameters);\n *\n * for await (const message of subscription) {\n * await process(message); // fully process the message before consuming the next\n * }\n * ```\n *\n * Moreover, the consumer is expected to completely process each message before\n * requesting the next. This is important for cleanup semantics (explained later).\n *\n * * **cancel()**, not close(): The underlying data in a subscription is logically infinite\n * and only terminated when the consumer is no longer interested in receiving the messages\n * (or requires a Subscription with a different configuration). As such, there is no API\n * for gracefully closing the subscription after pending messages are consumed; rather,\n * cancellation is immediate, and upon cancellation, pending messages are dropped. A\n * Subscription can also be terminated with exceptional (i.e. `Error`) circumstances,\n * for which the behavior is equivalent.\n *\n * * **Coalescing** (optional): A producer can configure pending messages in the Subscription\n * to be merged together with a {@link Options.coalesce coalesce} function. This is useful\n * for semantics in which the consumer is not necessarily interested in every incremental\n * change, but rather the cumulative change since the last processed message. A\n * Subscription with coalescing is guaranteed to have at most one outstanding message,\n * regardless of how quickly messages are produced and consumed. This effectively constrains\n * the amount of outstanding work in the system.\n *\n * ### Resource Tracking and Cleanup\n *\n * Because message consumption is constrained to the async iteration API, standard\n * control flow mechanisms allow the producer to perform bookkeeping without any\n * explicit cleanup actions on the part of the consumer. This includes:\n *\n * * **Per-message cleanup**: Each request for the {@link AsyncIterator.next next}\n * message, or the termination of the iteration, signals that the consumer has\n * finished processing the previous message. The producer of a Subscription can\n * supply a {@link Options.consumed consumed} callback to receive these processed\n * messages, allowing it to clean up attached resources (e.g. TransactionPools, etc.).\n *\n * * **Per-subscription cleanup**: The producer of a Subscription can supply a\n * {@link Options.cleanup cleanup} callback that is invoked when the Subscription\n * is terminated, either explicitly via {@link Subscription.cancel cancel()} /\n * {@link Subscription.fail fail()}, or implicitly when an iteration is exited via a\n * `break`, `return`, or `throw` statement. All unconsumed messages are passed to the\n * call back to facilitate bookkeeping.\n *\n * @param T The external message type, published to the AsyncIterable\n * @param M The internal message type used in the producer-side interfaces\n * (e.g. {@link push}, {@link Options.consumed}, {@link Options.coalesce},\n * and {@link Options.cleanup}). This is often the same as the external type\n * T, but may be diverged to facilitate internal bookkeeping.\n */\nexport class Subscription<T, M = T> implements Source<T>, Sink<M> {\n /**\n * Convenience factory method for creating a {@link Subscription} with internal message type\n * `M` as a subtype of `T`, defaulting to the same type. The default `publish` method publishes\n * the message of type `M` directly to the AsyncIterable.\n */\n static create<T, M extends T = T>(\n options: Options<M> = {},\n publish: (m: M) => T = m => m,\n ) {\n return new Subscription(options, publish);\n }\n\n // Consumers waiting to consume messages (i.e. an async iteration awaiting the next message).\n readonly #consumers: Resolver<Entry<M> | null>[] = [];\n // Messages waiting to be dequeued.\n readonly #messages: (Entry<M> | 'terminus')[] = [];\n // Messages dequeued but not yet consumed.\n readonly #consuming = new Set<Entry<M>>();\n readonly #pipelineEnabled: boolean;\n // Sentinel value signaling that the subscription is \"done\" and no more\n // messages can be added.\n #sentinel: 'canceled' | Error | undefined = undefined;\n\n #coalesce: ((curr: Entry<M>, prev: Entry<M>) => M) | undefined;\n #consumed: (prev: Entry<M>) => void;\n #cleanup: (unconsumed: Entry<M>[], err?: Error) => void;\n #publish: (internal: M) => T;\n\n /**\n * @param publish function for converting the internally pushed / coalesced message\n * of type `M` to the external type `T` exposed via async iteration.\n */\n constructor(options: Options<M> = {}, publish: (m: M) => T) {\n const {\n coalesce,\n consumed = () => {},\n cleanup = () => {},\n pipeline = coalesce === undefined,\n } = options;\n\n this.#coalesce = !coalesce\n ? undefined\n : (curr, prev) => {\n try {\n return coalesce(curr.value, prev.value);\n } finally {\n prev.resolve('coalesced');\n }\n };\n\n this.#consumed = entry => {\n consumed(entry.value);\n this.#consuming.delete(entry);\n entry.resolve('consumed');\n };\n\n this.#cleanup = (entries, err) => {\n cleanup(\n entries.map(e => e.value),\n err,\n );\n entries.forEach(e => e.resolve('unconsumed'));\n };\n\n this.#publish = publish;\n\n this.#pipelineEnabled = pipeline;\n }\n\n /**\n * Pushes the next message to be consumed, and returns a `result` that resolves to the\n * eventual {@link Result} of the `value`.\n *\n * If there is an existing unconsumed message and the Subscription has a\n * {@link Options#coalesce coalesce} function, the specified `value` will be coalesced\n * with the pending message. In this case, the result of the pending message\n * is resolved to `coalesced`, regardless of the `coalesce` function implementation.\n *\n * If the subscription is in a terminal state, the message is dropped and the\n * result resolves to `unconsumed`.\n */\n push(value: M): PendingResult {\n const {promise: result, resolve} = resolver<Result>();\n const entry = {value, resolve};\n\n if (this.#sentinel) {\n entry.resolve('unconsumed');\n return {result};\n }\n const consumer = this.#consumers.shift();\n if (consumer) {\n consumer.resolve(entry);\n } else if (\n this.#coalesce &&\n this.#messages.length &&\n this.#messages.at(-1) !== 'terminus'\n ) {\n // oxlint-disable-next-line typescript/no-non-null-assertion\n const prev = this.#messages.at(-1)!;\n assert(prev !== 'terminus', 'prev should not be terminus after check');\n this.#messages[this.#messages.length - 1] = {\n value: this.#coalesce(entry, prev),\n resolve,\n };\n } else {\n this.#messages.push(entry);\n }\n return {result};\n }\n\n /** False if the subscription has been canceled or has failed. */\n get active(): boolean {\n return this.#sentinel === undefined;\n }\n\n /** The number of messages waiting to be dequeued. */\n get queued(): number {\n return this.#messages.length;\n }\n\n /** The number of messages dequeued but not yet \"consumed\" */\n get consuming(): number {\n return this.#consuming.size;\n }\n\n /**\n * Cancels the subscription after any queued messages are consumed. This is\n * meant for the producer-side code.\n *\n * Any messages pushed after calling `end()` will be unconsumed as if\n * `cancel()` were called (once the first set of pending messages is\n * consumed). In particular, if a coalesce function is defined, the new\n * messages will not be coalesced with the messages enqueued before `end()`\n * was called. However, to effect the intent of memory efficiency, multiple\n * messages pushed after calling `end()` will be coalesced together.\n *\n */\n end() {\n if (this.#sentinel) {\n // already terminated\n } else if (this.#messages.length === 0) {\n this.cancel();\n } else {\n this.#messages.push('terminus');\n }\n }\n\n /**\n * Cancels the subscription immediately, cleans up, and terminates any iteration.\n * This is intended for the consumer to call when it is no longer interested\n * in the subscription.\n *\n * @param err If an `err` is specified, an iteration over the Subscription /\n * Sink will throw the `err` (equivalent to the producer calling\n * {@link fail()}). If undefined, the iteration will exit gracefully.\n */\n cancel(err?: Error) {\n this.#terminate(err ?? 'canceled');\n }\n\n /** Fails the subscription, cleans up, and throws from any iteration. */\n fail(err: Error) {\n this.#terminate(err);\n }\n\n #terminate(sentinel: 'canceled' | Error) {\n if (!this.#sentinel) {\n this.#sentinel = sentinel;\n this.#cleanup(\n [...this.#consuming, ...this.#messages.filter(m => m !== 'terminus')],\n sentinel instanceof Error ? sentinel : undefined,\n );\n this.#messages.splice(0);\n\n for (\n let consumer = this.#consumers.shift();\n consumer;\n consumer = this.#consumers.shift()\n ) {\n sentinel === 'canceled'\n ? consumer.resolve(null)\n : consumer.reject(sentinel);\n }\n }\n }\n\n get pipeline(): AsyncIterable<{value: T; consumed: () => void}> | undefined {\n return this.#pipelineEnabled\n ? {[Symbol.asyncIterator]: () => this.#pipeline()}\n : undefined;\n }\n\n #pipeline(): AsyncIterator<{value: T; consumed: () => void}> {\n return {\n next: async () => {\n const entry = this.#messages.shift();\n if (entry === 'terminus') {\n this.cancel();\n return {value: undefined, done: true};\n }\n if (entry !== undefined) {\n this.#consuming.add(entry);\n return {\n value: {\n value: this.#publish(entry.value),\n consumed: () => this.#consumed(entry),\n },\n };\n }\n if (this.#sentinel === 'canceled') {\n return {value: undefined, done: true};\n }\n if (this.#sentinel) {\n return Promise.reject(this.#sentinel);\n }\n const consumer = resolver<Entry<M> | null>();\n this.#consumers.push(consumer);\n\n // Wait for push() (or termination) to resolve the consumer.\n const result = await consumer.promise;\n if (result !== null) {\n this.#consuming.add(result);\n return {\n value: {\n value: this.#publish(result.value),\n consumed: () => this.#consumed(result),\n },\n };\n }\n return {value: undefined, done: true};\n },\n\n return: value => {\n this.cancel();\n return Promise.resolve({value, done: true});\n },\n };\n }\n\n [Symbol.asyncIterator](): AsyncIterator<T> {\n const delegate = this.#pipeline();\n\n let prevConsumed = () => {};\n return {\n next: async () => {\n prevConsumed();\n\n const entry = await delegate.next();\n if (entry.done) {\n return entry;\n }\n\n const {value, consumed} = entry.value;\n prevConsumed = consumed;\n return {value};\n },\n\n return: value => {\n prevConsumed();\n\n this.cancel();\n return Promise.resolve({value, done: true});\n },\n };\n }\n}\n\nexport type Options<M> = {\n /**\n * Coalesces messages waiting to be consumed. This is useful for \"watermark\" type\n * subscriptions in which the consumer is only interested in the cumulative state\n * change since the last processed message. When a `coalesce` function is specified,\n * there is guaranteed to be at most one message waiting to be consumed.\n *\n * Note that the `curr` argument comes before `prev`. This facilitates a common\n * scenario in which coalescing just means using the newest value; in such a case,\n * `coalesce` can simply be the identity function (e.g. `msg => msg`).\n */\n coalesce?: (curr: M, prev: M) => M;\n\n /**\n * Called on the previous message in an iteration (1) when the next message is requested,\n * or (2) when the iteration is terminated. This allows the producer to perform\n * per-message cleanup.\n *\n * Note that when a {@link Options.coalesce coalesce} function is defined,\n * `consumed` is _not_ called on the `prev` message; it is the responsibility of\n * producers requiring both coalescing and consumption notification to perform any\n * necessary cleanup of `prev` messages when coalescing.\n */\n consumed?: (prev: M) => void;\n\n /**\n * `cleanup` is called exactly once when the subscription is terminated via a failure or\n * cancelation (whichever happens first), which includes implicit cancelation when\n * the consumer exits an iteration via a `break`, `return`, or `throw` statement.\n *\n * Note that the `err` argument will only reflect an explicit cancelation via a call\n * to {@link Subscription.fail()}. On the other hand, if the iteration is canceled via\n * a `throw` statement, the thrown reason is not reflected in the `err` parameter, as that\n * information is not made available to the AsyncIterator implementation.\n */\n cleanup?: (unconsumed: M[], err?: Error) => void;\n\n /**\n * Enable or disable pipelining when streaming messages over a websocket.\n *\n * If unspecified, pipelining is enabled if there is no {@link Options.coalesce coalesce}\n * method, as pipelining is counter to the semantics of coalescing. However, the\n * application can explicitly enable pipelining even if there is a coalesce method\n * by specifying `true` for this option. This assumes that coalescing is either\n * not important for websocket semantics, or that the receiving end of the websocket\n * transport performs the desired coalescing.\n */\n pipeline?: boolean;\n};\n\n/** Post-queueing results of messages. */\nexport type Result = 'consumed' | 'coalesced' | 'unconsumed';\n\n/**\n * {@link Subscription.subscribe()} wraps the `Promise<Result>` in a `PendingResult`\n * object to avoid forcing all callers to handle the Promise, as most logic does not\n * need to.\n */\nexport type PendingResult = {result: Promise<Result>};\n\ntype Entry<M> = {\n readonly value: M;\n readonly resolve: (r: Result) => void;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeout.js","names":[],"sources":["../../../../../zero-cache/src/types/timeout.ts"],"sourcesContent":["import {sleepWithAbort} from '../../../shared/src/sleep.ts';\n\n/**\n * Resolves to the the string `\"timed-out\"` if `timeoutMs` elapses before\n * the specified `promise` resolves.\n */\nexport function orTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n): Promise<T | 'timed-out'> {\n return orTimeoutWith(promise, timeoutMs, 'timed-out');\n}\n\n/**\n * Resolves to the specified `timeoutValue` if `timeoutMs` elapses before\n * the specified `promise` resolves.\n */\nexport async function orTimeoutWith<T, U>(\n promise: Promise<T>,\n timeoutMs: number,\n timeoutValue: U,\n): Promise<T | U> {\n const ac = new AbortController();\n const [timeout] = sleepWithAbort(timeoutMs, ac.signal);\n try {\n return await Promise.race([promise, timeout.then(() => timeoutValue)]);\n } finally {\n ac.abort();\n }\n}\n"],"mappings":";;;;;;AAMA,SAAgB,UACd,SACA,WAC0B;
|
|
1
|
+
{"version":3,"file":"timeout.js","names":[],"sources":["../../../../../zero-cache/src/types/timeout.ts"],"sourcesContent":["import {sleepWithAbort} from '../../../shared/src/sleep.ts';\n\n/**\n * Resolves to the the string `\"timed-out\"` if `timeoutMs` elapses before\n * the specified `promise` resolves.\n */\nexport function orTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n): Promise<T | 'timed-out'> {\n return orTimeoutWith(promise, timeoutMs, 'timed-out');\n}\n\n/**\n * Resolves to the specified `timeoutValue` if `timeoutMs` elapses before\n * the specified `promise` resolves.\n */\nexport async function orTimeoutWith<T, U>(\n promise: Promise<T>,\n timeoutMs: number,\n timeoutValue: U,\n): Promise<T | U> {\n const ac = new AbortController();\n const [timeout] = sleepWithAbort(timeoutMs, ac.signal);\n try {\n return await Promise.race([promise, timeout.then(() => timeoutValue)]);\n } finally {\n ac.abort();\n }\n}\n"],"mappings":";;;;;;AAMA,SAAgB,UACd,SACA,WAC0B;CAC1B,OAAO,cAAc,SAAS,WAAW,WAAW;AACtD;;;;;AAMA,eAAsB,cACpB,SACA,WACA,cACgB;CAChB,MAAM,KAAK,IAAI,gBAAgB;CAC/B,MAAM,CAAC,WAAW,eAAe,WAAW,GAAG,MAAM;CACrD,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,WAAW,YAAY,CAAC,CAAC;CACvE,UAAU;EACR,GAAG,MAAM;CACX;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-params.js","names":[],"sources":["../../../../../zero-cache/src/types/url-params.ts"],"sourcesContent":["export class URLParams {\n readonly url: URL;\n\n constructor(url: URL) {\n this.url = url;\n }\n\n get(name: string, required: true): string;\n get(name: string, required: boolean): string | null;\n get(name: string, required: boolean) {\n const value = this.url.searchParams.get(name);\n if (value === '' || value === null) {\n if (required) {\n throw new Error(`invalid querystring - missing ${name}`);\n }\n return null;\n }\n return value;\n }\n\n getInteger(name: string, required: true): number;\n getInteger(name: string, required: boolean): number | null;\n getInteger(name: string, required: boolean) {\n const value = this.get(name, required);\n if (value === null) {\n return null;\n }\n const int = parseInt(value);\n if (isNaN(int)) {\n throw new Error(\n `invalid querystring parameter ${name}, got: ${value}, url: ${this.url}`,\n );\n }\n return int;\n }\n\n getBoolean(name: string): boolean {\n const value = this.get(name, false);\n if (value === null) {\n return false;\n }\n return value === 'true';\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,MAAuB;CACrB;CAEA,YAAY,KAAU;
|
|
1
|
+
{"version":3,"file":"url-params.js","names":[],"sources":["../../../../../zero-cache/src/types/url-params.ts"],"sourcesContent":["export class URLParams {\n readonly url: URL;\n\n constructor(url: URL) {\n this.url = url;\n }\n\n get(name: string, required: true): string;\n get(name: string, required: boolean): string | null;\n get(name: string, required: boolean) {\n const value = this.url.searchParams.get(name);\n if (value === '' || value === null) {\n if (required) {\n throw new Error(`invalid querystring - missing ${name}`);\n }\n return null;\n }\n return value;\n }\n\n getInteger(name: string, required: true): number;\n getInteger(name: string, required: boolean): number | null;\n getInteger(name: string, required: boolean) {\n const value = this.get(name, required);\n if (value === null) {\n return null;\n }\n const int = parseInt(value);\n if (isNaN(int)) {\n throw new Error(\n `invalid querystring parameter ${name}, got: ${value}, url: ${this.url}`,\n );\n }\n return int;\n }\n\n getBoolean(name: string): boolean {\n const value = this.get(name, false);\n if (value === null) {\n return false;\n }\n return value === 'true';\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,MAAuB;CACrB;CAEA,YAAY,KAAU;EACpB,KAAK,MAAM;CACb;CAIA,IAAI,MAAc,UAAmB;EACnC,MAAM,QAAQ,KAAK,IAAI,aAAa,IAAI,IAAI;EAC5C,IAAI,UAAU,MAAM,UAAU,MAAM;GAClC,IAAI,UACF,MAAM,IAAI,MAAM,iCAAiC,MAAM;GAEzD,OAAO;EACT;EACA,OAAO;CACT;CAIA,WAAW,MAAc,UAAmB;EAC1C,MAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ;EACrC,IAAI,UAAU,MACZ,OAAO;EAET,MAAM,MAAM,SAAS,KAAK;EAC1B,IAAI,MAAM,GAAG,GACX,MAAM,IAAI,MACR,iCAAiC,KAAK,SAAS,MAAM,SAAS,KAAK,KACrE;EAEF,OAAO;CACT;CAEA,WAAW,MAAuB;EAChC,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK;EAClC,IAAI,UAAU,MACZ,OAAO;EAET,OAAO,UAAU;CACnB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-handoff.js","names":[],"sources":["../../../../../zero-cache/src/types/websocket-handoff.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport {Server} from 'node:http';\nimport type {Socket} from 'node:net';\nimport type {LogContext} from '@rocicorp/logger';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {serializableSubset, type IncomingMessageSubset} from './http.ts';\nimport type {MESSAGE_TYPES} from './processes.ts';\nimport {type Receiver, type Sender, type Worker} from './processes.ts';\nimport {closeWithError, PROTOCOL_ERROR} from './ws.ts';\n\nexport type HandoffSpec<P> = {\n payload: P;\n sender: Sender;\n};\n\n/**\n * The WebSocketHandoff is a function that either returns the payload and\n * receiver, or invokes the specified `callback` with the payload and\n * receiver. It must not do both.\n *\n * Similarly, an error can be handled by throwing synchronously from the\n * function, or invoking the `onerror` callback.\n */\nexport type WebSocketHandoff<P> = (\n message: IncomingMessageSubset,\n callback: (h: HandoffSpec<P>) => void,\n onerror: (reason: unknown) => void,\n) => HandoffSpec<P> | void;\n\nexport type WebSocketReceiver<P> = (\n ws: WebSocket,\n payload: P,\n msg: IncomingMessageSubset,\n) => void;\n\nexport type WebSocketHandoffHandler = (\n message: IncomingMessageSubset,\n socket: Socket,\n head: ArrayBuffer,\n) => void;\n\n/**\n * Installs websocket handoff logic from either an http.Server\n * receiving requests, or a parent Worker process\n * that is handing off requests to this process.\n */\nexport function createWebSocketHandoffHandler<P>(\n lc: LogContext,\n handoff: WebSocketHandoff<P>,\n serverOptions?: ServerOptions,\n): WebSocketHandoffHandler {\n const wss = new WebSocketServer(\n serverOptions ?? {\n noServer: true,\n },\n );\n return (\n message: IncomingMessageSubset,\n socket: Socket,\n head: ArrayBuffer,\n ) => {\n let sent = false;\n\n function send({payload, sender}: HandoffSpec<P>) {\n assert(!sent, 'Handoff callback already invoked');\n sent = true;\n\n const data = [\n 'handoff',\n {\n message: serializableSubset(message),\n head,\n payload,\n },\n ] satisfies Handoff<P>;\n\n // \"This event is guaranteed to be passed an instance of the <net.Socket> class\"\n // https://nodejs.org/api/http.html#event-upgrade\n sender.send(data, socket);\n }\n\n function onError(error: unknown) {\n // Returning an error on the HTTP handshake looks like a hanging connection\n // (at least from Chrome) and doesn't report any meaningful error in the browser.\n // Instead, finish the upgrade to a websocket and then close it with an error.\n wss.handleUpgrade(\n message as IncomingMessage,\n socket,\n Buffer.from(head),\n ws => closeWithError(lc, ws, error, PROTOCOL_ERROR),\n );\n }\n\n try {\n const spec = handoff(message, send, onError);\n if (spec) {\n send(spec);\n }\n } catch (error) {\n onError(error);\n }\n };\n}\n\n/**\n * Installs websocket handoff logic from either an http.Server\n * receiving requests, or a parent Worker process\n * that is handing off requests to this process.\n */\nexport function installWebSocketHandoff<P>(\n lc: LogContext,\n handoff: WebSocketHandoff<P>,\n source: Server | Worker,\n serverOptions?: ServerOptions,\n) {\n const handle = createWebSocketHandoffHandler(lc, handoff, serverOptions);\n\n if (source instanceof Server) {\n // handoff messages from an HTTP server\n source.on('upgrade', handle);\n } else {\n // handoff messages from this worker's parent.\n source.onMessageType<Handoff<P>>('handoff', (msg, socket) => {\n const {message, head} = msg;\n handle(message, socket as Socket, head);\n });\n }\n}\n\nexport function installWebSocketReceiver<P>(\n lc: LogContext,\n server: WebSocketServer,\n receive: WebSocketReceiver<P>,\n receiver: Receiver,\n) {\n receiver.onMessageType<Handoff<P>>('handoff', (msg, socket) => {\n // Per https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback\n //\n // > Any 'message' handlers in the subprocess should verify that socket\n // > exists, as the connection may have been closed during the time it\n // > takes to send the connection to the child.\n if (!socket) {\n lc.warn?.('websocket closed during handoff');\n return;\n }\n const {message, head, payload} = msg;\n server.handleUpgrade(\n message as IncomingMessage,\n socket as Socket,\n Buffer.from(head),\n ws => {\n // Guard against WebSocket being closed during handoff.\n // This can happen due to network issues or client disconnection\n // between the time the socket was sent and when handleUpgrade completes.\n if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {\n lc.warn?.('websocket closed during upgrade, skipping receive');\n return;\n }\n receive(ws, payload, message);\n },\n );\n });\n}\n\nexport type Handoff<P> = [\n typeof MESSAGE_TYPES.handoff,\n {\n message: IncomingMessageSubset;\n head: ArrayBuffer;\n payload: P;\n },\n];\n"],"mappings":";;;;;;;;;;;;AA+CA,SAAgB,8BACd,IACA,SACA,eACyB;CACzB,MAAM,MAAM,IAAI,gBACd,iBAAiB,EACf,UAAU,
|
|
1
|
+
{"version":3,"file":"websocket-handoff.js","names":[],"sources":["../../../../../zero-cache/src/types/websocket-handoff.ts"],"sourcesContent":["import type {IncomingMessage} from 'node:http';\nimport {Server} from 'node:http';\nimport type {Socket} from 'node:net';\nimport type {LogContext} from '@rocicorp/logger';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {serializableSubset, type IncomingMessageSubset} from './http.ts';\nimport type {MESSAGE_TYPES} from './processes.ts';\nimport {type Receiver, type Sender, type Worker} from './processes.ts';\nimport {closeWithError, PROTOCOL_ERROR} from './ws.ts';\n\nexport type HandoffSpec<P> = {\n payload: P;\n sender: Sender;\n};\n\n/**\n * The WebSocketHandoff is a function that either returns the payload and\n * receiver, or invokes the specified `callback` with the payload and\n * receiver. It must not do both.\n *\n * Similarly, an error can be handled by throwing synchronously from the\n * function, or invoking the `onerror` callback.\n */\nexport type WebSocketHandoff<P> = (\n message: IncomingMessageSubset,\n callback: (h: HandoffSpec<P>) => void,\n onerror: (reason: unknown) => void,\n) => HandoffSpec<P> | void;\n\nexport type WebSocketReceiver<P> = (\n ws: WebSocket,\n payload: P,\n msg: IncomingMessageSubset,\n) => void;\n\nexport type WebSocketHandoffHandler = (\n message: IncomingMessageSubset,\n socket: Socket,\n head: ArrayBuffer,\n) => void;\n\n/**\n * Installs websocket handoff logic from either an http.Server\n * receiving requests, or a parent Worker process\n * that is handing off requests to this process.\n */\nexport function createWebSocketHandoffHandler<P>(\n lc: LogContext,\n handoff: WebSocketHandoff<P>,\n serverOptions?: ServerOptions,\n): WebSocketHandoffHandler {\n const wss = new WebSocketServer(\n serverOptions ?? {\n noServer: true,\n },\n );\n return (\n message: IncomingMessageSubset,\n socket: Socket,\n head: ArrayBuffer,\n ) => {\n let sent = false;\n\n function send({payload, sender}: HandoffSpec<P>) {\n assert(!sent, 'Handoff callback already invoked');\n sent = true;\n\n const data = [\n 'handoff',\n {\n message: serializableSubset(message),\n head,\n payload,\n },\n ] satisfies Handoff<P>;\n\n // \"This event is guaranteed to be passed an instance of the <net.Socket> class\"\n // https://nodejs.org/api/http.html#event-upgrade\n sender.send(data, socket);\n }\n\n function onError(error: unknown) {\n // Returning an error on the HTTP handshake looks like a hanging connection\n // (at least from Chrome) and doesn't report any meaningful error in the browser.\n // Instead, finish the upgrade to a websocket and then close it with an error.\n wss.handleUpgrade(\n message as IncomingMessage,\n socket,\n Buffer.from(head),\n ws => closeWithError(lc, ws, error, PROTOCOL_ERROR),\n );\n }\n\n try {\n const spec = handoff(message, send, onError);\n if (spec) {\n send(spec);\n }\n } catch (error) {\n onError(error);\n }\n };\n}\n\n/**\n * Installs websocket handoff logic from either an http.Server\n * receiving requests, or a parent Worker process\n * that is handing off requests to this process.\n */\nexport function installWebSocketHandoff<P>(\n lc: LogContext,\n handoff: WebSocketHandoff<P>,\n source: Server | Worker,\n serverOptions?: ServerOptions,\n) {\n const handle = createWebSocketHandoffHandler(lc, handoff, serverOptions);\n\n if (source instanceof Server) {\n // handoff messages from an HTTP server\n source.on('upgrade', handle);\n } else {\n // handoff messages from this worker's parent.\n source.onMessageType<Handoff<P>>('handoff', (msg, socket) => {\n const {message, head} = msg;\n handle(message, socket as Socket, head);\n });\n }\n}\n\nexport function installWebSocketReceiver<P>(\n lc: LogContext,\n server: WebSocketServer,\n receive: WebSocketReceiver<P>,\n receiver: Receiver,\n) {\n receiver.onMessageType<Handoff<P>>('handoff', (msg, socket) => {\n // Per https://nodejs.org/api/child_process.html#subprocesssendmessage-sendhandle-options-callback\n //\n // > Any 'message' handlers in the subprocess should verify that socket\n // > exists, as the connection may have been closed during the time it\n // > takes to send the connection to the child.\n if (!socket) {\n lc.warn?.('websocket closed during handoff');\n return;\n }\n const {message, head, payload} = msg;\n server.handleUpgrade(\n message as IncomingMessage,\n socket as Socket,\n Buffer.from(head),\n ws => {\n // Guard against WebSocket being closed during handoff.\n // This can happen due to network issues or client disconnection\n // between the time the socket was sent and when handleUpgrade completes.\n if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {\n lc.warn?.('websocket closed during upgrade, skipping receive');\n return;\n }\n receive(ws, payload, message);\n },\n );\n });\n}\n\nexport type Handoff<P> = [\n typeof MESSAGE_TYPES.handoff,\n {\n message: IncomingMessageSubset;\n head: ArrayBuffer;\n payload: P;\n },\n];\n"],"mappings":";;;;;;;;;;;;AA+CA,SAAgB,8BACd,IACA,SACA,eACyB;CACzB,MAAM,MAAM,IAAI,gBACd,iBAAiB,EACf,UAAU,KACZ,CACF;CACA,QACE,SACA,QACA,SACG;EACH,IAAI,OAAO;EAEX,SAAS,KAAK,EAAC,SAAS,UAAyB;GAC/C,OAAO,CAAC,MAAM,kCAAkC;GAChD,OAAO;GAEP,MAAM,OAAO,CACX,WACA;IACE,SAAS,mBAAmB,OAAO;IACnC;IACA;GACF,CACF;GAIA,OAAO,KAAK,MAAM,MAAM;EAC1B;EAEA,SAAS,QAAQ,OAAgB;GAI/B,IAAI,cACF,SACA,QACA,OAAO,KAAK,IAAI,IAChB,OAAM,eAAe,IAAI,IAAI,OAAO,cAAc,CACpD;EACF;EAEA,IAAI;GACF,MAAM,OAAO,QAAQ,SAAS,MAAM,OAAO;GAC3C,IAAI,MACF,KAAK,IAAI;EAEb,SAAS,OAAO;GACd,QAAQ,KAAK;EACf;CACF;AACF;;;;;;AAOA,SAAgB,wBACd,IACA,SACA,QACA,eACA;CACA,MAAM,SAAS,8BAA8B,IAAI,SAAS,aAAa;CAEvE,IAAI,kBAAkB,QAEpB,OAAO,GAAG,WAAW,MAAM;MAG3B,OAAO,cAA0B,YAAY,KAAK,WAAW;EAC3D,MAAM,EAAC,SAAS,SAAQ;EACxB,OAAO,SAAS,QAAkB,IAAI;CACxC,CAAC;AAEL;AAEA,SAAgB,yBACd,IACA,QACA,SACA,UACA;CACA,SAAS,cAA0B,YAAY,KAAK,WAAW;EAM7D,IAAI,CAAC,QAAQ;GACX,GAAG,OAAO,iCAAiC;GAC3C;EACF;EACA,MAAM,EAAC,SAAS,MAAM,YAAW;EACjC,OAAO,cACL,SACA,QACA,OAAO,KAAK,IAAI,IAChB,OAAM;GAIJ,IAAI,GAAG,eAAe,GAAG,UAAU,GAAG,eAAe,GAAG,SAAS;IAC/D,GAAG,OAAO,mDAAmD;IAC7D;GACF;GACA,QAAQ,IAAI,SAAS,OAAO;EAC9B,CACF;CACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.js","names":[],"sources":["../../../../../zero-cache/src/types/ws.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {WebSocket} from 'ws';\nimport {elide} from './strings.ts';\n\n// https://github.com/Luka967/websocket-close-codes\nexport const PROTOCOL_ERROR = 1002;\nexport const INTERNAL_ERROR = 1011;\n\nexport type ErrorCode = typeof PROTOCOL_ERROR | typeof INTERNAL_ERROR;\n\nexport function closeWithError(\n lc: LogContext,\n ws: WebSocket,\n err: unknown,\n code: ErrorCode = INTERNAL_ERROR,\n) {\n const endpoint = ws.url ?? 'client';\n const errMsg = String(err);\n lc.warn?.(`closing connection to ${endpoint} with error`, errMsg);\n\n // close messages must be less than or equal to 123 bytes:\n // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason\n ws.close(code, elide(errMsg, 123));\n}\n\nexport function sendPingsForLiveness(\n lc: LogContext,\n ws: WebSocket,\n intervalMs: number,\n) {\n let gotLivenessSignal = true;\n\n let livenessTimer: NodeJS.Timeout | undefined;\n function startHeartBeats() {\n livenessTimer = setInterval(() => {\n if (!gotLivenessSignal) {\n lc.warn?.(\n `socket@${ws.url} did not respond to heartbeat. Terminating...`,\n );\n ws.terminate();\n return;\n }\n // Reset gotLivenessSignal and expect another pong or message to arrive\n // before the next interval elapses.\n gotLivenessSignal = false;\n ws.ping();\n }, intervalMs);\n }\n\n if (ws.readyState === ws.CONNECTING) {\n ws.once('open', () => startHeartBeats());\n } else if (ws.readyState === ws.OPEN) {\n startHeartBeats();\n }\n\n // Both pongs and messages are accepted as signs of liveness.\n // Checking for pongs only risks false positives as pongs may be backed\n // up behind a large stream of messages.\n const signalAlive = () => (gotLivenessSignal = true);\n ws.on('pong', signalAlive);\n ws.on('message', signalAlive);\n ws.once('close', () => clearInterval(livenessTimer));\n}\n\nexport function expectPingsForLiveness(\n lc: LogContext,\n ws: WebSocket,\n intervalMs: number,\n timeoutBufferMs = 3_000,\n) {\n let gotLivenessSignal = false;\n\n const livenessTimer = setInterval(() => {\n if (!gotLivenessSignal) {\n lc.warn?.(\n `socket@${ws.url} did not send heartbeat or messages. Terminating...`,\n );\n ws.terminate();\n return;\n }\n // Reset gotLivenessSignal and expect another ping or message to arrive\n // before the next interval elapses.\n gotLivenessSignal = false;\n }, intervalMs + timeoutBufferMs);\n\n // Both pings and messages are accepted as signs of liveness.\n // Checking for pings only risks false positives as pings may be backed\n // up behind a large stream of messages.\n const signalAlive = () => (gotLivenessSignal = true);\n ws.on('ping', signalAlive);\n ws.on('message', signalAlive);\n ws.once('close', () => clearTimeout(livenessTimer));\n}\n"],"mappings":";;AAKA,IAAa,iBAAiB;AAC9B,IAAa,iBAAiB;AAI9B,SAAgB,eACd,IACA,IACA,KACA,OAAkB,gBAClB;CACA,MAAM,WAAW,GAAG,OAAO;CAC3B,MAAM,SAAS,OAAO,
|
|
1
|
+
{"version":3,"file":"ws.js","names":[],"sources":["../../../../../zero-cache/src/types/ws.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport type {WebSocket} from 'ws';\nimport {elide} from './strings.ts';\n\n// https://github.com/Luka967/websocket-close-codes\nexport const PROTOCOL_ERROR = 1002;\nexport const INTERNAL_ERROR = 1011;\n\nexport type ErrorCode = typeof PROTOCOL_ERROR | typeof INTERNAL_ERROR;\n\nexport function closeWithError(\n lc: LogContext,\n ws: WebSocket,\n err: unknown,\n code: ErrorCode = INTERNAL_ERROR,\n) {\n const endpoint = ws.url ?? 'client';\n const errMsg = String(err);\n lc.warn?.(`closing connection to ${endpoint} with error`, errMsg);\n\n // close messages must be less than or equal to 123 bytes:\n // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason\n ws.close(code, elide(errMsg, 123));\n}\n\nexport function sendPingsForLiveness(\n lc: LogContext,\n ws: WebSocket,\n intervalMs: number,\n) {\n let gotLivenessSignal = true;\n\n let livenessTimer: NodeJS.Timeout | undefined;\n function startHeartBeats() {\n livenessTimer = setInterval(() => {\n if (!gotLivenessSignal) {\n lc.warn?.(\n `socket@${ws.url} did not respond to heartbeat. Terminating...`,\n );\n ws.terminate();\n return;\n }\n // Reset gotLivenessSignal and expect another pong or message to arrive\n // before the next interval elapses.\n gotLivenessSignal = false;\n ws.ping();\n }, intervalMs);\n }\n\n if (ws.readyState === ws.CONNECTING) {\n ws.once('open', () => startHeartBeats());\n } else if (ws.readyState === ws.OPEN) {\n startHeartBeats();\n }\n\n // Both pongs and messages are accepted as signs of liveness.\n // Checking for pongs only risks false positives as pongs may be backed\n // up behind a large stream of messages.\n const signalAlive = () => (gotLivenessSignal = true);\n ws.on('pong', signalAlive);\n ws.on('message', signalAlive);\n ws.once('close', () => clearInterval(livenessTimer));\n}\n\nexport function expectPingsForLiveness(\n lc: LogContext,\n ws: WebSocket,\n intervalMs: number,\n timeoutBufferMs = 3_000,\n) {\n let gotLivenessSignal = false;\n\n const livenessTimer = setInterval(() => {\n if (!gotLivenessSignal) {\n lc.warn?.(\n `socket@${ws.url} did not send heartbeat or messages. Terminating...`,\n );\n ws.terminate();\n return;\n }\n // Reset gotLivenessSignal and expect another ping or message to arrive\n // before the next interval elapses.\n gotLivenessSignal = false;\n }, intervalMs + timeoutBufferMs);\n\n // Both pings and messages are accepted as signs of liveness.\n // Checking for pings only risks false positives as pings may be backed\n // up behind a large stream of messages.\n const signalAlive = () => (gotLivenessSignal = true);\n ws.on('ping', signalAlive);\n ws.on('message', signalAlive);\n ws.once('close', () => clearTimeout(livenessTimer));\n}\n"],"mappings":";;AAKA,IAAa,iBAAiB;AAC9B,IAAa,iBAAiB;AAI9B,SAAgB,eACd,IACA,IACA,KACA,OAAkB,gBAClB;CACA,MAAM,WAAW,GAAG,OAAO;CAC3B,MAAM,SAAS,OAAO,GAAG;CACzB,GAAG,OAAO,yBAAyB,SAAS,cAAc,MAAM;CAIhE,GAAG,MAAM,MAAM,MAAM,QAAQ,GAAG,CAAC;AACnC;AAEA,SAAgB,qBACd,IACA,IACA,YACA;CACA,IAAI,oBAAoB;CAExB,IAAI;CACJ,SAAS,kBAAkB;EACzB,gBAAgB,kBAAkB;GAChC,IAAI,CAAC,mBAAmB;IACtB,GAAG,OACD,UAAU,GAAG,IAAI,8CACnB;IACA,GAAG,UAAU;IACb;GACF;GAGA,oBAAoB;GACpB,GAAG,KAAK;EACV,GAAG,UAAU;CACf;CAEA,IAAI,GAAG,eAAe,GAAG,YACvB,GAAG,KAAK,cAAc,gBAAgB,CAAC;MAClC,IAAI,GAAG,eAAe,GAAG,MAC9B,gBAAgB;CAMlB,MAAM,oBAAqB,oBAAoB;CAC/C,GAAG,GAAG,QAAQ,WAAW;CACzB,GAAG,GAAG,WAAW,WAAW;CAC5B,GAAG,KAAK,eAAe,cAAc,aAAa,CAAC;AACrD;AAEA,SAAgB,uBACd,IACA,IACA,YACA,kBAAkB,KAClB;CACA,IAAI,oBAAoB;CAExB,MAAM,gBAAgB,kBAAkB;EACtC,IAAI,CAAC,mBAAmB;GACtB,GAAG,OACD,UAAU,GAAG,IAAI,oDACnB;GACA,GAAG,UAAU;GACb;EACF;EAGA,oBAAoB;CACtB,GAAG,aAAa,eAAe;CAK/B,MAAM,oBAAqB,oBAAoB;CAC/C,GAAG,GAAG,QAAQ,WAAW;CACzB,GAAG,GAAG,WAAW,WAAW;CAC5B,GAAG,KAAK,eAAe,aAAa,aAAa,CAAC;AACpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-params.js","names":[],"sources":["../../../../../zero-cache/src/workers/connect-params.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http2';\nimport {must} from '../../../shared/src/must.ts';\nimport {\n decodeSecProtocols,\n type InitConnectionMessage,\n} from '../../../zero-protocol/src/connect.ts';\nimport {URLParams} from '../types/url-params.ts';\n\nexport type ConnectParams = {\n readonly protocolVersion: number;\n readonly clientID: string;\n readonly clientGroupID: string;\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly timestamp: number;\n readonly lmID: number;\n readonly wsID: string;\n readonly debugPerf: boolean;\n readonly auth: string | undefined;\n readonly userID: string | undefined;\n readonly initConnectionMsg: InitConnectionMessage | undefined;\n readonly httpCookie: string | undefined;\n readonly origin: string | undefined;\n};\n\nexport function getConnectParams(\n protocolVersion: number,\n url: URL,\n headers: IncomingHttpHeaders,\n):\n | {\n params: ConnectParams;\n error: null;\n }\n | {\n params: null;\n error: string;\n } {\n const params = new URLParams(url);\n\n try {\n const clientID = params.get('clientID', true);\n const clientGroupID = params.get('clientGroupID', true);\n const profileID = params.get('profileID', false);\n const baseCookie = params.get('baseCookie', false);\n const timestamp = params.getInteger('ts', true);\n const lmID = params.getInteger('lmid', true);\n const wsID = params.get('wsid', false) ?? '';\n const userID = params.get('userID', false) ?? undefined;\n const debugPerf = params.getBoolean('debugPerf');\n const {initConnectionMessage, authToken} = decodeSecProtocols(\n must(headers['sec-websocket-protocol']),\n );\n\n return {\n params: {\n protocolVersion,\n clientID,\n clientGroupID,\n profileID,\n baseCookie,\n timestamp,\n lmID,\n wsID,\n debugPerf,\n initConnectionMsg: initConnectionMessage,\n auth: authToken,\n userID,\n httpCookie: headers.cookie,\n origin: headers.origin,\n },\n error: null,\n };\n } catch (e) {\n return {\n params: null,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n"],"mappings":";;;;AAyBA,SAAgB,iBACd,iBACA,KACA,SASI;CACJ,MAAM,SAAS,IAAI,UAAU,
|
|
1
|
+
{"version":3,"file":"connect-params.js","names":[],"sources":["../../../../../zero-cache/src/workers/connect-params.ts"],"sourcesContent":["import type {IncomingHttpHeaders} from 'node:http2';\nimport {must} from '../../../shared/src/must.ts';\nimport {\n decodeSecProtocols,\n type InitConnectionMessage,\n} from '../../../zero-protocol/src/connect.ts';\nimport {URLParams} from '../types/url-params.ts';\n\nexport type ConnectParams = {\n readonly protocolVersion: number;\n readonly clientID: string;\n readonly clientGroupID: string;\n readonly profileID: string | null;\n readonly baseCookie: string | null;\n readonly timestamp: number;\n readonly lmID: number;\n readonly wsID: string;\n readonly debugPerf: boolean;\n readonly auth: string | undefined;\n readonly userID: string | undefined;\n readonly initConnectionMsg: InitConnectionMessage | undefined;\n readonly httpCookie: string | undefined;\n readonly origin: string | undefined;\n};\n\nexport function getConnectParams(\n protocolVersion: number,\n url: URL,\n headers: IncomingHttpHeaders,\n):\n | {\n params: ConnectParams;\n error: null;\n }\n | {\n params: null;\n error: string;\n } {\n const params = new URLParams(url);\n\n try {\n const clientID = params.get('clientID', true);\n const clientGroupID = params.get('clientGroupID', true);\n const profileID = params.get('profileID', false);\n const baseCookie = params.get('baseCookie', false);\n const timestamp = params.getInteger('ts', true);\n const lmID = params.getInteger('lmid', true);\n const wsID = params.get('wsid', false) ?? '';\n const userID = params.get('userID', false) ?? undefined;\n const debugPerf = params.getBoolean('debugPerf');\n const {initConnectionMessage, authToken} = decodeSecProtocols(\n must(headers['sec-websocket-protocol']),\n );\n\n return {\n params: {\n protocolVersion,\n clientID,\n clientGroupID,\n profileID,\n baseCookie,\n timestamp,\n lmID,\n wsID,\n debugPerf,\n initConnectionMsg: initConnectionMessage,\n auth: authToken,\n userID,\n httpCookie: headers.cookie,\n origin: headers.origin,\n },\n error: null,\n };\n } catch (e) {\n return {\n params: null,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n"],"mappings":";;;;AAyBA,SAAgB,iBACd,iBACA,KACA,SASI;CACJ,MAAM,SAAS,IAAI,UAAU,GAAG;CAEhC,IAAI;EACF,MAAM,WAAW,OAAO,IAAI,YAAY,IAAI;EAC5C,MAAM,gBAAgB,OAAO,IAAI,iBAAiB,IAAI;EACtD,MAAM,YAAY,OAAO,IAAI,aAAa,KAAK;EAC/C,MAAM,aAAa,OAAO,IAAI,cAAc,KAAK;EACjD,MAAM,YAAY,OAAO,WAAW,MAAM,IAAI;EAC9C,MAAM,OAAO,OAAO,WAAW,QAAQ,IAAI;EAC3C,MAAM,OAAO,OAAO,IAAI,QAAQ,KAAK,KAAK;EAC1C,MAAM,SAAS,OAAO,IAAI,UAAU,KAAK,KAAK,KAAA;EAC9C,MAAM,YAAY,OAAO,WAAW,WAAW;EAC/C,MAAM,EAAC,uBAAuB,cAAa,mBACzC,KAAK,QAAQ,yBAAyB,CACxC;EAEA,OAAO;GACL,QAAQ;IACN;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,mBAAmB;IACnB,MAAM;IACN;IACA,YAAY,QAAQ;IACpB,QAAQ,QAAQ;GAClB;GACA,OAAO;EACT;CACF,SAAS,GAAG;EACV,OAAO;GACL,QAAQ;GACR,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;EAClD;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","names":["#ws","#wsID","#protocolVersion","#lc","#onClose","#messageHandler","#downstreamMsgTimer","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#lastDownstreamMsgTime"],"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 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 {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.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 type: 'stream';\n source: 'viewSyncer' | '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;\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 #downstreamMsgTimer: NodeJS.Timeout | undefined;\n\n #viewSyncerOutboundStream: Source<Downstream> | 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.#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 this.#proxyOutbound(result.stream);\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 this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\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(outboundStream: Source<Downstream>) {\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: Downstream, _encoding, callback) =>\n this.send(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 ) {\n this.#lastDownstreamMsgTime = Date.now();\n return send(this.#lc, this.#ws, data, callback);\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, 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) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(\n JSON.stringify(data),\n callback === 'ignore-backpressure' ? undefined : callback,\n );\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\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","#downstreamMsgTimer","#handleClose","#handleError","#proxyInbound","#maybeSendPong","#closeWithError","#closed","#viewSyncerOutboundStream","#pusherOutboundStream","#handleMessage","#handleMessageResult","#closeWithThrown","#proxyOutbound","#lastDownstreamMsgTime"],"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 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 {\n ProtocolErrorWithLevel,\n getLogLevel,\n wrapWithProtocolError,\n} from '../types/error-with-level.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 type: 'stream';\n source: 'viewSyncer' | '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;\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 #downstreamMsgTimer: NodeJS.Timeout | undefined;\n\n #viewSyncerOutboundStream: Source<Downstream> | 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.#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 this.#proxyOutbound(result.stream);\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 this.close('WebSocket close event', {code, reason, wasClean});\n };\n\n #handleError = (e: ErrorEvent) => {\n this.#lc.warn?.('WebSocket error event', e.message, e.error);\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(outboundStream: Source<Downstream>) {\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: Downstream, _encoding, callback) =>\n this.send(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 ) {\n this.#lastDownstreamMsgTime = Date.now();\n return send(this.#lc, this.#ws, data, callback);\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, 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) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(\n JSON.stringify(data),\n callback === 'ignore-backpressure' ? undefined : callback,\n );\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\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutator.js","names":["#stopped"],"sources":["../../../../../zero-cache/src/workers/mutator.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport {resolver} from '@rocicorp/resolver';\nimport type {SingletonService} from '../services/service.ts';\n\n// TODO:\n// - install websocket receiver\n// - spin up pusher services for each unique client group that connects\nexport class Mutator implements SingletonService {\n readonly id = `mutator-${pid}`;\n readonly #stopped;\n\n constructor() {\n this.#stopped = resolver();\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n\n drain(): Promise<void> {\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n"],"mappings":";;;AAOA,IAAa,UAAb,MAAiD;CAC/C,KAAc,WAAW;CACzB;CAEA,cAAc;
|
|
1
|
+
{"version":3,"file":"mutator.js","names":["#stopped"],"sources":["../../../../../zero-cache/src/workers/mutator.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport {resolver} from '@rocicorp/resolver';\nimport type {SingletonService} from '../services/service.ts';\n\n// TODO:\n// - install websocket receiver\n// - spin up pusher services for each unique client group that connects\nexport class Mutator implements SingletonService {\n readonly id = `mutator-${pid}`;\n readonly #stopped;\n\n constructor() {\n this.#stopped = resolver();\n }\n\n run(): Promise<void> {\n return this.#stopped.promise;\n }\n\n stop(): Promise<void> {\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n\n drain(): Promise<void> {\n this.#stopped.resolve();\n return this.#stopped.promise;\n }\n}\n"],"mappings":";;;AAOA,IAAa,UAAb,MAAiD;CAC/C,KAAc,WAAW;CACzB;CAEA,cAAc;EACZ,KAAKA,WAAW,SAAS;CAC3B;CAEA,MAAqB;EACnB,OAAO,KAAKA,SAAS;CACvB;CAEA,OAAsB;EACpB,KAAKA,SAAS,QAAQ;EACtB,OAAO,KAAKA,SAAS;CACvB;CAEA,QAAuB;EACrB,KAAKA,SAAS,QAAQ;EACtB,OAAO,KAAKA,SAAS;CACvB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/workers/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {ReplicaOptions} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {Notifier} from '../services/replicator/notifier.ts';\nimport type {\n ReplicaState,\n ReplicaStateNotifier,\n Replicator,\n} from '../services/replicator/replicator.ts';\nimport {\n getAscendingEvents,\n recordEvent,\n} from '../services/replicator/schema/replication-state.ts';\nimport {\n applyPragmas,\n type PragmaConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport type {Worker} from '../types/processes.ts';\n\nexport const replicaFileModeSchema = v.literalUnion(\n 'serving',\n 'serving-copy',\n 'backup',\n);\n\nexport type ReplicaFileMode = v.Infer<typeof replicaFileModeSchema>;\n\nexport type WalMode = 'wal' | 'wal2';\n\nexport function replicaFileName(replicaFile: string, mode: ReplicaFileMode) {\n return mode === 'serving-copy' ? `${replicaFile}-serving-copy` : replicaFile;\n}\n\nconst MILLIS_PER_HOUR = 1000 * 60 * 60;\nconst MB = 1024 * 1024;\n\nasync function prepare(\n lc: LogContext,\n {file, vacuumIntervalHours}: ReplicaOptions,\n walMode: WalMode,\n mode: ReplicaFileMode,\n): Promise<{file: string; walMode: WalMode}> {\n const replica = new Database(lc, file);\n\n // Perform any upgrades to the replica in case the backup is an\n // earlier version.\n await upgradeReplica(lc, `${mode}-replica`, file);\n\n // Start by folding any (e.g. restored) WAL(2) files into the main db.\n await setJournalMode(lc, replica, 'delete');\n\n const [{page_size: pageSize}] = replica.pragma<{page_size: number}>(\n 'page_size',\n );\n const [{page_count: pageCount}] = replica.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{freelist_count: freelistCount}] = replica.pragma<{\n freelist_count: number;\n }>('freelist_count');\n\n const dbSize = ((pageCount * pageSize) / MB).toFixed(2);\n const freelistSize = ((freelistCount * pageSize) / MB).toFixed(2);\n\n // TODO: Consider adding a freelist size or ratio based vacuum trigger.\n lc.info?.(`Size of db ${file}: ${dbSize} MB (${freelistSize} MB freeable)`);\n\n // Check for the VACUUM threshold.\n const events = getAscendingEvents(replica);\n lc.debug?.(`Runtime events for db ${file}`, {events});\n if (vacuumIntervalHours !== undefined) {\n const millisSinceLastEvent =\n Date.now() - (events.at(-1)?.timestamp.getTime() ?? 0);\n if (millisSinceLastEvent / MILLIS_PER_HOUR > vacuumIntervalHours) {\n lc.info?.(`Performing maintenance cleanup on ${file}`);\n const t0 = performance.now();\n replica.unsafeMode(true);\n replica.pragma('journal_mode = OFF');\n replica.exec('VACUUM');\n recordEvent(replica, 'vacuum');\n replica.unsafeMode(false);\n const t1 = performance.now();\n lc.info?.(`VACUUM completed (${t1 - t0} ms)`);\n }\n }\n\n await setJournalMode(lc, replica, walMode);\n\n const pragmas = getPragmaConfig(mode);\n applyPragmas(replica, pragmas);\n\n replica.pragma('optimize = 0x10002');\n lc.info?.(`optimized ${file}`);\n replica.close();\n return {file, walMode};\n}\n\n// Setting the journal_mode requires an exclusive lock on the replica.\n// Add resilience against random replica reads (for stats, etc.) by\n// retrying if the database is locked. Note that the busy_timeout doesn't\n// work here.\nasync function setJournalMode(\n lc: LogContext,\n replica: Database,\n mode: 'delete' | 'wal' | 'wal2',\n) {\n lc.info?.(`setting ${replica.name} to ${mode} mode`);\n let err: unknown;\n for (let i = 0; i < 5; i++) {\n try {\n replica.pragma(`journal_mode = ${mode}`);\n return;\n } catch (e) {\n lc.warn?.(`error setting journal_mode to ${mode} (attempt ${i + 1})`, e);\n err = e;\n }\n await sleep(500);\n }\n throw err;\n}\n\n/**\n * Returns the PragmaConfig for a given replica file mode.\n * This is used by both the main thread (setupReplica) and\n * the write worker thread to apply the same pragma settings.\n */\nexport function getPragmaConfig(mode: ReplicaFileMode): PragmaConfig {\n return {\n busyTimeout: 30000,\n analysisLimit: 1000,\n walAutocheckpoint: mode === 'backup' ? 0 : undefined,\n };\n}\n\nexport function setupReplica(\n lc: LogContext,\n mode: ReplicaFileMode,\n replicaOptions: ReplicaOptions,\n) {\n lc.info?.(`setting up ${mode} replica`);\n\n switch (mode) {\n case 'backup':\n return prepare(lc, replicaOptions, 'wal', mode);\n\n case 'serving-copy': {\n // In 'serving-copy' mode, the original file is being used for 'backup'\n // mode, so we make a copy for servicing sync requests.\n const {file} = replicaOptions;\n const copyLocation = replicaFileName(file, mode);\n deleteLiteDB(copyLocation);\n\n const start = Date.now();\n lc.info?.(`copying ${file} to ${copyLocation}`);\n const replica = new Database(lc, file);\n replica.prepare(`VACUUM INTO ?`).run(copyLocation);\n replica.close();\n lc.info?.(`finished copy (${Date.now() - start} ms)`);\n\n return prepare(lc, {...replicaOptions, file: copyLocation}, 'wal2', mode);\n }\n\n case 'serving':\n return prepare(lc, replicaOptions, 'wal2', mode);\n\n default:\n throw new Error(`Invalid ReplicaMode ${mode}`);\n }\n}\n\nexport function setUpMessageHandlers(\n lc: LogContext,\n replicator: Replicator,\n parent: Worker,\n) {\n handleSubscriptionsFrom(lc, parent, replicator);\n}\n\ntype Notification = ['notify', ReplicaState];\n\nexport function handleSubscriptionsFrom(\n lc: LogContext,\n subscriber: Worker,\n notifier: ReplicaStateNotifier,\n) {\n subscriber.onMessageType('subscribe', async () => {\n const subscription = notifier.subscribe();\n\n subscriber.on('close', () => {\n lc.debug?.(`closing replication subscription from ${subscriber.pid}`);\n subscription.cancel();\n });\n\n for await (const msg of subscription) {\n try {\n subscriber.send<Notification>(['notify', msg]);\n } catch (e) {\n const log =\n e instanceof Error &&\n 'code' in e &&\n // This can happen in a race condition if the subscribing process\n // is closed before the 'close' message is processed.\n e.code === 'ERR_IPC_CHANNEL_CLOSED'\n ? 'warn'\n : 'error';\n\n lc[log]?.(\n `error sending replicator notification to ${subscriber.pid}: ${String(e)}`,\n e,\n );\n }\n }\n });\n}\n\n/**\n * Creates a Notifier to relay notifications the notifier of another Worker.\n * This does not send the initial subscription message. Use {@link subscribeTo}\n * to initiate the subscription.\n */\nexport function createNotifierFrom(_lc: LogContext, source: Worker): Notifier {\n const notifier = new Notifier();\n source.onMessageType<Notification>('notify', msg =>\n notifier.notifySubscribers(msg),\n );\n return notifier;\n}\n\nexport function subscribeTo(_lc: LogContext, source: Worker) {\n source.send(['subscribe', {}]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/workers/replicator.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport {Database} from '../../../zqlite/src/db.ts';\nimport type {ReplicaOptions} from '../config/zero-config.ts';\nimport {deleteLiteDB} from '../db/delete-lite-db.ts';\nimport {upgradeReplica} from '../services/change-source/common/replica-schema.ts';\nimport {Notifier} from '../services/replicator/notifier.ts';\nimport type {\n ReplicaState,\n ReplicaStateNotifier,\n Replicator,\n} from '../services/replicator/replicator.ts';\nimport {\n getAscendingEvents,\n recordEvent,\n} from '../services/replicator/schema/replication-state.ts';\nimport {\n applyPragmas,\n type PragmaConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport type {Worker} from '../types/processes.ts';\n\nexport const replicaFileModeSchema = v.literalUnion(\n 'serving',\n 'serving-copy',\n 'backup',\n);\n\nexport type ReplicaFileMode = v.Infer<typeof replicaFileModeSchema>;\n\nexport type WalMode = 'wal' | 'wal2';\n\nexport function replicaFileName(replicaFile: string, mode: ReplicaFileMode) {\n return mode === 'serving-copy' ? `${replicaFile}-serving-copy` : replicaFile;\n}\n\nconst MILLIS_PER_HOUR = 1000 * 60 * 60;\nconst MB = 1024 * 1024;\n\nasync function prepare(\n lc: LogContext,\n {file, vacuumIntervalHours}: ReplicaOptions,\n walMode: WalMode,\n mode: ReplicaFileMode,\n): Promise<{file: string; walMode: WalMode}> {\n const replica = new Database(lc, file);\n\n // Perform any upgrades to the replica in case the backup is an\n // earlier version.\n await upgradeReplica(lc, `${mode}-replica`, file);\n\n // Start by folding any (e.g. restored) WAL(2) files into the main db.\n await setJournalMode(lc, replica, 'delete');\n\n const [{page_size: pageSize}] = replica.pragma<{page_size: number}>(\n 'page_size',\n );\n const [{page_count: pageCount}] = replica.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{freelist_count: freelistCount}] = replica.pragma<{\n freelist_count: number;\n }>('freelist_count');\n\n const dbSize = ((pageCount * pageSize) / MB).toFixed(2);\n const freelistSize = ((freelistCount * pageSize) / MB).toFixed(2);\n\n // TODO: Consider adding a freelist size or ratio based vacuum trigger.\n lc.info?.(`Size of db ${file}: ${dbSize} MB (${freelistSize} MB freeable)`);\n\n // Check for the VACUUM threshold.\n const events = getAscendingEvents(replica);\n lc.debug?.(`Runtime events for db ${file}`, {events});\n if (vacuumIntervalHours !== undefined) {\n const millisSinceLastEvent =\n Date.now() - (events.at(-1)?.timestamp.getTime() ?? 0);\n if (millisSinceLastEvent / MILLIS_PER_HOUR > vacuumIntervalHours) {\n lc.info?.(`Performing maintenance cleanup on ${file}`);\n const t0 = performance.now();\n replica.unsafeMode(true);\n replica.pragma('journal_mode = OFF');\n replica.exec('VACUUM');\n recordEvent(replica, 'vacuum');\n replica.unsafeMode(false);\n const t1 = performance.now();\n lc.info?.(`VACUUM completed (${t1 - t0} ms)`);\n }\n }\n\n await setJournalMode(lc, replica, walMode);\n\n const pragmas = getPragmaConfig(mode);\n applyPragmas(replica, pragmas);\n\n replica.pragma('optimize = 0x10002');\n lc.info?.(`optimized ${file}`);\n replica.close();\n return {file, walMode};\n}\n\n// Setting the journal_mode requires an exclusive lock on the replica.\n// Add resilience against random replica reads (for stats, etc.) by\n// retrying if the database is locked. Note that the busy_timeout doesn't\n// work here.\nasync function setJournalMode(\n lc: LogContext,\n replica: Database,\n mode: 'delete' | 'wal' | 'wal2',\n) {\n lc.info?.(`setting ${replica.name} to ${mode} mode`);\n let err: unknown;\n for (let i = 0; i < 5; i++) {\n try {\n replica.pragma(`journal_mode = ${mode}`);\n return;\n } catch (e) {\n lc.warn?.(`error setting journal_mode to ${mode} (attempt ${i + 1})`, e);\n err = e;\n }\n await sleep(500);\n }\n throw err;\n}\n\n/**\n * Returns the PragmaConfig for a given replica file mode.\n * This is used by both the main thread (setupReplica) and\n * the write worker thread to apply the same pragma settings.\n */\nexport function getPragmaConfig(mode: ReplicaFileMode): PragmaConfig {\n return {\n busyTimeout: 30000,\n analysisLimit: 1000,\n walAutocheckpoint: mode === 'backup' ? 0 : undefined,\n };\n}\n\nexport function setupReplica(\n lc: LogContext,\n mode: ReplicaFileMode,\n replicaOptions: ReplicaOptions,\n) {\n lc.info?.(`setting up ${mode} replica`);\n\n switch (mode) {\n case 'backup':\n return prepare(lc, replicaOptions, 'wal', mode);\n\n case 'serving-copy': {\n // In 'serving-copy' mode, the original file is being used for 'backup'\n // mode, so we make a copy for servicing sync requests.\n const {file} = replicaOptions;\n const copyLocation = replicaFileName(file, mode);\n deleteLiteDB(copyLocation);\n\n const start = Date.now();\n lc.info?.(`copying ${file} to ${copyLocation}`);\n const replica = new Database(lc, file);\n replica.prepare(`VACUUM INTO ?`).run(copyLocation);\n replica.close();\n lc.info?.(`finished copy (${Date.now() - start} ms)`);\n\n return prepare(lc, {...replicaOptions, file: copyLocation}, 'wal2', mode);\n }\n\n case 'serving':\n return prepare(lc, replicaOptions, 'wal2', mode);\n\n default:\n throw new Error(`Invalid ReplicaMode ${mode}`);\n }\n}\n\nexport function setUpMessageHandlers(\n lc: LogContext,\n replicator: Replicator,\n parent: Worker,\n) {\n handleSubscriptionsFrom(lc, parent, replicator);\n}\n\ntype Notification = ['notify', ReplicaState];\n\nexport function handleSubscriptionsFrom(\n lc: LogContext,\n subscriber: Worker,\n notifier: ReplicaStateNotifier,\n) {\n subscriber.onMessageType('subscribe', async () => {\n const subscription = notifier.subscribe();\n\n subscriber.on('close', () => {\n lc.debug?.(`closing replication subscription from ${subscriber.pid}`);\n subscription.cancel();\n });\n\n for await (const msg of subscription) {\n try {\n subscriber.send<Notification>(['notify', msg]);\n } catch (e) {\n const log =\n e instanceof Error &&\n 'code' in e &&\n // This can happen in a race condition if the subscribing process\n // is closed before the 'close' message is processed.\n e.code === 'ERR_IPC_CHANNEL_CLOSED'\n ? 'warn'\n : 'error';\n\n lc[log]?.(\n `error sending replicator notification to ${subscriber.pid}: ${String(e)}`,\n e,\n );\n }\n }\n });\n}\n\n/**\n * Creates a Notifier to relay notifications the notifier of another Worker.\n * This does not send the initial subscription message. Use {@link subscribeTo}\n * to initiate the subscription.\n */\nexport function createNotifierFrom(_lc: LogContext, source: Worker): Notifier {\n const notifier = new Notifier();\n source.onMessageType<Notification>('notify', msg =>\n notifier.notifySubscribers(msg),\n );\n return notifier;\n}\n\nexport function subscribeTo(_lc: LogContext, source: Worker) {\n source.send(['subscribe', {}]);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer-ws-message-handler.js","names":["#viewSyncer","#mutagen","#mutationLock","#lc","#clientGroupID","#connectionSelector","#connContextManager","#pusher"],"sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation, trace} from '@opentelemetry/api';\nimport {Lock} from '@rocicorp/lock';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan, startSpan} from '../../../otel/src/span.ts';\nimport {version} from '../../../otel/src/version.ts';\nimport {assert, unreachable} from '../../../shared/src/asserts.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 type {Upstream} from '../../../zero-protocol/src/up.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport {\n type ConnectionContextManager,\n type ConnectionSelector,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport {type ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport type {HandlerResult, MessageHandler} from './connection.ts';\n\nconst tracer = trace.getTracer('syncer-ws-server', version);\n\n/**\n * Wraps a function in an OTEL context extracted from a W3C traceparent header.\n * This enables distributed tracing from the client through zero-cache to\n * the user's API server.\n */\nfunction withTraceparent<T>(traceparent: string | undefined, fn: () => T): T {\n if (!traceparent) {\n return fn();\n }\n const extracted = propagation.extract(ROOT_CONTEXT, {traceparent});\n return context.with(extracted, fn);\n}\n\nexport class SyncerWsMessageHandler implements MessageHandler {\n readonly #viewSyncer: ViewSyncer;\n readonly #mutagen: Mutagen | undefined;\n readonly #mutationLock: Lock;\n readonly #lc: LogContext;\n readonly #clientGroupID: string;\n readonly #connectionSelector: ConnectionSelector;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: Pusher | undefined;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n connContextManager: ConnectionContextManager,\n viewSyncer: ViewSyncer,\n mutagen: Mutagen | undefined,\n pusher: Pusher | undefined,\n ) {\n const {clientGroupID, clientID, wsID} = connectParams;\n this.#viewSyncer = viewSyncer;\n this.#mutagen = mutagen;\n this.#connContextManager = connContextManager;\n this.#mutationLock = new Lock();\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#clientGroupID = clientGroupID;\n this.#pusher = pusher;\n this.#connectionSelector = {\n clientID,\n wsID,\n };\n }\n\n async handleMessage(msg: Upstream): Promise<HandlerResult[]> {\n const lc = this.#lc;\n const msgType = msg[0];\n const viewSyncer = this.#viewSyncer;\n switch (msgType) {\n case 'ping':\n lc.error?.('Ping is not supported at this layer by Zero');\n break;\n case 'pull':\n lc.error?.('Pull is not supported by Zero');\n break;\n case 'push': {\n return withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan<HandlerResult[]>(\n tracer,\n 'connection.push',\n async () => {\n const {clientGroupID, mutations} = msg[1];\n if (clientGroupID !== this.#clientGroupID) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n `clientGroupID in mutation \"${clientGroupID}\" does not match ` +\n `clientGroupID of connection \"${this.#clientGroupID}`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n if (mutations.length === 0) {\n return [\n {\n type: 'ok',\n },\n ];\n }\n\n // The client only ever sends 1 mutation per push.\n // #pusher will throw if it sees a CRUD mutation.\n // #mutagen will throw if it see a custom mutation.\n if (mutations[0].type === 'custom') {\n if (!this.#pusher) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n 'A ZERO_MUTATE_URL must be set in order to process custom mutations.',\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n return [\n this.#pusher.enqueuePush(this.#connectionSelector, msg[1]),\n ];\n }\n\n const mutagen = this.#mutagen;\n if (!mutagen) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message: `Support for legacy CRUD mutations is disabled`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n const auth = this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n ).auth;\n assert(\n auth?.type !== 'opaque',\n 'Only JWT auth is supported for CRUD mutations',\n );\n\n // Hold a connection-level lock while processing mutations so that:\n // 1. Mutations are processed in the order in which they are received and\n // 2. A single view syncer connection cannot hog multiple upstream connections.\n const ret = await this.#mutationLock.withLock(async () => {\n const errors: ErrorBody[] = [];\n for (const mutation of mutations) {\n const maybeError = await mutagen.processMutation(\n mutation,\n auth?.decoded,\n this.#pusher !== undefined,\n );\n if (maybeError !== undefined) {\n errors.push({\n kind: maybeError[0],\n message: maybeError[1],\n origin: ErrorOrigin.ZeroCache,\n });\n }\n }\n if (errors.length > 0) {\n return {type: 'transient', errors} satisfies HandlerResult;\n }\n return {type: 'ok'} satisfies HandlerResult;\n });\n return [ret];\n },\n ),\n );\n }\n case 'changeDesiredQueries':\n await withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan(tracer, 'connection.changeDesiredQueries', () =>\n viewSyncer.changeDesiredQueries(this.#connectionSelector, msg),\n ),\n );\n break;\n case 'updateAuth':\n await startAsyncSpan(tracer, 'connection.updateAuth', async () => {\n const initialConnCtx =\n this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n );\n const updatedConnCtx = await this.#connContextManager.updateAuth(\n this.#connectionSelector,\n msg[1],\n );\n const authRevisionChanged =\n updatedConnCtx.revision !== initialConnCtx.revision;\n\n await viewSyncer.updateAuth(\n this.#connectionSelector,\n msg,\n authRevisionChanged,\n );\n });\n break;\n case 'deleteClients': {\n const deletedClientIDs = await startAsyncSpan(\n tracer,\n 'connection.deleteClients',\n () => viewSyncer.deleteClients(this.#connectionSelector, msg),\n );\n if (this.#pusher && deletedClientIDs.length > 0) {\n await this.#pusher.deleteClientMutations(\n this.#connectionSelector,\n deletedClientIDs,\n );\n }\n break;\n }\n case 'initConnection': {\n this.#connContextManager.initConnection(\n this.#connectionSelector,\n msg[1],\n );\n return withTraceparent(msg[1].traceparent, () => {\n const ret: HandlerResult[] = [\n {\n type: 'stream',\n source: 'viewSyncer',\n stream: startSpan(tracer, 'connection.initConnection', () =>\n viewSyncer.initConnection(this.#connectionSelector, msg),\n ),\n },\n ];\n\n // Given we support both CRUD and Custom mutators,\n // we do not initialize the `pusher` unless the user has opted\n // into custom mutations. We detect that by checking\n // if the pushURL has been set.\n if (this.#pusher) {\n ret.push({\n type: 'stream',\n source: 'pusher',\n stream: this.#pusher.initConnection(this.#connectionSelector),\n });\n }\n\n return ret;\n });\n }\n case 'closeConnection':\n // This message is deprecated and no longer used.\n break;\n\n case 'inspect':\n await startAsyncSpan(tracer, 'connection.inspect', () =>\n viewSyncer.inspect(this.#connectionSelector, msg),\n );\n break;\n\n case 'ackMutationResponses':\n if (this.#pusher) {\n await this.#pusher.ackMutationResponses(\n this.#connectionSelector,\n msg[1],\n );\n }\n break;\n\n default:\n unreachable(msgType);\n }\n\n return [{type: 'ok'}];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncer-ws-message-handler.js","names":["#viewSyncer","#mutagen","#mutationLock","#lc","#clientGroupID","#connectionSelector","#connContextManager","#pusher"],"sources":["../../../../../zero-cache/src/workers/syncer-ws-message-handler.ts"],"sourcesContent":["import {ROOT_CONTEXT, context, propagation, trace} from '@opentelemetry/api';\nimport {Lock} from '@rocicorp/lock';\nimport type {LogContext} from '@rocicorp/logger';\nimport {startAsyncSpan, startSpan} from '../../../otel/src/span.ts';\nimport {version} from '../../../otel/src/version.ts';\nimport {assert, unreachable} from '../../../shared/src/asserts.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 type {Upstream} from '../../../zero-protocol/src/up.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport {\n type ConnectionContextManager,\n type ConnectionSelector,\n} from '../services/view-syncer/connection-context-manager.ts';\nimport {type ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport type {HandlerResult, MessageHandler} from './connection.ts';\n\nconst tracer = trace.getTracer('syncer-ws-server', version);\n\n/**\n * Wraps a function in an OTEL context extracted from a W3C traceparent header.\n * This enables distributed tracing from the client through zero-cache to\n * the user's API server.\n */\nfunction withTraceparent<T>(traceparent: string | undefined, fn: () => T): T {\n if (!traceparent) {\n return fn();\n }\n const extracted = propagation.extract(ROOT_CONTEXT, {traceparent});\n return context.with(extracted, fn);\n}\n\nexport class SyncerWsMessageHandler implements MessageHandler {\n readonly #viewSyncer: ViewSyncer;\n readonly #mutagen: Mutagen | undefined;\n readonly #mutationLock: Lock;\n readonly #lc: LogContext;\n readonly #clientGroupID: string;\n readonly #connectionSelector: ConnectionSelector;\n readonly #connContextManager: ConnectionContextManager;\n readonly #pusher: Pusher | undefined;\n\n constructor(\n lc: LogContext,\n connectParams: ConnectParams,\n connContextManager: ConnectionContextManager,\n viewSyncer: ViewSyncer,\n mutagen: Mutagen | undefined,\n pusher: Pusher | undefined,\n ) {\n const {clientGroupID, clientID, wsID} = connectParams;\n this.#viewSyncer = viewSyncer;\n this.#mutagen = mutagen;\n this.#connContextManager = connContextManager;\n this.#mutationLock = new Lock();\n this.#lc = lc\n .withContext('connection')\n .withContext('clientID', clientID)\n .withContext('clientGroupID', clientGroupID)\n .withContext('wsID', wsID);\n this.#clientGroupID = clientGroupID;\n this.#pusher = pusher;\n this.#connectionSelector = {\n clientID,\n wsID,\n };\n }\n\n async handleMessage(msg: Upstream): Promise<HandlerResult[]> {\n const lc = this.#lc;\n const msgType = msg[0];\n const viewSyncer = this.#viewSyncer;\n switch (msgType) {\n case 'ping':\n lc.error?.('Ping is not supported at this layer by Zero');\n break;\n case 'pull':\n lc.error?.('Pull is not supported by Zero');\n break;\n case 'push': {\n return withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan<HandlerResult[]>(\n tracer,\n 'connection.push',\n async () => {\n const {clientGroupID, mutations} = msg[1];\n if (clientGroupID !== this.#clientGroupID) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n `clientGroupID in mutation \"${clientGroupID}\" does not match ` +\n `clientGroupID of connection \"${this.#clientGroupID}`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n if (mutations.length === 0) {\n return [\n {\n type: 'ok',\n },\n ];\n }\n\n // The client only ever sends 1 mutation per push.\n // #pusher will throw if it sees a CRUD mutation.\n // #mutagen will throw if it see a custom mutation.\n if (mutations[0].type === 'custom') {\n if (!this.#pusher) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message:\n 'A ZERO_MUTATE_URL must be set in order to process custom mutations.',\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n return [\n this.#pusher.enqueuePush(this.#connectionSelector, msg[1]),\n ];\n }\n\n const mutagen = this.#mutagen;\n if (!mutagen) {\n return [\n {\n type: 'fatal',\n error: {\n kind: ErrorKind.InvalidPush,\n message: `Support for legacy CRUD mutations is disabled`,\n origin: ErrorOrigin.ZeroCache,\n },\n } satisfies HandlerResult,\n ];\n }\n\n const auth = this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n ).auth;\n assert(\n auth?.type !== 'opaque',\n 'Only JWT auth is supported for CRUD mutations',\n );\n\n // Hold a connection-level lock while processing mutations so that:\n // 1. Mutations are processed in the order in which they are received and\n // 2. A single view syncer connection cannot hog multiple upstream connections.\n const ret = await this.#mutationLock.withLock(async () => {\n const errors: ErrorBody[] = [];\n for (const mutation of mutations) {\n const maybeError = await mutagen.processMutation(\n mutation,\n auth?.decoded,\n this.#pusher !== undefined,\n );\n if (maybeError !== undefined) {\n errors.push({\n kind: maybeError[0],\n message: maybeError[1],\n origin: ErrorOrigin.ZeroCache,\n });\n }\n }\n if (errors.length > 0) {\n return {type: 'transient', errors} satisfies HandlerResult;\n }\n return {type: 'ok'} satisfies HandlerResult;\n });\n return [ret];\n },\n ),\n );\n }\n case 'changeDesiredQueries':\n await withTraceparent(msg[1].traceparent, () =>\n startAsyncSpan(tracer, 'connection.changeDesiredQueries', () =>\n viewSyncer.changeDesiredQueries(this.#connectionSelector, msg),\n ),\n );\n break;\n case 'updateAuth':\n await startAsyncSpan(tracer, 'connection.updateAuth', async () => {\n const initialConnCtx =\n this.#connContextManager.mustGetConnectionContext(\n this.#connectionSelector,\n );\n const updatedConnCtx = await this.#connContextManager.updateAuth(\n this.#connectionSelector,\n msg[1],\n );\n const authRevisionChanged =\n updatedConnCtx.revision !== initialConnCtx.revision;\n\n await viewSyncer.updateAuth(\n this.#connectionSelector,\n msg,\n authRevisionChanged,\n );\n });\n break;\n case 'deleteClients': {\n const deletedClientIDs = await startAsyncSpan(\n tracer,\n 'connection.deleteClients',\n () => viewSyncer.deleteClients(this.#connectionSelector, msg),\n );\n if (this.#pusher && deletedClientIDs.length > 0) {\n await this.#pusher.deleteClientMutations(\n this.#connectionSelector,\n deletedClientIDs,\n );\n }\n break;\n }\n case 'initConnection': {\n this.#connContextManager.initConnection(\n this.#connectionSelector,\n msg[1],\n );\n return withTraceparent(msg[1].traceparent, () => {\n const ret: HandlerResult[] = [\n {\n type: 'stream',\n source: 'viewSyncer',\n stream: startSpan(tracer, 'connection.initConnection', () =>\n viewSyncer.initConnection(this.#connectionSelector, msg),\n ),\n },\n ];\n\n // Given we support both CRUD and Custom mutators,\n // we do not initialize the `pusher` unless the user has opted\n // into custom mutations. We detect that by checking\n // if the pushURL has been set.\n if (this.#pusher) {\n ret.push({\n type: 'stream',\n source: 'pusher',\n stream: this.#pusher.initConnection(this.#connectionSelector),\n });\n }\n\n return ret;\n });\n }\n case 'closeConnection':\n // This message is deprecated and no longer used.\n break;\n\n case 'inspect':\n await startAsyncSpan(tracer, 'connection.inspect', () =>\n viewSyncer.inspect(this.#connectionSelector, msg),\n );\n break;\n\n case 'ackMutationResponses':\n if (this.#pusher) {\n await this.#pusher.ackMutationResponses(\n this.#connectionSelector,\n msg[1],\n );\n }\n break;\n\n default:\n unreachable(msgType);\n }\n\n return [{type: 'ok'}];\n }\n}\n"],"mappings":"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"}
|