@rocicorp/zero 1.3.0 → 1.4.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/analyze-query/src/analyze-cli.d.ts +24 -0
- package/out/analyze-query/src/analyze-cli.d.ts.map +1 -0
- package/out/analyze-query/src/analyze-cli.js +289 -0
- package/out/analyze-query/src/analyze-cli.js.map +1 -0
- package/out/analyze-query/src/bin-analyze.js +6 -6
- package/out/analyze-query/src/bin-transform.js +2 -2
- package/out/ast-to-zql/src/bin.js +1 -1
- package/out/shared/src/logging.d.ts.map +1 -1
- package/out/shared/src/logging.js +1 -1
- package/out/shared/src/logging.js.map +1 -1
- package/out/shared/src/options.d.ts.map +1 -1
- package/out/shared/src/options.js +1 -1
- package/out/shared/src/options.js.map +1 -1
- package/out/z2s/src/compiler.d.ts.map +1 -1
- package/out/z2s/src/compiler.js +4 -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 +1 -0
- package/out/z2s/src/sql.js.map +1 -1
- package/out/zero/package.js +95 -89
- package/out/zero/package.js.map +1 -1
- package/out/zero/src/analyze.d.ts +2 -0
- package/out/zero/src/analyze.d.ts.map +1 -0
- package/out/zero/src/analyze.js +2 -0
- package/out/zero/src/bindings.js +1 -1
- package/out/zero/src/zero-cache-dev.js +1 -1
- package/out/zero/src/zero-cache-dev.js.map +1 -1
- package/out/zero/src/zero-out.js +1 -1
- package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
- package/out/zero-cache/src/auth/auth.js.map +1 -1
- package/out/zero-cache/src/auth/load-permissions.js +2 -2
- package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
- package/out/zero-cache/src/auth/write-authorizer.js +5 -14
- package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
- package/out/zero-cache/src/config/network.d.ts +1 -1
- package/out/zero-cache/src/config/network.d.ts.map +1 -1
- package/out/zero-cache/src/config/network.js +1 -1
- package/out/zero-cache/src/config/network.js.map +1 -1
- package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
- package/out/zero-cache/src/config/normalize.js.map +1 -1
- package/out/zero-cache/src/config/zero-config.d.ts +5 -0
- package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
- package/out/zero-cache/src/config/zero-config.js +16 -3
- package/out/zero-cache/src/config/zero-config.js.map +1 -1
- package/out/zero-cache/src/db/lite-tables.d.ts.map +1 -1
- package/out/zero-cache/src/db/lite-tables.js +3 -3
- package/out/zero-cache/src/db/lite-tables.js.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.d.ts +43 -40
- package/out/zero-cache/src/db/transaction-pool.d.ts.map +1 -1
- package/out/zero-cache/src/db/transaction-pool.js +76 -56
- package/out/zero-cache/src/db/transaction-pool.js.map +1 -1
- package/out/zero-cache/src/observability/events.d.ts.map +1 -1
- package/out/zero-cache/src/observability/events.js +1 -1
- package/out/zero-cache/src/observability/events.js.map +1 -1
- package/out/zero-cache/src/scripts/decommission.js +1 -1
- package/out/zero-cache/src/scripts/deploy-permissions.js +2 -2
- package/out/zero-cache/src/scripts/permissions.js +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/anonymous-otel-start.js +4 -4
- package/out/zero-cache/src/server/anonymous-otel-start.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +27 -12
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/server/logging.d.ts +1 -3
- package/out/zero-cache/src/server/logging.d.ts.map +1 -1
- package/out/zero-cache/src/server/logging.js +6 -3
- package/out/zero-cache/src/server/logging.js.map +1 -1
- package/out/zero-cache/src/server/main.d.ts.map +1 -1
- package/out/zero-cache/src/server/main.js +26 -26
- package/out/zero-cache/src/server/main.js.map +1 -1
- package/out/zero-cache/src/server/mutator.js +4 -2
- package/out/zero-cache/src/server/mutator.js.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-log-sink.js +0 -2
- package/out/zero-cache/src/server/otel-log-sink.js.map +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts +1 -1
- package/out/zero-cache/src/server/otel-start.d.ts.map +1 -1
- package/out/zero-cache/src/server/otel-start.js +7 -3
- package/out/zero-cache/src/server/otel-start.js.map +1 -1
- package/out/zero-cache/src/server/reaper.js +6 -6
- package/out/zero-cache/src/server/reaper.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +5 -3
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/server/runner/run-worker.js +2 -2
- package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
- package/out/zero-cache/src/server/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/server/syncer.js +13 -12
- package/out/zero-cache/src/server/syncer.js.map +1 -1
- package/out/zero-cache/src/server/worker-dispatcher.js +1 -1
- package/out/zero-cache/src/services/analyze.js +1 -1
- package/out/zero-cache/src/services/change-source/common/backfill-manager.js +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-schema.js +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/backfill-stream.js +4 -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.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source.js +19 -23
- package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +58 -3
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +209 -52
- 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/stream.js +2 -2
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +24 -15
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +35 -58
- 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.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/init.js +2 -2
- 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.d.ts +1 -2
- package/out/zero-cache/src/services/change-source/pg/schema/published.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/schema/published.js +15 -18
- 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 +1 -1
- package/out/zero-cache/src/services/change-source/protocol/current/data.js +1 -1
- package/out/zero-cache/src/services/change-streamer/backup-monitor.js +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +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.d.ts +5 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +10 -7
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/replica-monitor.js +2 -2
- package/out/zero-cache/src/services/change-streamer/storer.d.ts +19 -2
- package/out/zero-cache/src/services/change-streamer/storer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/storer.js +70 -6
- package/out/zero-cache/src/services/change-streamer/storer.js.map +1 -1
- package/out/zero-cache/src/services/heapz.d.ts.map +1 -1
- package/out/zero-cache/src/services/heapz.js +1 -1
- package/out/zero-cache/src/services/heapz.js.map +1 -1
- package/out/zero-cache/src/services/life-cycle.d.ts +2 -1
- package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
- package/out/zero-cache/src/services/life-cycle.js +10 -7
- package/out/zero-cache/src/services/life-cycle.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +15 -4
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +40 -34
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/mutagen/mutagen.js +3 -3
- package/out/zero-cache/src/services/mutagen/pusher.d.ts +28 -28
- package/out/zero-cache/src/services/replicator/change-processor.js +2 -2
- package/out/zero-cache/src/services/replicator/incremental-sync.js +1 -1
- package/out/zero-cache/src/services/replicator/schema/replication-state.js +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 +3 -3
- 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 +3 -3
- package/out/zero-cache/src/services/run-ast.js.map +1 -1
- package/out/zero-cache/src/services/statz.d.ts.map +1 -1
- package/out/zero-cache/src/services/statz.js +3 -3
- package/out/zero-cache/src/services/statz.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/active-users-gauge.js +1 -1
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +2 -2
- package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.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 +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +3 -3
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +1 -1
- package/out/zero-cache/src/services/view-syncer/inspect-handler.js +2 -2
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +6 -16
- 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 +31 -39
- 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.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js +4 -4
- package/out/zero-cache/src/services/view-syncer/row-record-cache.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +2 -2
- 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 +6 -6
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/profiler.d.ts.map +1 -1
- package/out/zero-cache/src/types/profiler.js.map +1 -1
- package/out/zero-cache/src/types/row-key.d.ts.map +1 -1
- package/out/zero-cache/src/types/row-key.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +1 -1
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.d.ts +1 -1
- package/out/zero-cache/src/types/websocket-handoff.d.ts.map +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js +1 -1
- package/out/zero-cache/src/types/websocket-handoff.js.map +1 -1
- package/out/zero-cache/src/workers/connection.d.ts +1 -1
- package/out/zero-cache/src/workers/connection.d.ts.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/syncer.d.ts +1 -1
- package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
- package/out/zero-cache/src/workers/syncer.js +3 -3
- package/out/zero-cache/src/workers/syncer.js.map +1 -1
- package/out/zero-client/src/client/bindings.js +1 -1
- package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
- package/out/zero-client/src/client/crud-impl.js +4 -13
- package/out/zero-client/src/client/crud-impl.js.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.d.ts +24 -0
- package/out/zero-client/src/client/inspector/inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/inspector.js +28 -0
- package/out/zero-client/src/client/inspector/inspector.js.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts +9 -0
- package/out/zero-client/src/client/inspector/lazy-inspector.d.ts.map +1 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js +28 -1
- package/out/zero-client/src/client/inspector/lazy-inspector.js.map +1 -1
- package/out/zero-client/src/client/ivm-branch.d.ts.map +1 -1
- package/out/zero-client/src/client/ivm-branch.js +4 -13
- package/out/zero-client/src/client/ivm-branch.js.map +1 -1
- package/out/zero-client/src/client/log-options.d.ts +1 -0
- package/out/zero-client/src/client/log-options.d.ts.map +1 -1
- package/out/zero-client/src/client/log-options.js +3 -2
- package/out/zero-client/src/client/log-options.js.map +1 -1
- package/out/zero-client/src/client/options.d.ts +13 -1
- package/out/zero-client/src/client/options.d.ts.map +1 -1
- package/out/zero-client/src/client/options.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +2 -1
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/error.d.ts.map +1 -1
- package/out/zero-protocol/src/error.js +1 -1
- package/out/zero-protocol/src/error.js.map +1 -1
- package/out/zero-react/src/bindings.js +1 -1
- package/out/zero-solid/src/bindings.js +1 -1
- package/out/zero-solid/src/solid-view.d.ts.map +1 -1
- package/out/zero-solid/src/solid-view.js +14 -14
- package/out/zero-solid/src/solid-view.js.map +1 -1
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/array-view.d.ts.map +1 -1
- package/out/zql/src/ivm/array-view.js +27 -2
- package/out/zql/src/ivm/array-view.js.map +1 -1
- package/out/zql/src/ivm/change-index-enum.d.ts +9 -0
- package/out/zql/src/ivm/change-index-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/change-index.d.ts +5 -0
- package/out/zql/src/ivm/change-index.d.ts.map +1 -0
- package/out/zql/src/ivm/change-type-enum.d.ts +9 -0
- package/out/zql/src/ivm/change-type-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/change-type.d.ts +5 -0
- package/out/zql/src/ivm/change-type.d.ts.map +1 -0
- package/out/zql/src/ivm/change.d.ts +20 -22
- package/out/zql/src/ivm/change.d.ts.map +1 -1
- package/out/zql/src/ivm/change.js +33 -0
- package/out/zql/src/ivm/change.js.map +1 -0
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +27 -38
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.d.ts +3 -2
- package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.d.ts +1 -1
- package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-out.js +1 -1
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +3 -3
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter-push.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-push.js +7 -7
- package/out/zql/src/ivm/filter-push.js.map +1 -1
- package/out/zql/src/ivm/filter.d.ts +1 -1
- package/out/zql/src/ivm/filter.d.ts.map +1 -1
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +49 -58
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join-utils.d.ts +2 -6
- package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
- package/out/zql/src/ivm/join-utils.js +25 -25
- package/out/zql/src/ivm/join-utils.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +32 -51
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.d.ts.map +1 -1
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js +5 -10
- package/out/zql/src/ivm/maybe-split-and-push-edit-change.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +52 -60
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +1 -1
- package/out/zql/src/ivm/operator.d.ts.map +1 -1
- package/out/zql/src/ivm/operator.js +2 -4
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.d.ts +3 -2
- package/out/zql/src/ivm/push-accumulated.d.ts.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js +98 -122
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/skip-yields.d.ts +4 -0
- package/out/zql/src/ivm/skip-yields.d.ts.map +1 -0
- package/out/zql/src/ivm/skip-yields.js +33 -0
- package/out/zql/src/ivm/skip-yields.js.map +1 -0
- package/out/zql/src/ivm/skip.d.ts +1 -1
- package/out/zql/src/ivm/skip.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.js +2 -2
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/source-change-index-enum.d.ts +7 -0
- package/out/zql/src/ivm/source-change-index-enum.d.ts.map +1 -0
- package/out/zql/src/ivm/source-change-index.d.ts +5 -0
- package/out/zql/src/ivm/source-change-index.d.ts.map +1 -0
- package/out/zql/src/ivm/source.d.ts +11 -13
- package/out/zql/src/ivm/source.d.ts.map +1 -1
- package/out/zql/src/ivm/source.js +26 -0
- package/out/zql/src/ivm/source.js.map +1 -0
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +27 -50
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +2 -1
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +3 -3
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js +1 -1
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/ivm/view-apply-change.js +1 -1
- package/out/zql/src/planner/planner-debug.d.ts +2 -2
- package/out/zql/src/planner/planner-debug.d.ts.map +1 -1
- package/out/zql/src/planner/planner-debug.js.map +1 -1
- package/out/zql/src/planner/planner-graph.d.ts +1 -1
- package/out/zql/src/planner/planner-graph.d.ts.map +1 -1
- package/out/zql/src/planner/planner-graph.js.map +1 -1
- package/out/zqlite/src/internal/sql-inline.d.ts.map +1 -1
- package/out/zqlite/src/internal/sql-inline.js.map +1 -1
- package/out/zqlite/src/query-builder.d.ts.map +1 -1
- package/out/zqlite/src/query-builder.js.map +1 -1
- package/out/zqlite/src/table-source.d.ts.map +1 -1
- package/out/zqlite/src/table-source.js +11 -11
- package/out/zqlite/src/table-source.js.map +1 -1
- package/package.json +99 -93
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"row-key.js","names":[],"sources":["../../../../../zero-cache/src/types/row-key.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {
|
|
1
|
+
{"version":3,"file":"row-key.js","names":[],"sources":["../../../../../zero-cache/src/types/row-key.ts"],"sourcesContent":["import {assert} from '../../../shared/src/asserts.ts';\nimport {stringify, type JSONValue} from '../../../shared/src/bigint-json.ts';\nimport {h128} from '../../../shared/src/hash.ts';\n\nexport type ColumnType = {readonly typeOid: number};\nexport type RowKeyType = Readonly<Record<string, ColumnType>>;\nexport type RowKey = Readonly<Record<string, JSONValue>>;\n\nexport type RowID = Readonly<{schema: string; table: string; rowKey: RowKey}>;\n\n// Aliased for documentation purposes when dealing with full rows vs row keys.\n// The actual structure of the objects is the same.\nexport type RowType = RowKeyType;\nexport type RowValue = RowKey;\n\n/**\n * Returns the `RowKey` such that key iteration produces a sorted sequence. If the\n * keys are already sorted, the input is returned as is.\n *\n * Note that the value type is parameterized as `V` so that this method can be used\n * for both (pg) RowKeys and LiteRowKeys.\n */\nexport function normalizedKeyOrder<V>(\n rowKey: Readonly<Record<string, V>>,\n): Readonly<Record<string, V>> {\n let last = '';\n let empty = true;\n for (const col in rowKey) {\n empty = false;\n if (last > col) {\n const entries = Object.entries(rowKey).sort(([a], [b]) =>\n a < b ? -1 : a > b ? 1 : 0,\n );\n assert(entries.length > 0, 'empty row key');\n return Object.fromEntries(entries);\n }\n last = col;\n }\n assert(!empty, 'empty row key');\n // This case iterates over columns and avoids object allocations, which is\n // expected to be the common case (e.g. single column key).\n return rowKey;\n}\n\n/**\n * Returns a normalized string suitable for representing a row key in a form\n * that can be used as a Map key.\n */\nexport function rowKeyString(key: RowKey): string {\n return stringify(tuples(key));\n}\n\nfunction tuples(key: RowKey) {\n return Object.entries(normalizedKeyOrder(key)).flat();\n}\n\nconst rowIDStrings = new WeakMap<RowID, string>();\n\n/**\n * A normalized string representation of a {@link RowID} suitable to use\n * as a Map key. Use {@link rowIDHash} if you need string keys of bounded\n * length.\n */\nexport function rowIDString(id: RowID): string {\n let val = rowIDStrings.get(id);\n if (val) {\n return val;\n }\n val = stringify([id.schema, id.table, ...tuples(id.rowKey)]);\n rowIDStrings.set(id, val);\n return val;\n}\n\nconst rowIDHashes = new WeakMap<RowID, string>();\n\n/**\n * A RowIDHash is a 128-bit column-order-agnostic hash of the schema, table name, and\n * column name / value tuples of a row key. It serves as a compact identifier for\n * a row in the database that:\n *\n * * is guaranteed to fit within the constraints of the CVR store (Durable Object\n * storage keys cannot exceed 2KiB)\n * * can be used to compactly encode (and lookup) the rows of query results for CVR\n * bookkeeping.\n *\n * The hash is encoded in `base36`, with the maximum 128-bit value being 25 characters long.\n */\nexport function rowIDHash(id: RowID): string {\n let hash = rowIDHashes.get(id);\n if (hash) {\n return hash;\n }\n\n const str = rowIDString(id);\n hash = h128(str).toString(36);\n rowIDHashes.set(id, hash);\n return hash;\n}\n"],"mappings":";;;;;;;;;;;AAsBA,SAAgB,mBACd,QAC6B;CAC7B,IAAI,OAAO;CACX,IAAI,QAAQ;AACZ,MAAK,MAAM,OAAO,QAAQ;AACxB,UAAQ;AACR,MAAI,OAAO,KAAK;GACd,MAAM,UAAU,OAAO,QAAQ,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OACjD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,EAC1B;AACD,UAAO,QAAQ,SAAS,GAAG,gBAAgB;AAC3C,UAAO,OAAO,YAAY,QAAQ;;AAEpC,SAAO;;AAET,QAAO,CAAC,OAAO,gBAAgB;AAG/B,QAAO;;AAWT,SAAS,OAAO,KAAa;AAC3B,QAAO,OAAO,QAAQ,mBAAmB,IAAI,CAAC,CAAC,MAAM;;AAGvD,IAAM,+BAAe,IAAI,SAAwB;;;;;;AAOjD,SAAgB,YAAY,IAAmB;CAC7C,IAAI,MAAM,aAAa,IAAI,GAAG;AAC9B,KAAI,IACF,QAAO;AAET,OAAM,UAAU;EAAC,GAAG;EAAQ,GAAG;EAAO,GAAG,OAAO,GAAG,OAAO;EAAC,CAAC;AAC5D,cAAa,IAAI,IAAI,IAAI;AACzB,QAAO"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LogContext } from '@rocicorp/logger';
|
|
2
1
|
import { Readable, type DuplexOptions } from 'node:stream';
|
|
2
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
3
3
|
import { type WebSocket } from 'ws';
|
|
4
4
|
import { type JSONValue } from '../../../shared/src/bigint-json.ts';
|
|
5
5
|
import * as v from '../../../shared/src/valita.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/types/streams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/types/streams.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,IAAI,CAAC;AAEZ,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,oCAAoC,CAAC;AAE9E,OAAO,KAAK,CAAC,MAAM,+BAA+B,CAAC;AACnD,OAAO,EAAC,YAAY,EAAE,KAAK,OAAO,EAAC,MAAM,mBAAmB,CAAC;AAW7D,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG;IACzC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAE9B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;KAAC,CAAC,GAAG,SAAS,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI;IACpB,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF;;;GAGG;AAIH,wBAAgB,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,SAAS,EAChE,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EACpB,UAAU,GAAE,OAAO,CAAC,GAAG,CAAM,EAC7B,SAAS,GAAE,OAAO,CAAC,EAAE,CAAM,EAC3B,aAAa,GAAE,aAAkB,GAChC;IAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAA;CAAC,CAwE9C;AAED,KAAK,WAAW,CAAC,CAAC,IAAI;IACpB,MAAM,EAAE,QAAQ,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAC,EAAE,WAAW,CAAC,CAAC,CAAC,QAsD5E;AAkBD,wBAAsB,SAAS,CAAC,CAAC,SAAS,SAAS,EACjD,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,IAAI,EAAE,SAAS,GACd,OAAO,CAAC,IAAI,CAAC,CA0Df;AAED,wBAAsB,QAAQ,CAAC,CAAC,SAAS,SAAS,EAChD,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAChB,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAqCpB"}
|
|
@@ -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 type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {\n pipeline,\n Readable,\n Transform,\n Writable,\n type DuplexOptions,\n} from 'node:stream';\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 async function streamOut<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\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 = BigIntJSON.stringify({msg, id} satisfies Streamed<T>);\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 = BigIntJSON.stringify({msg, id} satisfies Streamed<T>);\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 async function streamOut<T extends JSONValue>(\n lc: LogContext,\n source: Source<T>,\n sink: WebSocket,\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 = BigIntJSON.stringify({msg, id} satisfies Streamed<T>);\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 = BigIntJSON.stringify({msg, id} satisfies Streamed<T>);\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":";;;;;;;;;;AA6BA,IAAM,mBAAmB;;;;;AAqCzB,SAAgB,OACd,IACA,IACA,UACA,aAA2B,EAAE,EAC7B,YAAyB,EAAE,EAC3B,gBAA+B,EAAE,EACa;CAC9C,MAAM,WAAW,GAAG,OAAO;CAC3B,SAAS,MAAM,KAAe;AAC5B,MAAI,GAAG,eAAe,GAAG,UAAU,GAAG,eAAe,GAAG,QACtD,KAAI,IACF,gBAAe,IAAI,IAAI,IAAI;OACtB;AACL,MAAG,OAAO,yBAAyB,WAAW;AAC9C,MAAG,OAAO;;;CAKhB,MAAM,WAAW,aAAa,OAAW;EACvC,GAAG;EACH,UAAU,YAAY,QAAQ;AAC5B,aAAU,UAAU,YAAY,IAAI;AACpC,SAAM,IAAI;;EAEb,CAAC;CACF,MAAM,YAAY,aAAa,OAAY;EACzC,GAAG;EACH,UAAU,YAAY,QAAQ;AAC5B,cAAW,UAAU,YAAY,IAAI;AACrC,SAAM,IAAI;;EAEb,CAAC;CAEF,MAAM,SAAS,sBAAsB,IAAI;EACvC,GAAG;EACH,eAAe;EAChB,CAAC;CAGF,SAAS,YAAY;AAEnB,SAAO,GAAG,eAAe,GAAG,QAAQ,uBAAuB,WAAW,CAAC;AAEvE,WACE,SAAS,KAAK,UAAU,EACxB,IAAI,UAAU;GACZ,YAAY;GACZ,YAAY,KAAK,WAAW,aAC1B,SAAS,MAAM,WAAW,UAAU,IAAI,CAAC;GAC5C,CAAC,EACF,SACA,QAAQ,MAAM,UAAU,KAAK,IAAI,GAAG,UAAU,QAAQ,CACvD;;AAGH,KAAI,GAAG,eAAe,GAAG,WACvB,IAAG,GAAG,cAAc;AAClB,KAAG,OAAO,gBAAgB,WAAW;AACrC,aAAW;GACX;KAEF,YAAW;AAIb,MAAK;EACH,QAAQ;EACR,MAAM;EACN,QAAO,UAAS;AAEd,UAAO,MADM,WAAW,MAAM,MAAM,UAAU,CAAC,EAC1B,UAAU,cAAc;;EAEhD,CAAC;AAEF,sBAAqB,IAAI,IAAI,iBAAiB;AAE9C,QAAO;EAAC;EAAW;EAAS;;AAU9B,SAAgB,KAAQ,EAAC,QAAQ,MAAM,OAAO,kBAAiC;AAC7E,oBAAmB;AACnB,QAAO,kBAAkB,GAAG,sCAAsC;CAClE,MAAM,UAA8B,EAAE;AAEtC,UACE,QACA,IAAI,SAAS;EACX,eAAe;EACf,QAAQ,OAAO,WAAW,aAAa;GACrC,IAAI;AACJ,OAAI;AACF,SAAK,MAAM,MAAM,MAAM,MAAM,MAAM;AACjC,eAAU;AACV;;YAEK,KAAK;AACZ,aAAS,YAAY,IAAI,CAAC;AAC1B;;GAOF,MAAM,EAAC,WAAU,KAAK,KAAK,IAAI;AAC/B,WAAQ,KAAK,OAAO;AACf,UAAO,WAAW,QAAQ,OAAO,CAAC;AAEvC,OAAI,QAAQ,UAAU,eAEpB,WAAU;OAGV,SAAQ,GAAG,WACH,UAAU,GAChB,QAAO,SAAS,YAAY,IAAI,CAAC,CAClC;;EAGL,UAAU,KAAK,aAAa;AAC1B,OAAI,IACF,MAAK,KAAK,YAAY,IAAI,CAAC;AAG7B,aAAU;;EAEZ,QAAO,aAAY;AACjB,QAAK,QAAQ;AACb,aAAU;;EAEb,CAAC,GACF,QAAQ,MAAM,KAAK,KAAK,IAAI,GAAG,KAAK,QAAQ,CAC7C;;AAGH,SAAS,YAAY,KAAc;AACjC,QAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,IAAI,CAAC;;AAG5D,IAAM,YAAY,eAAE,OAAO,EAAC,KAAK,eAAE,QAAQ,EAAC,CAAC;AAY7C,eAAsB,UACpB,IACA,QACA,MACe;AACf,sBAAqB,IAAI,MAAM,iBAAiB;CAEhD,MAAM,SAAS,gBAAgB,UAAU,IAAI,MAAM,OAAO;CAE1D,MAAM,OAAO,IAAI,OAAY;AAC7B,MAAK,iBAAiB,YAAY,EAAC,WAAU;AAC3C,MAAI;AACF,OAAI,OAAO,SAAS,SAClB,OAAM,IAAI,MAAM,0BAA0B;AAE5C,QAAK,QAAQ,MAAQ,KAAK,MAAM,KAAK,EAAE,UAAU,CAAC;WAC3C,GAAG;AACV,MAAG,QAAQ,qBAAqB,EAAE;AAClC,UAAO,MAAM,EAAE;;GAEjB;AAEF,KAAI;EACF,IAAI,SAAS;EACb,MAAM,EAAC,aAAY;AACnB,MAAI,UAAU;AACZ,MAAG,QAAQ,oCAAoC;AAC/C,cAAW,MAAM,EAAC,OAAO,KAAK,cAAa,UAAU;IACnD,MAAM,KAAK,EAAE;IACb,MAAM,OAAO,WAAW,UAAU;KAAC;KAAK;KAAG,CAAuB;AAGlE,SAAK,KAAK,KAAK;AAEf,KAAM,YAAY;KAChB,MAAM,EAAC,QAAO,MAAM,KAAK,SAAS;AAElC,SAAI,QAAQ,GACV,OAAM,IAAI,MAAM,sBAAsB,GAAG,IAAI,MAAM;AAErD,eAAU;QACR;;SAED;AACL,MAAG,QAAQ,sCAAsC;AACjD,cAAW,MAAM,OAAO,QAAQ;IAC9B,MAAM,KAAK,EAAE;IACb,MAAM,OAAO,WAAW,UAAU;KAAC;KAAK;KAAG,CAAuB;AAGlE,SAAK,KAAK,KAAK;IAEf,MAAM,EAAC,QAAO,MAAM,KAAK,SAAS;AAClC,QAAI,QAAQ,GACV,OAAM,IAAI,MAAM,sBAAsB,GAAG,IAAI,MAAM;;;AAIzD,SAAO,OAAO;UACP,GAAG;AACV,SAAO,MAAM,EAAE;;;AAInB,eAAsB,SACpB,IACA,QACA,QACoB;AACpB,wBAAuB,IAAI,QAAQ,iBAAiB;CAEpD,MAAM,iBAAiB,eAAE,OAAO;EAC9B,KAAK;EACL,IAAI,eAAE,QAAQ;EACf,CAAC;CAEF,MAAM,OAAqC,IAAI,aAC7C;EACE,WAAW,EAAC,SAAQ,OAAO,KAAK,KAAK,UAAU,EAAC,KAAK,IAAG,CAAe,CAAC;EACxE,eAAe,OAAO,OAAO;EAC9B,GACA,EAAC,UAAS,IACZ;CAED,MAAM,SAAS,gBAAgB,QAAQ,IAAI,QAAQ,MAAM,cAAc;CAEvE,SAAS,cAAc,OAAqB;EAC1C,MAAM,OAAO,MAAM,KAAK,UAAU;AAClC,MAAI,CAAC,KAAK,QAAQ;AAChB,MAAG,OAAO,4CAA4C,KAAK;AAC3D;;AAEF,MAAI;GAEF,MAAM,MAAM,MADE,WAAW,MAAM,KAAK,EACT,gBAAgB,cAAc;AAGzD,QAAK,KAAK,IAAI;WACP,GAAG;AACV,UAAO,MAAM,EAAE;;;AAInB,OAAM,OAAO;AACb,QAAO;;AAGT,IAAM,kBAAN,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA,aAAsB,UAAU;CAEhC,IAAI,YAA2B;AAC7B,SAAO,MAAA,UAAgB;;CAGzB,OAAO,UAAa,IAAgB,IAAe,QAAmB;AAGpE,SAAO,IAAI,gBAAgB,IAAI,UAAU,OAAO,QAAQ,CAAC;;CAG3D,OAAO,QACL,IACA,IACA,QACA,gBACA;AAGA,SAAO,IAAI,gBAAgB,IAAI,UAAU,OAAO,KAAK,EAAE,eAAe;;CAGxE,YACE,IACA,IACA,aACA,gBACA;AACA,QAAA,KAAW;AACX,QAAA,KAAW;AACX,QAAA,cAAoB;AACpB,QAAA,iBAAuB,kBAAkB;AAEzC,KAAG,iBAAiB,QAAQ,MAAA,WAAiB;AAC7C,KAAG,iBAAiB,SAAS,MAAA,YAAkB;AAC/C,KAAG,iBAAiB,SAAS,MAAA,YAAkB;AAC/C,MAAI,MAAA,eACF,IAAG,iBAAiB,WAAW,MAAA,eAAqB;AAGtD,UAAQ,GAAG,YAAX;GACE,KAAK,GAAG,WACN;GACF,KAAK,GAAG;AACN,UAAA,UAAgB,SAAS;AACzB;GACF;AACE,UAAA,UAAgB,uBACd,IAAI,MAAM,8BAA8B,GAAG,aAAa,CACzD;AACD;;;CAIN,KAAA,OAAoB;AAClB,SAAO,gBAAgB,MAAA,GAAS,MAAM,OAAO,MAAA,GAAS,QAAQ;;CAGhE,oBAAoB;AAClB,QAAA,GAAS,OAAO,GAAG,MAAA,KAAW,cAAc;AAC5C,QAAA,UAAgB,SAAS;;CAG3B,gBAAgB,MAAkB;EAChC,MAAM,EAAC,MAAM,QAAQ,aAAY;AACjC,QAAA,GAAS,OAAO,GAAG,MAAA,KAAW,UAAU;GACtC;GACA;GACA;GACD,CAAC;AACF,OAAK,OAAO;AACZ,QAAA,UAAgB,OAAO,GAAG,MAAA,KAAW,oBAAoB,OAAO;;CAGlE,gBAAgB,EAAC,SAAS,YAAuB;AAC/C,MAAI,MAAA,GAAS,eAAe,MAAA,GAAS,KACnC,OAAA,GAAS,QAAQ,YAAY,MAAA,QAAc,SAAS,MAAM;AAE5D,QAAA,UAAgB,OAAO,MAAM;;CAG/B,MAAM,KAAe;AACnB,MAAI,IACF,OAAA,GAAS,QAAQ,6BAA6B,IAAI;AAEpD,QAAA,aAAmB;AACnB,MAAI,CAAC,KAAK,QAAQ,CAChB,OAAA,GAAS,OAAO;;CAIpB,SAAS;AACP,SACE,MAAA,GAAS,eAAe,MAAA,GAAS,UACjC,MAAA,GAAS,eAAe,MAAA,GAAS"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { LogContext } from '@rocicorp/logger';
|
|
2
1
|
import { Server } from 'node:http';
|
|
3
2
|
import type { Socket } from 'node:net';
|
|
3
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
4
4
|
import { WebSocketServer, type ServerOptions, type WebSocket } from 'ws';
|
|
5
5
|
import { type IncomingMessageSubset } from './http.ts';
|
|
6
6
|
import type { MESSAGE_TYPES } from './processes.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-handoff.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/types/websocket-handoff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"websocket-handoff.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/types/websocket-handoff.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,MAAM,EAAC,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAC,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAC,MAAM,IAAI,CAAC;AAEvE,OAAO,EAAqB,KAAK,qBAAqB,EAAC,MAAM,WAAW,CAAC;AACzE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAC,MAAM,gBAAgB,CAAC;AAGvE,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,CAChC,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,EACrC,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,KAC/B,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;AAE3B,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,CACjC,EAAE,EAAE,SAAS,EACb,OAAO,EAAE,CAAC,EACV,GAAG,EAAE,qBAAqB,KACvB,IAAI,CAAC;AAEV,MAAM,MAAM,uBAAuB,GAAG,CACpC,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,KACd,IAAI,CAAC;AAEV;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,EAC7C,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,aAAa,CAAC,EAAE,aAAa,GAC5B,uBAAuB,CAoDzB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,aAAa,CAAC,EAAE,aAAa,QAc9B;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAC7B,QAAQ,EAAE,QAAQ,QA6BnB;AAED,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI;IACvB,OAAO,aAAa,CAAC,OAAO;IAC5B;QACE,OAAO,EAAE,qBAAqB,CAAC;QAC/B,IAAI,EAAE,WAAW,CAAC;QAClB,OAAO,EAAE,CAAC,CAAC;KACZ;CACF,CAAC"}
|
|
@@ -2,8 +2,8 @@ import { assert } from "../../../shared/src/asserts.js";
|
|
|
2
2
|
import "./processes.js";
|
|
3
3
|
import { serializableSubset } from "./http.js";
|
|
4
4
|
import { PROTOCOL_ERROR, closeWithError } from "./ws.js";
|
|
5
|
-
import { Server } from "node:http";
|
|
6
5
|
import { WebSocketServer } from "ws";
|
|
6
|
+
import { Server } from "node:http";
|
|
7
7
|
//#region ../zero-cache/src/types/websocket-handoff.ts
|
|
8
8
|
/**
|
|
9
9
|
* Installs websocket handoff logic from either an http.Server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-handoff.js","names":[],"sources":["../../../../../zero-cache/src/types/websocket-handoff.ts"],"sourcesContent":["import type {
|
|
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,MACX,CACF;AACD,SACE,SACA,QACA,SACG;EACH,IAAI,OAAO;EAEX,SAAS,KAAK,EAAC,SAAS,UAAyB;AAC/C,UAAO,CAAC,MAAM,mCAAmC;AACjD,UAAO;GAEP,MAAM,OAAO,CACX,WACA;IACE,SAAS,mBAAmB,QAAQ;IACpC;IACA;IACD,CACF;AAID,UAAO,KAAK,MAAM,OAAO;;EAG3B,SAAS,QAAQ,OAAgB;AAI/B,OAAI,cACF,SACA,QACA,OAAO,KAAK,KAAK,GACjB,OAAM,eAAe,IAAI,IAAI,OAAO,eAAe,CACpD;;AAGH,MAAI;GACF,MAAM,OAAO,QAAQ,SAAS,MAAM,QAAQ;AAC5C,OAAI,KACF,MAAK,KAAK;WAEL,OAAO;AACd,WAAQ,MAAM;;;;;;;;;AAUpB,SAAgB,wBACd,IACA,SACA,QACA,eACA;CACA,MAAM,SAAS,8BAA8B,IAAI,SAAS,cAAc;AAExE,KAAI,kBAAkB,OAEpB,QAAO,GAAG,WAAW,OAAO;KAG5B,QAAO,cAA0B,YAAY,KAAK,WAAW;EAC3D,MAAM,EAAC,SAAS,SAAQ;AACxB,SAAO,SAAS,QAAkB,KAAK;GACvC;;AAIN,SAAgB,yBACd,IACA,QACA,SACA,UACA;AACA,UAAS,cAA0B,YAAY,KAAK,WAAW;AAM7D,MAAI,CAAC,QAAQ;AACX,MAAG,OAAO,kCAAkC;AAC5C;;EAEF,MAAM,EAAC,SAAS,MAAM,YAAW;AACjC,SAAO,cACL,SACA,QACA,OAAO,KAAK,KAAK,GACjB,OAAM;AAIJ,OAAI,GAAG,eAAe,GAAG,UAAU,GAAG,eAAe,GAAG,SAAS;AAC/D,OAAG,OAAO,oDAAoD;AAC9D;;AAEF,WAAQ,IAAI,SAAS,QAAQ;IAEhC;GACD"}
|
|
@@ -2,10 +2,10 @@ import type { LogContext } from '@rocicorp/logger';
|
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
3
|
import type { Downstream } from '../../../zero-protocol/src/down.ts';
|
|
4
4
|
import type { ErrorBody } from '../../../zero-protocol/src/error.ts';
|
|
5
|
+
import { type ProtocolError } from '../../../zero-protocol/src/error.ts';
|
|
5
6
|
import { type Upstream } from '../../../zero-protocol/src/up.ts';
|
|
6
7
|
import type { Source } from '../types/streams.ts';
|
|
7
8
|
import type { ConnectParams } from './connect-params.ts';
|
|
8
|
-
import { type ProtocolError } from '../../../zero-protocol/src/error.ts';
|
|
9
9
|
export type HandlerResult = {
|
|
10
10
|
type: 'ok';
|
|
11
11
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/connection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/connection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,UAAU,EAAW,MAAM,kBAAkB,CAAC;AAE3D,OAAO,SAAkC,MAAM,IAAI,CAAC;AAIpD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oCAAoC,CAAC;AAGnE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qCAAqC,CAAC;AACnE,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;AAK7C,OAAO,EAAiB,KAAK,QAAQ,EAAC,MAAM,kCAAkC,CAAC;AAM/E,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,IAAI,CAAC;CACZ,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;CAClB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB,GACD,YAAY,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CACxD;AAeD;;;;;;;GAOG;AACH,qBAAa,UAAU;;gBAcnB,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,aAAa,EAC5B,EAAE,EAAE,SAAS,EACb,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,MAAM,IAAI;IA2BrB;;;;;;OAMG;IACH,IAAI,IAAI,OAAO;IAyBf,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE;IAsBxC,oBAAoB,CAAC,iBAAiB,EAAE,MAAM;IAmJ9C,IAAI,CACF,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,qBAAqB;IAMlE,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO;CAGjD;AAED,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG;IAC1D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;CACtD,CAAC;AAGF,wBAAgB,IAAI,CAClB,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,aAAa,EACjB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,qBAAqB,QAwBjE;AAED,wBAAgB,SAAS,CACvB,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,SAAS,EACpB,MAAM,CAAC,EAAE,OAAO,QA+BjB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAQ3E"}
|
|
@@ -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 type {LogContext, LogLevel} from '@rocicorp/logger';\nimport {pipeline, Readable, Writable} from 'node:stream';\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 type {ErrorBody} 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';\nimport {\n isProtocolError,\n type ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.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 this.#lc.warn?.(`failed to parse message \"${data}\": ${String(e)}`);\n this.#closeWithError(\n {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n },\n e,\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.error?.('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 this.#lc.warn?.(`failed to parse message \"${data}\": ${String(e)}`);\n this.#closeWithError(\n {\n kind: ErrorKind.InvalidMessage,\n message: String(e),\n origin: ErrorOrigin.ZeroCache,\n },\n e,\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.error?.('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 {resolver} from '@rocicorp/resolver';\nimport type {SingletonService} from '../services/service.ts';\
|
|
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;AACZ,QAAA,UAAgB,UAAU;;CAG5B,MAAqB;AACnB,SAAO,MAAA,QAAc;;CAGvB,OAAsB;AACpB,QAAA,QAAc,SAAS;AACvB,SAAO,MAAA,QAAc;;CAGvB,QAAuB;AACrB,QAAA,QAAc,SAAS;AACvB,SAAO,MAAA,QAAc"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LogContext } from '@rocicorp/logger';
|
|
2
1
|
import type { MessagePort } from 'node:worker_threads';
|
|
2
|
+
import type { LogContext } from '@rocicorp/logger';
|
|
3
3
|
import { type ValidateLegacyJWT } from '../auth/auth.ts';
|
|
4
4
|
import { type ZeroConfig } from '../config/zero-config.ts';
|
|
5
5
|
import type { Mutagen } from '../services/mutagen/mutagen.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/workers/syncer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAUjD,OAAO,EAAyB,KAAK,iBAAiB,EAAC,MAAM,iBAAiB,CAAC;AAE/E,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAC;AAOzD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,gCAAgC,CAAC;AAC5D,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,+BAA+B,CAAC;AAC1D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sCAAsC,CAAC;AAEvE,OAAO,KAAK,EACV,oBAAoB,EACpB,OAAO,EACP,gBAAgB,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,wBAAwB,EAAC,MAAM,uDAAuD,CAAC;AACpG,OAAO,EAAC,gBAAgB,EAAC,MAAM,8CAA8C,CAAC;AAC9E,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AACvE,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAO3D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,WAAW,CAAC;CAC7B,CAAC;AA4BF;;;;;;GAMG;AACH,qBAAa,MAAO,YAAW,gBAAgB;;IAC7C,QAAQ,CAAC,EAAE,SAAmB;gBAc5B,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,iBAAiB,EAAE,CACjB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,YAAY,CAAC,YAAY,CAAC,EAC/B,gBAAgB,EAAE,gBAAgB,KAC/B,UAAU,GAAG,oBAAoB,EACtC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,GAAG,SAAS,EAC/D,aAAa,EACT,CAAC,CACC,EAAE,EAAE,MAAM,EACV,cAAc,EAAE,wBAAwB,KACrC,MAAM,GAAG,OAAO,CAAC,GACtB,SAAS,EACb,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,iBAAiB,GAAG,SAAS;IAsOlD,GAAG;IAIH;;;;;OAKG;IACG,KAAK;IAqBX,IAAI;CAKL"}
|
|
@@ -3,19 +3,19 @@ import { Unauthorized } from "../../../zero-protocol/src/error-kind-enum.js";
|
|
|
3
3
|
import { ZeroCache } from "../../../zero-protocol/src/error-origin-enum.js";
|
|
4
4
|
import { ProtocolError, isProtocolError } from "../../../zero-protocol/src/error.js";
|
|
5
5
|
import "../config/zero-config.js";
|
|
6
|
-
import { installWebSocketReceiver } from "../types/websocket-handoff.js";
|
|
7
6
|
import { getOrCreateGauge } from "../observability/metrics.js";
|
|
7
|
+
import { installWebSocketReceiver } from "../types/websocket-handoff.js";
|
|
8
8
|
import { createNotifierFrom, subscribeTo } from "./replicator.js";
|
|
9
9
|
import { recordConnectionAttempted, recordConnectionSuccess, setActiveClientGroupsGetter } from "../server/anonymous-otel-start.js";
|
|
10
|
-
import { resolveAuth } from "../auth/auth.js";
|
|
11
10
|
import { tokenConfigOptions } from "../auth/jwt.js";
|
|
11
|
+
import { resolveAuth } from "../auth/auth.js";
|
|
12
12
|
import { ServiceRunner } from "../services/runner.js";
|
|
13
13
|
import { DrainCoordinator } from "../services/view-syncer/drain-coordinator.js";
|
|
14
14
|
import { Connection, sendError } from "./connection.js";
|
|
15
15
|
import { SyncerWsMessageHandler } from "./syncer-ws-message-handler.js";
|
|
16
16
|
import { resolver } from "@rocicorp/resolver";
|
|
17
|
-
import { pid } from "node:process";
|
|
18
17
|
import { WebSocketServer } from "ws";
|
|
18
|
+
import { pid } from "node:process";
|
|
19
19
|
//#region ../zero-cache/src/workers/syncer.ts
|
|
20
20
|
function getWebSocketServerOptions(config) {
|
|
21
21
|
const options = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n contextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent);\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(id, this.#viewSyncers.getService(id).contextManager),\n p => p.hasRefs(),\n );\n }\n this.#parent = parent;\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#createConnection,\n this.#parent,\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n userID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n userID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n throw e;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const contextManager = viewSyncer.contextManager;\n const group = contextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (group.validated && group.userID !== userID) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n contextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n contextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n contextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n contextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n connection.init() && recordConnectionSuccess();\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"syncer.js","names":["#lc","#viewSyncers","#mutagens","#pushers","#connections","#drainCoordinator","#parent","#wss","#stopped","#config","#validateLegacyJWT","#createConnection"],"sources":["../../../../../zero-cache/src/workers/syncer.ts"],"sourcesContent":["import {pid} from 'node:process';\nimport type {MessagePort} from 'node:worker_threads';\nimport type {LogContext} from '@rocicorp/logger';\nimport {resolver} from '@rocicorp/resolver';\nimport {WebSocketServer, type ServerOptions, type WebSocket} from 'ws';\nimport {promiseVoid} from '../../../shared/src/resolved-promises.ts';\nimport {ErrorKind} from '../../../zero-protocol/src/error-kind.ts';\nimport {ErrorOrigin} from '../../../zero-protocol/src/error-origin.ts';\nimport {\n isProtocolError,\n ProtocolError,\n} from '../../../zero-protocol/src/error.ts';\nimport {resolveAuth, type Auth, type ValidateLegacyJWT} from '../auth/auth.ts';\nimport {tokenConfigOptions} from '../auth/jwt.ts';\nimport {type ZeroConfig} from '../config/zero-config.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n recordConnectionAttempted,\n recordConnectionSuccess,\n setActiveClientGroupsGetter,\n} from '../server/anonymous-otel-start.ts';\nimport type {Mutagen} from '../services/mutagen/mutagen.ts';\nimport type {Pusher} from '../services/mutagen/pusher.ts';\nimport type {ReplicaState} from '../services/replicator/replicator.ts';\nimport {ServiceRunner} from '../services/runner.ts';\nimport type {\n ActivityBasedService,\n Service,\n SingletonService,\n} from '../services/service.ts';\nimport type {ConnectionContextManager} from '../services/view-syncer/connection-context-manager.ts';\nimport {DrainCoordinator} from '../services/view-syncer/drain-coordinator.ts';\nimport type {ViewSyncer} from '../services/view-syncer/view-syncer.ts';\nimport type {Worker} from '../types/processes.ts';\nimport type {Subscription} from '../types/subscription.ts';\nimport {installWebSocketReceiver} from '../types/websocket-handoff.ts';\nimport type {ConnectParams} from './connect-params.ts';\nimport {Connection, sendError} from './connection.ts';\nimport {createNotifierFrom, subscribeTo} from './replicator.ts';\nimport {SyncerWsMessageHandler} from './syncer-ws-message-handler.ts';\n\nexport type SyncerWorkerData = {\n replicatorPort: MessagePort;\n};\n\nfunction getWebSocketServerOptions(config: ZeroConfig): ServerOptions {\n const options: ServerOptions = {\n noServer: true,\n maxPayload: config.websocketMaxPayloadBytes,\n };\n\n if (config.websocketCompression) {\n options.perMessageDeflate = true;\n\n if (config.websocketCompressionOptions) {\n try {\n const compressionOptions = JSON.parse(\n config.websocketCompressionOptions,\n );\n options.perMessageDeflate = compressionOptions;\n } catch (e) {\n throw new Error(\n `Failed to parse ZERO_WEBSOCKET_COMPRESSION_OPTIONS: ${String(e)}. Expected valid JSON.`,\n );\n }\n }\n }\n\n return options;\n}\n\n/**\n * The Syncer worker receives websocket handoffs for \"/sync\" connections\n * from the Dispatcher in the main thread, and creates websocket\n * {@link Connection}s with a corresponding {@link ViewSyncer}, {@link Mutagen},\n * and {@link Subscription} to version notifications from the Replicator\n * worker.\n */\nexport class Syncer implements SingletonService {\n readonly id = `syncer-${pid}`;\n readonly #lc: LogContext;\n readonly #viewSyncers: ServiceRunner<ViewSyncer & ActivityBasedService>;\n readonly #mutagens: ServiceRunner<Mutagen & Service> | undefined;\n readonly #pushers: ServiceRunner<Pusher & Service> | undefined;\n readonly #connections = new Map<string, Connection>();\n readonly #drainCoordinator = new DrainCoordinator();\n readonly #parent: Worker;\n readonly #wss: WebSocketServer;\n readonly #stopped = resolver();\n readonly #config: ZeroConfig;\n readonly #validateLegacyJWT: ValidateLegacyJWT | undefined;\n\n constructor(\n lc: LogContext,\n config: ZeroConfig,\n viewSyncerFactory: (\n id: string,\n sub: Subscription<ReplicaState>,\n drainCoordinator: DrainCoordinator,\n ) => ViewSyncer & ActivityBasedService,\n mutagenFactory: ((id: string) => Mutagen & Service) | undefined,\n pusherFactory:\n | ((\n id: string,\n contextManager: ConnectionContextManager,\n ) => Pusher & Service)\n | undefined,\n parent: Worker,\n validateLegacyJWT: ValidateLegacyJWT | undefined,\n ) {\n this.#config = config;\n this.#validateLegacyJWT = validateLegacyJWT;\n // Relays notifications from the parent thread subscription\n // to ViewSyncers within this thread.\n const notifier = createNotifierFrom(lc, parent);\n subscribeTo(lc, parent);\n\n this.#lc = lc;\n this.#viewSyncers = new ServiceRunner(\n lc,\n id => viewSyncerFactory(id, notifier.subscribe(), this.#drainCoordinator),\n v => v.keepalive(),\n );\n if (mutagenFactory) {\n this.#mutagens = new ServiceRunner(lc, mutagenFactory, m => m.hasRefs());\n }\n if (pusherFactory) {\n this.#pushers = new ServiceRunner(\n lc,\n id =>\n pusherFactory(id, this.#viewSyncers.getService(id).contextManager),\n p => p.hasRefs(),\n );\n }\n this.#parent = parent;\n this.#wss = new WebSocketServer(getWebSocketServerOptions(config));\n\n installWebSocketReceiver(\n lc,\n this.#wss,\n this.#createConnection,\n this.#parent,\n );\n\n setActiveClientGroupsGetter(() => this.#viewSyncers.size);\n\n getOrCreateGauge(\n 'sync',\n 'active-client-groups',\n 'Number of active client groups',\n ).addCallback(result => result.observe(this.#viewSyncers.size));\n\n getOrCreateGauge(\n 'sync',\n 'queries',\n 'Active queries (pipelines) across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.queryCount;\n }\n result.observe(total);\n });\n\n getOrCreateGauge(\n 'sync',\n 'rows',\n 'Tracked rows across all client groups',\n ).addCallback(result => {\n let total = 0;\n for (const vs of this.#viewSyncers.getServices()) {\n total += vs.rowCount;\n }\n result.observe(total);\n });\n }\n\n readonly #createConnection = async (ws: WebSocket, params: ConnectParams) => {\n this.#lc.debug?.(\n 'creating connection',\n params.clientGroupID,\n params.clientID,\n );\n recordConnectionAttempted();\n const {clientID, clientGroupID, auth, userID} = params;\n const hasProvidedAuth = auth !== undefined && auth !== '';\n\n if (hasProvidedAuth) {\n const tokenOptions = tokenConfigOptions(this.#config.auth ?? {});\n\n const hasPushOrMutate =\n this.#config?.push?.url !== undefined ||\n this.#config?.mutate?.url !== undefined;\n const hasQueries =\n this.#config?.query?.url !== undefined ||\n this.#config?.getQueries?.url !== undefined;\n\n // must either have one of the token options set or have custom mutations & queries enabled\n const hasExactlyOneTokenOption = tokenOptions.length === 1;\n const hasCustomEndpoints = hasPushOrMutate && hasQueries;\n if (!hasExactlyOneTokenOption && !hasCustomEndpoints) {\n throw new Error(\n 'Exactly one of jwk, secret, or jwksUrl must be set in order to verify tokens but actually the following were set: ' +\n JSON.stringify(tokenOptions) +\n '. You may also set both ZERO_MUTATE_URL and ZERO_QUERY_URL to enable custom mutations and queries without passing token verification options.',\n );\n }\n }\n\n let initialAuth: Auth | undefined;\n\n // Verify JWT BEFORE touching existing connections - prevents unauthenticated\n // attackers from force-disconnecting legitimate users via DoS.\n try {\n initialAuth = await resolveAuth(\n this.#lc\n .withContext('clientGroupID', clientGroupID)\n .withContext('clientID', clientID),\n // no previous auth, since this is a new connection, and resolveAuth is\n // connection scoped, not client group scoped\n undefined,\n userID,\n auth,\n this.#validateLegacyJWT,\n );\n } catch (e) {\n if (isProtocolError(e)) {\n this.#lc.warn?.(\n 'Rejecting sync connection during initial auth resolution',\n {\n clientGroupID,\n clientID,\n userID,\n hasProvidedAuth,\n errorKind: e.message,\n },\n );\n sendError(this.#lc, ws, e.errorBody);\n ws.close(3000, e.errorBody.message);\n return;\n }\n throw e;\n }\n\n const viewSyncer = this.#viewSyncers.getService(clientGroupID);\n const contextManager = viewSyncer.contextManager;\n const group = contextManager.getGroupState();\n\n // TODO(0xcadams): we only check for user ID mismatch here if the group is\n // already validated. This prevents wrong-user reconnects from evicting a\n // healthy connection, but it does not protect against same-user reconnects\n // with an invalid opaque token. The long-term fix is to keep the replacement\n // connection pending until its auth is fully validated, and only then replace\n // the existing socket.\n if (group.validated && group.userID !== userID) {\n const error = new ProtocolError({\n kind: ErrorKind.Unauthorized,\n message:\n 'Client groups are pinned to a single userID. Connection userID does not match existing client group userID.',\n origin: ErrorOrigin.ZeroCache,\n });\n sendError(this.#lc, ws, error.errorBody);\n ws.close(3000, error.message);\n return;\n }\n\n // Check for and close existing connections AFTER auth is validated\n const existing = this.#connections.get(clientID);\n if (existing) {\n this.#lc.debug?.(\n `client ${clientID} already connected, closing existing connection`,\n );\n existing.close(`replaced by ${params.wsID}`);\n }\n\n contextManager.registerConnection(\n {clientID, wsID: params.wsID},\n params,\n initialAuth,\n );\n\n const mutagen = this.#mutagens?.getService(clientGroupID);\n const pusher = this.#pushers?.getService(clientGroupID);\n // a new connection is using the mutagen and pusher. Bump their ref counts.\n mutagen?.ref();\n pusher?.ref();\n\n let connection: Connection;\n try {\n connection = new Connection(\n this.#lc,\n params,\n ws,\n new SyncerWsMessageHandler(\n this.#lc,\n params,\n contextManager,\n viewSyncer,\n mutagen,\n pusher,\n ),\n () => {\n contextManager.closeConnection({\n clientID,\n wsID: params.wsID,\n });\n if (this.#connections.get(clientID) === connection) {\n this.#connections.delete(clientID);\n }\n // Connection is closed. We can unref the mutagen and pusher.\n // If their ref counts are zero, they will stop themselves and set themselves invalid.\n mutagen?.unref();\n pusher?.unref();\n },\n );\n } catch (e) {\n contextManager.closeConnection({clientID, wsID: params.wsID});\n mutagen?.unref();\n pusher?.unref();\n throw e;\n }\n\n this.#connections.set(clientID, connection);\n\n connection.init() && recordConnectionSuccess();\n\n if (params.initConnectionMsg) {\n this.#lc.debug?.(\n 'handling init connection message from sec header',\n params.clientGroupID,\n params.clientID,\n );\n await connection.handleInitConnection(\n JSON.stringify(params.initConnectionMsg),\n );\n }\n };\n\n run() {\n return this.#stopped.promise;\n }\n\n /**\n * Graceful shutdown involves shutting down view syncers one at a time, pausing\n * for the duration of view syncer's hydration between each one. This paces the\n * disconnects to avoid creating a backlog of hydrations in the receiving server\n * when the clients reconnect.\n */\n async drain() {\n const start = Date.now();\n this.#lc.info?.(`draining ${this.#viewSyncers.size} view-syncers`);\n\n this.#drainCoordinator.drainNextIn(0);\n\n while (this.#viewSyncers.size) {\n await this.#drainCoordinator.forceDrainTimeout;\n\n // Pick an arbitrary view syncer to force drain.\n for (const vs of this.#viewSyncers.getServices()) {\n this.#lc.debug?.(`draining view-syncer ${vs.id} (forced)`);\n // When this drain or an elective drain completes, the forceDrainTimeout will\n // resolve after the next drain interval.\n void vs.stop();\n break;\n }\n }\n this.#lc.info?.(`finished draining (${Date.now() - start} ms)`);\n }\n\n stop() {\n this.#wss.close();\n this.#stopped.resolve();\n return promiseVoid;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6CA,SAAS,0BAA0B,QAAmC;CACpE,MAAM,UAAyB;EAC7B,UAAU;EACV,YAAY,OAAO;EACpB;AAED,KAAI,OAAO,sBAAsB;AAC/B,UAAQ,oBAAoB;AAE5B,MAAI,OAAO,4BACT,KAAI;AAIF,WAAQ,oBAHmB,KAAK,MAC9B,OAAO,4BACR;WAEM,GAAG;AACV,SAAM,IAAI,MACR,uDAAuD,OAAO,EAAE,CAAC,wBAClE;;;AAKP,QAAO;;;;;;;;;AAUT,IAAa,SAAb,MAAgD;CAC9C,KAAc,UAAU;CACxB;CACA;CACA;CACA;CACA,+BAAwB,IAAI,KAAyB;CACrD,oBAA6B,IAAI,kBAAkB;CACnD;CACA;CACA,WAAoB,UAAU;CAC9B;CACA;CAEA,YACE,IACA,QACA,mBAKA,gBACA,eAMA,QACA,mBACA;AACA,QAAA,SAAe;AACf,QAAA,oBAA0B;EAG1B,MAAM,WAAW,mBAAmB,IAAI,OAAO;AAC/C,cAAY,IAAI,OAAO;AAEvB,QAAA,KAAW;AACX,QAAA,cAAoB,IAAI,cACtB,KACA,OAAM,kBAAkB,IAAI,SAAS,WAAW,EAAE,MAAA,iBAAuB,GACzE,MAAK,EAAE,WAAW,CACnB;AACD,MAAI,eACF,OAAA,WAAiB,IAAI,cAAc,IAAI,iBAAgB,MAAK,EAAE,SAAS,CAAC;AAE1E,MAAI,cACF,OAAA,UAAgB,IAAI,cAClB,KACA,OACE,cAAc,IAAI,MAAA,YAAkB,WAAW,GAAG,CAAC,eAAe,GACpE,MAAK,EAAE,SAAS,CACjB;AAEH,QAAA,SAAe;AACf,QAAA,MAAY,IAAI,gBAAgB,0BAA0B,OAAO,CAAC;AAElE,2BACE,IACA,MAAA,KACA,MAAA,kBACA,MAAA,OACD;AAED,oCAAkC,MAAA,YAAkB,KAAK;AAEzD,mBACE,QACA,wBACA,iCACD,CAAC,aAAY,WAAU,OAAO,QAAQ,MAAA,YAAkB,KAAK,CAAC;AAE/D,mBACE,QACA,WACA,sDACD,CAAC,aAAY,WAAU;GACtB,IAAI,QAAQ;AACZ,QAAK,MAAM,MAAM,MAAA,YAAkB,aAAa,CAC9C,UAAS,GAAG;AAEd,UAAO,QAAQ,MAAM;IACrB;AAEF,mBACE,QACA,QACA,wCACD,CAAC,aAAY,WAAU;GACtB,IAAI,QAAQ;AACZ,QAAK,MAAM,MAAM,MAAA,YAAkB,aAAa,CAC9C,UAAS,GAAG;AAEd,UAAO,QAAQ,MAAM;IACrB;;CAGJ,oBAA6B,OAAO,IAAe,WAA0B;AAC3E,QAAA,GAAS,QACP,uBACA,OAAO,eACP,OAAO,SACR;AACD,6BAA2B;EAC3B,MAAM,EAAC,UAAU,eAAe,MAAM,WAAU;EAChD,MAAM,kBAAkB,SAAS,KAAA,KAAa,SAAS;AAEvD,MAAI,iBAAiB;GACnB,MAAM,eAAe,mBAAmB,MAAA,OAAa,QAAQ,EAAE,CAAC;GAEhE,MAAM,kBACJ,MAAA,QAAc,MAAM,QAAQ,KAAA,KAC5B,MAAA,QAAc,QAAQ,QAAQ,KAAA;GAChC,MAAM,aACJ,MAAA,QAAc,OAAO,QAAQ,KAAA,KAC7B,MAAA,QAAc,YAAY,QAAQ,KAAA;AAKpC,OAAI,EAF6B,aAAa,WAAW,MAExB,EADN,mBAAmB,YAE5C,OAAM,IAAI,MACR,uHACE,KAAK,UAAU,aAAa,GAC5B,gJACH;;EAIL,IAAI;AAIJ,MAAI;AACF,iBAAc,MAAM,YAClB,MAAA,GACG,YAAY,iBAAiB,cAAc,CAC3C,YAAY,YAAY,SAAS,EAGpC,KAAA,GACA,QACA,MACA,MAAA,kBACD;WACM,GAAG;AACV,OAAI,gBAAgB,EAAE,EAAE;AACtB,UAAA,GAAS,OACP,4DACA;KACE;KACA;KACA;KACA;KACA,WAAW,EAAE;KACd,CACF;AACD,cAAU,MAAA,IAAU,IAAI,EAAE,UAAU;AACpC,OAAG,MAAM,KAAM,EAAE,UAAU,QAAQ;AACnC;;AAEF,SAAM;;EAGR,MAAM,aAAa,MAAA,YAAkB,WAAW,cAAc;EAC9D,MAAM,iBAAiB,WAAW;EAClC,MAAM,QAAQ,eAAe,eAAe;AAQ5C,MAAI,MAAM,aAAa,MAAM,WAAW,QAAQ;GAC9C,MAAM,QAAQ,IAAI,cAAc;IAC9B,MAAM;IACN,SACE;IACF,QAAQ;IACT,CAAC;AACF,aAAU,MAAA,IAAU,IAAI,MAAM,UAAU;AACxC,MAAG,MAAM,KAAM,MAAM,QAAQ;AAC7B;;EAIF,MAAM,WAAW,MAAA,YAAkB,IAAI,SAAS;AAChD,MAAI,UAAU;AACZ,SAAA,GAAS,QACP,UAAU,SAAS,iDACpB;AACD,YAAS,MAAM,eAAe,OAAO,OAAO;;AAG9C,iBAAe,mBACb;GAAC;GAAU,MAAM,OAAO;GAAK,EAC7B,QACA,YACD;EAED,MAAM,UAAU,MAAA,UAAgB,WAAW,cAAc;EACzD,MAAM,SAAS,MAAA,SAAe,WAAW,cAAc;AAEvD,WAAS,KAAK;AACd,UAAQ,KAAK;EAEb,IAAI;AACJ,MAAI;AACF,gBAAa,IAAI,WACf,MAAA,IACA,QACA,IACA,IAAI,uBACF,MAAA,IACA,QACA,gBACA,YACA,SACA,OACD,QACK;AACJ,mBAAe,gBAAgB;KAC7B;KACA,MAAM,OAAO;KACd,CAAC;AACF,QAAI,MAAA,YAAkB,IAAI,SAAS,KAAK,WACtC,OAAA,YAAkB,OAAO,SAAS;AAIpC,aAAS,OAAO;AAChB,YAAQ,OAAO;KAElB;WACM,GAAG;AACV,kBAAe,gBAAgB;IAAC;IAAU,MAAM,OAAO;IAAK,CAAC;AAC7D,YAAS,OAAO;AAChB,WAAQ,OAAO;AACf,SAAM;;AAGR,QAAA,YAAkB,IAAI,UAAU,WAAW;AAE3C,aAAW,MAAM,IAAI,yBAAyB;AAE9C,MAAI,OAAO,mBAAmB;AAC5B,SAAA,GAAS,QACP,oDACA,OAAO,eACP,OAAO,SACR;AACD,SAAM,WAAW,qBACf,KAAK,UAAU,OAAO,kBAAkB,CACzC;;;CAIL,MAAM;AACJ,SAAO,MAAA,QAAc;;;;;;;;CASvB,MAAM,QAAQ;EACZ,MAAM,QAAQ,KAAK,KAAK;AACxB,QAAA,GAAS,OAAO,YAAY,MAAA,YAAkB,KAAK,eAAe;AAElE,QAAA,iBAAuB,YAAY,EAAE;AAErC,SAAO,MAAA,YAAkB,MAAM;AAC7B,SAAM,MAAA,iBAAuB;AAG7B,QAAK,MAAM,MAAM,MAAA,YAAkB,aAAa,EAAE;AAChD,UAAA,GAAS,QAAQ,wBAAwB,GAAG,GAAG,WAAW;AAGrD,OAAG,MAAM;AACd;;;AAGJ,QAAA,GAAS,OAAO,sBAAsB,KAAK,KAAK,GAAG,MAAM,MAAM;;CAGjE,OAAO;AACL,QAAA,IAAU,OAAO;AACjB,QAAA,QAAc,SAAS;AACvB,SAAO"}
|
|
@@ -3,7 +3,7 @@ import "../../../shared/src/must.js";
|
|
|
3
3
|
import "../../../zql/src/query/query-internals.js";
|
|
4
4
|
import "../../../zql/src/query/query-impl.js";
|
|
5
5
|
import "../../../zql/src/query/query-registry.js";
|
|
6
|
-
import "../../../zql/src/ivm/
|
|
6
|
+
import "../../../zql/src/ivm/skip-yields.js";
|
|
7
7
|
import "../../../zql/src/ivm/stream.js";
|
|
8
8
|
import "../../../zql/src/ivm/view-apply-change.js";
|
|
9
9
|
import "../../../zql/src/query/ttl.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-impl.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/crud-impl.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"crud-impl.d.ts","sourceRoot":"","sources":["../../../../../zero-client/src/client/crud-impl.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mCAAmC,CAAC;AAO9D,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,iBAAiB,CAAC;AAErD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAC,YAAY,EAAC,MAAM,iCAAiC,CAAC;AAelE,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,GAAG,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,MAAM,CAC1B,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED,iBAAe,UAAU,CACvB,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,eAAe,GAAG,SAAS,GACrC,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED,OAAO,EAAC,UAAU,IAAI,MAAM,EAAC,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { __exportAll } from "../../../_virtual/_rolldown/runtime.js";
|
|
|
2
2
|
import { must } from "../../../shared/src/must.js";
|
|
3
3
|
import "../../../zero-protocol/src/push.js";
|
|
4
4
|
import { consume } from "../../../zql/src/ivm/stream.js";
|
|
5
|
+
import { makeSourceChangeAdd, makeSourceChangeEdit, makeSourceChangeRemove } from "../../../zql/src/ivm/source.js";
|
|
5
6
|
import { toPrimaryKeyString } from "./keys.js";
|
|
6
7
|
//#region ../zero-client/src/client/crud-impl.ts
|
|
7
8
|
var crud_impl_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -23,10 +24,7 @@ async function insert(tx, arg, schema, ivmBranch) {
|
|
|
23
24
|
if (!await tx.has(key)) {
|
|
24
25
|
const val = defaultOptionalFieldsToNull(schema.tables[arg.tableName], arg.value);
|
|
25
26
|
await tx.set(key, val);
|
|
26
|
-
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(
|
|
27
|
-
type: "add",
|
|
28
|
-
row: arg.value
|
|
29
|
-
}));
|
|
27
|
+
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(makeSourceChangeAdd(arg.value)));
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
30
|
async function upsert(tx, arg, schema, ivmBranch) {
|
|
@@ -48,21 +46,14 @@ async function update(tx, arg, schema, ivmBranch) {
|
|
|
48
46
|
const next = { ...prev };
|
|
49
47
|
for (const k in update) if (update[k] !== void 0) next[k] = update[k];
|
|
50
48
|
await tx.set(key, next);
|
|
51
|
-
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(
|
|
52
|
-
type: "edit",
|
|
53
|
-
oldRow: prev,
|
|
54
|
-
row: next
|
|
55
|
-
}));
|
|
49
|
+
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(makeSourceChangeEdit(next, prev)));
|
|
56
50
|
}
|
|
57
51
|
async function deleteImpl(tx, arg, schema, ivmBranch) {
|
|
58
52
|
const key = toPrimaryKeyString(arg.tableName, schema.tables[arg.tableName].primaryKey, arg.value);
|
|
59
53
|
const prev = await tx.get(key);
|
|
60
54
|
if (prev === void 0) return;
|
|
61
55
|
await tx.del(key);
|
|
62
|
-
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(
|
|
63
|
-
type: "remove",
|
|
64
|
-
row: prev
|
|
65
|
-
}));
|
|
56
|
+
if (ivmBranch) consume(must(ivmBranch.getSource(arg.tableName)).push(makeSourceChangeRemove(prev)));
|
|
66
57
|
}
|
|
67
58
|
//#endregion
|
|
68
59
|
export { crud_impl_exports };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-impl.js","names":[],"sources":["../../../../../zero-client/src/client/crud-impl.ts"],"sourcesContent":["import type {ReadonlyJSONObject} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {\n type DeleteOp,\n type InsertOp,\n type UpdateOp,\n type UpsertOp,\n} from '../../../zero-protocol/src/push.ts';\nimport type {TableSchema} from '../../../zero-schema/src/table-schema.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport {consume} from '../../../zql/src/ivm/stream.ts';\nimport type {IVMSourceBranch} from './ivm-branch.ts';\nimport {toPrimaryKeyString} from './keys.ts';\nimport type {WriteTransaction} from './replicache-types.ts';\nexport type {TableMutator} from '../../../zql/src/mutate/crud.ts';\n\nfunction defaultOptionalFieldsToNull(\n schema: TableSchema,\n value: ReadonlyJSONObject,\n): ReadonlyJSONObject {\n let rv = value;\n for (const name in schema.columns) {\n if (rv[name] === undefined) {\n rv = {...rv, [name]: null};\n }\n }\n return rv;\n}\n\nexport async function insert(\n tx: WriteTransaction,\n arg: InsertOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n if (!(await tx.has(key))) {\n const val = defaultOptionalFieldsToNull(\n schema.tables[arg.tableName],\n arg.value,\n );\n await tx.set(key, val);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(
|
|
1
|
+
{"version":3,"file":"crud-impl.js","names":[],"sources":["../../../../../zero-client/src/client/crud-impl.ts"],"sourcesContent":["import type {ReadonlyJSONObject} from '../../../shared/src/json.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport type {Row} from '../../../zero-protocol/src/data.ts';\nimport {\n type DeleteOp,\n type InsertOp,\n type UpdateOp,\n type UpsertOp,\n} from '../../../zero-protocol/src/push.ts';\nimport type {TableSchema} from '../../../zero-schema/src/table-schema.ts';\nimport type {Schema} from '../../../zero-types/src/schema.ts';\nimport {\n makeSourceChangeAdd,\n makeSourceChangeEdit,\n makeSourceChangeRemove,\n} from '../../../zql/src/ivm/source.ts';\nimport {consume} from '../../../zql/src/ivm/stream.ts';\nimport type {IVMSourceBranch} from './ivm-branch.ts';\nimport {toPrimaryKeyString} from './keys.ts';\nimport type {WriteTransaction} from './replicache-types.ts';\nexport type {TableMutator} from '../../../zql/src/mutate/crud.ts';\n\nfunction defaultOptionalFieldsToNull(\n schema: TableSchema,\n value: ReadonlyJSONObject,\n): ReadonlyJSONObject {\n let rv = value;\n for (const name in schema.columns) {\n if (rv[name] === undefined) {\n rv = {...rv, [name]: null};\n }\n }\n return rv;\n}\n\nexport async function insert(\n tx: WriteTransaction,\n arg: InsertOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n if (!(await tx.has(key))) {\n const val = defaultOptionalFieldsToNull(\n schema.tables[arg.tableName],\n arg.value,\n );\n await tx.set(key, val);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeAdd(arg.value),\n ),\n );\n }\n }\n}\n\nexport async function upsert(\n tx: WriteTransaction,\n arg: InsertOp | UpsertOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n if (await tx.has(key)) {\n await update(tx, {...arg, op: 'update'}, schema, ivmBranch);\n } else {\n await insert(tx, {...arg, op: 'insert'}, schema, ivmBranch);\n }\n}\n\nexport async function update(\n tx: WriteTransaction,\n arg: UpdateOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n const prev = await tx.get(key);\n if (prev === undefined) {\n return;\n }\n const update = arg.value;\n const next = {...(prev as ReadonlyJSONObject)};\n for (const k in update) {\n if (update[k] !== undefined) {\n next[k] = update[k];\n }\n }\n await tx.set(key, next);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeEdit(next, prev as Row),\n ),\n );\n }\n}\n\nasync function deleteImpl(\n tx: WriteTransaction,\n arg: DeleteOp,\n schema: Schema,\n ivmBranch: IVMSourceBranch | undefined,\n): Promise<void> {\n const key = toPrimaryKeyString(\n arg.tableName,\n schema.tables[arg.tableName].primaryKey,\n arg.value,\n );\n const prev = await tx.get(key);\n if (prev === undefined) {\n return;\n }\n await tx.del(key);\n if (ivmBranch) {\n consume(\n must(ivmBranch.getSource(arg.tableName)).push(\n makeSourceChangeRemove(prev as Row),\n ),\n );\n }\n}\n\nexport {deleteImpl as delete};\n"],"mappings":";;;;;;;;;;;;;AAsBA,SAAS,4BACP,QACA,OACoB;CACpB,IAAI,KAAK;AACT,MAAK,MAAM,QAAQ,OAAO,QACxB,KAAI,GAAG,UAAU,KAAA,EACf,MAAK;EAAC,GAAG;GAAK,OAAO;EAAK;AAG9B,QAAO;;AAGT,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;AACD,KAAI,CAAE,MAAM,GAAG,IAAI,IAAI,EAAG;EACxB,MAAM,MAAM,4BACV,OAAO,OAAO,IAAI,YAClB,IAAI,MACL;AACD,QAAM,GAAG,IAAI,KAAK,IAAI;AACtB,MAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,oBAAoB,IAAI,MAAM,CAC/B,CACF;;;AAKP,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;AACD,KAAI,MAAM,GAAG,IAAI,IAAI,CACnB,OAAM,OAAO,IAAI;EAAC,GAAG;EAAK,IAAI;EAAS,EAAE,QAAQ,UAAU;KAE3D,OAAM,OAAO,IAAI;EAAC,GAAG;EAAK,IAAI;EAAS,EAAE,QAAQ,UAAU;;AAI/D,eAAsB,OACpB,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;CACD,MAAM,OAAO,MAAM,GAAG,IAAI,IAAI;AAC9B,KAAI,SAAS,KAAA,EACX;CAEF,MAAM,SAAS,IAAI;CACnB,MAAM,OAAO,EAAC,GAAI,MAA4B;AAC9C,MAAK,MAAM,KAAK,OACd,KAAI,OAAO,OAAO,KAAA,EAChB,MAAK,KAAK,OAAO;AAGrB,OAAM,GAAG,IAAI,KAAK,KAAK;AACvB,KAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,qBAAqB,MAAM,KAAY,CACxC,CACF;;AAIL,eAAe,WACb,IACA,KACA,QACA,WACe;CACf,MAAM,MAAM,mBACV,IAAI,WACJ,OAAO,OAAO,IAAI,WAAW,YAC7B,IAAI,MACL;CACD,MAAM,OAAO,MAAM,GAAG,IAAI,IAAI;AAC9B,KAAI,SAAS,KAAA,EACX;AAEF,OAAM,GAAG,IAAI,IAAI;AACjB,KAAI,UACF,SACE,KAAK,UAAU,UAAU,IAAI,UAAU,CAAC,CAAC,KACvC,uBAAuB,KAAY,CACpC,CACF"}
|