@livestore/common 0.4.0-dev.9 → 0.4.0
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/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +27 -12
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
- package/dist/WorkerTransportError.d.ts +11 -0
- package/dist/WorkerTransportError.d.ts.map +1 -0
- package/dist/WorkerTransportError.js +11 -0
- package/dist/WorkerTransportError.js.map +1 -0
- package/dist/adapter-types.d.ts +37 -7
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +27 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/bounded-collections.js +6 -4
- package/dist/bounded-collections.js.map +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +33 -6
- package/dist/debug-info.js.map +1 -1
- package/dist/devtools/devtools-compatibility.test.d.ts +2 -0
- package/dist/devtools/devtools-compatibility.test.d.ts.map +1 -0
- package/dist/devtools/devtools-compatibility.test.js +15 -0
- package/dist/devtools/devtools-compatibility.test.js.map +1 -0
- package/dist/devtools/devtools-messages-client-session.d.ts +55 -24
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-client-session.js +22 -5
- package/dist/devtools/devtools-messages-client-session.js.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +11 -14
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +7 -9
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +65 -30
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +29 -11
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/devtools/devtools-sessioninfo.d.ts +14 -2
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
- package/dist/devtools/devtools-sessioninfo.js +7 -4
- package/dist/devtools/devtools-sessioninfo.js.map +1 -1
- package/dist/devtools/mod.d.ts +13 -2
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +10 -3
- package/dist/devtools/mod.js.map +1 -1
- package/dist/errors.d.ts +48 -18
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -12
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +53 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +325 -257
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/RejectedPushError.d.ts +107 -0
- package/dist/leader-thread/RejectedPushError.d.ts.map +1 -0
- package/dist/leader-thread/RejectedPushError.js +78 -0
- package/dist/leader-thread/RejectedPushError.js.map +1 -0
- package/dist/leader-thread/connection.js +1 -1
- package/dist/leader-thread/connection.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +19 -14
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +78 -18
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -2
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +90 -58
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +15 -7
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +49 -17
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
- package/dist/leader-thread/materialize-event.d.ts +1 -1
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +28 -9
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -0
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +2 -2
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +6 -6
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -2
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -2
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/stream-events.d.ts +56 -0
- package/dist/leader-thread/stream-events.d.ts.map +1 -0
- package/dist/leader-thread/stream-events.js +167 -0
- package/dist/leader-thread/stream-events.js.map +1 -0
- package/dist/leader-thread/types.d.ts +95 -17
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js +13 -0
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/logging.d.ts +40 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +33 -0
- package/dist/logging.js.map +1 -0
- package/dist/make-client-session.d.ts +5 -3
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +7 -4
- package/dist/make-client-session.js.map +1 -1
- package/dist/materializer-helper.d.ts +6 -6
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +18 -8
- package/dist/materializer-helper.js.map +1 -1
- package/dist/otel.d.ts +2 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +7 -2
- package/dist/otel.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +3 -3
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +40 -29
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef/define.d.ts +161 -0
- package/dist/schema/EventDef/define.d.ts.map +1 -0
- package/dist/schema/EventDef/define.js +140 -0
- package/dist/schema/EventDef/define.js.map +1 -0
- package/dist/schema/EventDef/deprecated.d.ts +99 -0
- package/dist/schema/EventDef/deprecated.d.ts.map +1 -0
- package/dist/schema/EventDef/deprecated.js +144 -0
- package/dist/schema/EventDef/deprecated.js.map +1 -0
- package/dist/schema/EventDef/deprecated.test.d.ts +2 -0
- package/dist/schema/EventDef/deprecated.test.d.ts.map +1 -0
- package/dist/schema/EventDef/deprecated.test.js +95 -0
- package/dist/schema/EventDef/deprecated.test.js.map +1 -0
- package/dist/schema/EventDef/event-def.d.ts +110 -0
- package/dist/schema/EventDef/event-def.d.ts.map +1 -0
- package/dist/schema/EventDef/event-def.js +2 -0
- package/dist/schema/EventDef/event-def.js.map +1 -0
- package/dist/schema/EventDef/facts.d.ts +118 -0
- package/dist/schema/EventDef/facts.d.ts.map +1 -0
- package/dist/schema/EventDef/facts.js +53 -0
- package/dist/schema/EventDef/facts.js.map +1 -0
- package/dist/schema/EventDef/materializer.d.ts +155 -0
- package/dist/schema/EventDef/materializer.d.ts.map +1 -0
- package/dist/schema/EventDef/materializer.js +83 -0
- package/dist/schema/EventDef/materializer.js.map +1 -0
- package/dist/schema/EventDef/mod.d.ts +6 -0
- package/dist/schema/EventDef/mod.d.ts.map +1 -0
- package/dist/schema/EventDef/mod.js +6 -0
- package/dist/schema/EventDef/mod.js.map +1 -0
- package/dist/schema/EventSequenceNumber/client.d.ts +136 -0
- package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -0
- package/dist/schema/EventSequenceNumber/client.js +193 -0
- package/dist/schema/EventSequenceNumber/client.js.map +1 -0
- package/dist/schema/EventSequenceNumber/global.d.ts +15 -0
- package/dist/schema/EventSequenceNumber/global.d.ts.map +1 -0
- package/dist/schema/EventSequenceNumber/global.js +14 -0
- package/dist/schema/EventSequenceNumber/global.js.map +1 -0
- package/dist/schema/EventSequenceNumber/mod.d.ts +37 -0
- package/dist/schema/EventSequenceNumber/mod.d.ts.map +1 -0
- package/dist/schema/EventSequenceNumber/mod.js +37 -0
- package/dist/schema/EventSequenceNumber/mod.js.map +1 -0
- package/dist/schema/EventSequenceNumber.test.js +44 -44
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/{LiveStoreEvent.d.ts → LiveStoreEvent/client.d.ts} +102 -111
- package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/client.js +176 -0
- package/dist/schema/LiveStoreEvent/client.js.map +1 -0
- package/dist/schema/LiveStoreEvent/client.test.d.ts +2 -0
- package/dist/schema/LiveStoreEvent/client.test.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/client.test.js +111 -0
- package/dist/schema/LiveStoreEvent/client.test.js.map +1 -0
- package/dist/schema/LiveStoreEvent/for-event-def.d.ts +52 -0
- package/dist/schema/LiveStoreEvent/for-event-def.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/for-event-def.js +2 -0
- package/dist/schema/LiveStoreEvent/for-event-def.js.map +1 -0
- package/dist/schema/LiveStoreEvent/global.d.ts +36 -0
- package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/global.js +31 -0
- package/dist/schema/LiveStoreEvent/global.js.map +1 -0
- package/dist/schema/LiveStoreEvent/input.d.ts +46 -0
- package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/input.js +26 -0
- package/dist/schema/LiveStoreEvent/input.js.map +1 -0
- package/dist/schema/LiveStoreEvent/mod.d.ts +5 -0
- package/dist/schema/LiveStoreEvent/mod.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent/mod.js +5 -0
- package/dist/schema/LiveStoreEvent/mod.js.map +1 -0
- package/dist/schema/events.d.ts +1 -1
- package/dist/schema/events.d.ts.map +1 -1
- package/dist/schema/events.js +1 -1
- package/dist/schema/events.js.map +1 -1
- package/dist/schema/mod.d.ts +6 -4
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +5 -4
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +16 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +32 -4
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +2 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +36 -15
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +121 -2
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.js +1 -1
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.test.js +2 -2
- package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +96 -47
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +51 -12
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-spec.js +30 -12
- package/dist/schema/state/sqlite/column-spec.js.map +1 -1
- package/dist/schema/state/sqlite/column-spec.test.js +24 -15
- package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +2 -2
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +16 -10
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +15 -4
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +2 -2
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +5 -7
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +51 -22
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.js +99 -22
- package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +28 -15
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +231 -93
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.d.ts +2 -2
- package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.js +24 -14
- package/dist/schema/state/sqlite/schema-helpers.js.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.test.d.ts +2 -0
- package/dist/schema/state/sqlite/schema-helpers.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/schema-helpers.test.js +36 -0
- package/dist/schema/state/sqlite/schema-helpers.test.js.map +1 -0
- package/dist/schema/state/sqlite/{system-tables.d.ts → system-tables/eventlog-tables.d.ts} +21 -450
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +54 -0
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js.map +1 -0
- package/dist/schema/state/sqlite/system-tables/mod.d.ts +3 -0
- package/dist/schema/state/sqlite/system-tables/mod.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/mod.js +3 -0
- package/dist/schema/state/sqlite/system-tables/mod.js.map +1 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +456 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.js +55 -0
- package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -0
- package/dist/schema/state/sqlite/table-def.d.ts +5 -3
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +1 -1
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +92 -3
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts +47 -0
- package/dist/schema/unknown-events.d.ts.map +1 -0
- package/dist/schema/unknown-events.js +69 -0
- package/dist/schema/unknown-events.js.map +1 -0
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.d.ts +2 -0
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.d.ts.map +1 -0
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +73 -0
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -0
- package/dist/schema-management/common.js +2 -2
- package/dist/schema-management/common.js.map +1 -1
- package/dist/schema-management/migrations.d.ts +32 -2
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +38 -6
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-schema.d.ts +3 -3
- package/dist/schema-management/validate-schema.d.ts.map +1 -1
- package/dist/schema-management/validate-schema.js +2 -2
- package/dist/schema-management/validate-schema.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +18 -6
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js.map +1 -1
- package/dist/sqlite-db-helper.js +3 -3
- package/dist/sqlite-db-helper.js.map +1 -1
- package/dist/sqlite-types.d.ts +5 -5
- package/dist/sqlite-types.d.ts.map +1 -1
- package/dist/sqlite-types.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -12
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +99 -114
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +0 -33
- package/dist/sync/errors.d.ts.map +1 -1
- package/dist/sync/errors.js +5 -22
- package/dist/sync/errors.js.map +1 -1
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +2 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mock-sync-backend.d.ts +10 -8
- package/dist/sync/mock-sync-backend.d.ts.map +1 -1
- package/dist/sync/mock-sync-backend.js +71 -69
- package/dist/sync/mock-sync-backend.js.map +1 -1
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +11 -12
- package/dist/sync/next/compact-events.js.map +1 -1
- package/dist/sync/next/facts.d.ts +5 -5
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +7 -8
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +54 -15
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +198 -9
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +11 -11
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +5 -5
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +6 -6
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.calculator.test.js +2 -2
- package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.d.ts.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +2 -2
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts +2 -2
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +11 -11
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/sync-backend-kv.d.ts +3 -3
- package/dist/sync/sync-backend-kv.d.ts.map +1 -1
- package/dist/sync/sync-backend-kv.js +3 -3
- package/dist/sync/sync-backend-kv.js.map +1 -1
- package/dist/sync/sync-backend.d.ts +33 -13
- package/dist/sync/sync-backend.d.ts.map +1 -1
- package/dist/sync/sync-backend.js +38 -1
- package/dist/sync/sync-backend.js.map +1 -1
- package/dist/sync/sync.d.ts +23 -2
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/syncstate.d.ts +55 -55
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +80 -98
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +221 -132
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/transport-chunking.d.ts +36 -0
- package/dist/sync/transport-chunking.d.ts.map +1 -0
- package/dist/sync/transport-chunking.js +56 -0
- package/dist/sync/transport-chunking.js.map +1 -0
- package/dist/sync/validate-push-payload.d.ts +2 -2
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +4 -6
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/testing/event-factory.d.ts +3 -3
- package/dist/testing/event-factory.d.ts.map +1 -1
- package/dist/testing/event-factory.js +5 -7
- package/dist/testing/event-factory.js.map +1 -1
- package/dist/util.js +2 -2
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +24 -5
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +25 -8
- package/dist/version.js.map +1 -1
- package/package.json +67 -15
- package/src/ClientSessionLeaderThreadProxy.ts +27 -12
- package/src/WorkerTransportError.ts +12 -0
- package/src/adapter-types.ts +50 -7
- package/src/bounded-collections.ts +6 -5
- package/src/debug-info.ts +37 -6
- package/src/devtools/devtools-compatibility.test.ts +18 -0
- package/src/devtools/devtools-messages-client-session.ts +22 -4
- package/src/devtools/devtools-messages-common.ts +7 -12
- package/src/devtools/devtools-messages-leader.ts +29 -10
- package/src/devtools/devtools-sessioninfo.ts +8 -5
- package/src/devtools/mod.ts +11 -2
- package/src/errors.ts +32 -24
- package/src/index.ts +4 -1
- package/src/leader-thread/LeaderSyncProcessor.ts +523 -373
- package/src/leader-thread/RejectedPushError.ts +106 -0
- package/src/leader-thread/connection.ts +1 -1
- package/src/leader-thread/eventlog.ts +112 -39
- package/src/leader-thread/leader-worker-devtools.ts +201 -120
- package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
- package/src/leader-thread/make-leader-thread-layer.ts +125 -40
- package/src/leader-thread/materialize-event.ts +40 -10
- package/src/leader-thread/mod.ts +1 -0
- package/src/leader-thread/recreate-db.ts +7 -7
- package/src/leader-thread/shutdown-channel.ts +4 -8
- package/src/leader-thread/stream-events.ts +206 -0
- package/src/leader-thread/types.ts +68 -18
- package/src/logging.ts +62 -0
- package/src/make-client-session.ts +11 -5
- package/src/materializer-helper.ts +27 -16
- package/src/otel.ts +13 -2
- package/src/rematerialize-from-eventlog.ts +61 -51
- package/src/schema/EventDef/define.ts +217 -0
- package/src/schema/EventDef/deprecated.test.ts +129 -0
- package/src/schema/EventDef/deprecated.ts +175 -0
- package/src/schema/EventDef/event-def.ts +125 -0
- package/src/schema/EventDef/facts.ts +135 -0
- package/src/schema/EventDef/materializer.ts +172 -0
- package/src/schema/EventDef/mod.ts +5 -0
- package/src/schema/EventSequenceNumber/client.ts +257 -0
- package/src/schema/EventSequenceNumber/global.ts +19 -0
- package/src/schema/EventSequenceNumber/mod.ts +37 -0
- package/src/schema/EventSequenceNumber.test.ts +72 -53
- package/src/schema/LiveStoreEvent/client.test.ts +129 -0
- package/src/schema/LiveStoreEvent/client.ts +235 -0
- package/src/schema/LiveStoreEvent/for-event-def.ts +60 -0
- package/src/schema/LiveStoreEvent/global.ts +45 -0
- package/src/schema/LiveStoreEvent/input.ts +63 -0
- package/src/schema/LiveStoreEvent/mod.ts +4 -0
- package/src/schema/events.ts +1 -1
- package/src/schema/mod.ts +6 -4
- package/src/schema/schema.ts +46 -5
- package/src/schema/state/sqlite/client-document-def.test.ts +144 -5
- package/src/schema/state/sqlite/client-document-def.ts +47 -34
- package/src/schema/state/sqlite/column-annotations.test.ts +3 -2
- package/src/schema/state/sqlite/column-annotations.ts +2 -1
- package/src/schema/state/sqlite/column-def.test.ts +66 -12
- package/src/schema/state/sqlite/column-def.ts +119 -47
- package/src/schema/state/sqlite/column-spec.test.ts +32 -17
- package/src/schema/state/sqlite/column-spec.ts +37 -11
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -2
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +2 -1
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +41 -15
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +13 -19
- package/src/schema/state/sqlite/mod.ts +7 -8
- package/src/schema/state/sqlite/query-builder/api.ts +55 -17
- package/src/schema/state/sqlite/query-builder/astToSql.ts +110 -21
- package/src/schema/state/sqlite/query-builder/impl.test.ts +267 -93
- package/src/schema/state/sqlite/query-builder/impl.ts +26 -13
- package/src/schema/state/sqlite/schema-helpers.test.ts +44 -0
- package/src/schema/state/sqlite/schema-helpers.ts +30 -22
- package/src/schema/state/sqlite/system-tables/eventlog-tables.ts +64 -0
- package/src/schema/state/sqlite/system-tables/mod.ts +2 -0
- package/src/schema/state/sqlite/system-tables/state-tables.ts +69 -0
- package/src/schema/state/sqlite/table-def.test.ts +114 -3
- package/src/schema/state/sqlite/table-def.ts +16 -22
- package/src/schema/unknown-events.ts +131 -0
- package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +88 -0
- package/src/schema-management/common.ts +2 -2
- package/src/schema-management/migrations.ts +42 -9
- package/src/schema-management/validate-schema.ts +3 -3
- package/src/sql-queries/sql-queries.ts +18 -6
- package/src/sql-queries/sql-query-builder.ts +1 -0
- package/src/sqlite-db-helper.ts +3 -3
- package/src/sqlite-types.ts +6 -5
- package/src/sync/ClientSessionSyncProcessor.ts +152 -142
- package/src/sync/errors.ts +12 -24
- package/src/sync/index.ts +2 -0
- package/src/sync/mock-sync-backend.ts +146 -104
- package/src/sync/next/compact-events.ts +10 -11
- package/src/sync/next/facts.ts +13 -14
- package/src/sync/next/history-dag-common.ts +280 -26
- package/src/sync/next/history-dag.ts +17 -13
- package/src/sync/next/rebase-events.ts +12 -12
- package/src/sync/next/test/compact-events.calculator.test.ts +3 -2
- package/src/sync/next/test/compact-events.test.ts +4 -3
- package/src/sync/next/test/event-fixtures.ts +13 -13
- package/src/sync/sync-backend-kv.ts +4 -3
- package/src/sync/sync-backend.ts +66 -17
- package/src/sync/sync.ts +24 -2
- package/src/sync/syncstate.test.ts +583 -419
- package/src/sync/syncstate.ts +127 -122
- package/src/sync/transport-chunking.ts +90 -0
- package/src/sync/validate-push-payload.ts +6 -8
- package/src/testing/event-factory.ts +10 -12
- package/src/util.ts +2 -2
- package/src/version.ts +33 -8
- package/dist/schema/EventDef.d.ts +0 -126
- package/dist/schema/EventDef.d.ts.map +0 -1
- package/dist/schema/EventDef.js +0 -46
- package/dist/schema/EventDef.js.map +0 -1
- package/dist/schema/EventSequenceNumber.d.ts +0 -80
- package/dist/schema/EventSequenceNumber.d.ts.map +0 -1
- package/dist/schema/EventSequenceNumber.js +0 -139
- package/dist/schema/EventSequenceNumber.js.map +0 -1
- package/dist/schema/LiveStoreEvent.d.ts.map +0 -1
- package/dist/schema/LiveStoreEvent.js +0 -147
- package/dist/schema/LiveStoreEvent.js.map +0 -1
- package/dist/schema/state/sqlite/system-tables.d.ts.map +0 -1
- package/dist/schema/state/sqlite/system-tables.js +0 -81
- package/dist/schema/state/sqlite/system-tables.js.map +0 -1
- package/dist/sync/next/graphology.d.ts +0 -8
- package/dist/sync/next/graphology.d.ts.map +0 -1
- package/dist/sync/next/graphology.js +0 -30
- package/dist/sync/next/graphology.js.map +0 -1
- package/dist/sync/next/graphology_.d.ts +0 -3
- package/dist/sync/next/graphology_.d.ts.map +0 -1
- package/dist/sync/next/graphology_.js +0 -3
- package/dist/sync/next/graphology_.js.map +0 -1
- package/src/schema/EventDef.ts +0 -222
- package/src/schema/EventSequenceNumber.ts +0 -199
- package/src/schema/LiveStoreEvent.ts +0 -286
- package/src/schema/state/sqlite/system-tables.ts +0 -106
- package/src/sync/next/ambient.d.ts +0 -3
- package/src/sync/next/graphology.ts +0 -41
- package/src/sync/next/graphology_.ts +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { casesHandled, isNotUndefined, LS_DEV,
|
|
1
|
+
import { casesHandled, isNotUndefined, LS_DEV, TRACE_VERBOSE } from '@livestore/utils'
|
|
2
2
|
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
|
3
3
|
import {
|
|
4
4
|
BucketQueue,
|
|
@@ -10,42 +10,37 @@ import {
|
|
|
10
10
|
FiberHandle,
|
|
11
11
|
Layer,
|
|
12
12
|
Option,
|
|
13
|
-
OtelTracer,
|
|
14
|
-
pipe,
|
|
15
13
|
Queue,
|
|
16
14
|
ReadonlyArray,
|
|
17
15
|
Schedule,
|
|
16
|
+
Schema,
|
|
18
17
|
Stream,
|
|
19
18
|
Subscribable,
|
|
20
19
|
SubscriptionRef,
|
|
21
20
|
} from '@livestore/utils/effect'
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
type MaterializeError,
|
|
26
|
-
type SqliteDb,
|
|
27
|
-
UnexpectedError,
|
|
28
|
-
} from '../adapter-types.ts'
|
|
21
|
+
|
|
22
|
+
import { type MaterializeError, type SqliteDb, UnknownError } from '../adapter-types.ts'
|
|
23
|
+
import { IntentionalShutdownCause } from '../errors.ts'
|
|
29
24
|
import { makeMaterializerHash } from '../materializer-helper.ts'
|
|
30
25
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
|
31
|
-
import { EventSequenceNumber,
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
type IsOfflineError,
|
|
36
|
-
LeaderAheadError,
|
|
37
|
-
type SyncBackend,
|
|
38
|
-
} from '../sync/sync.ts'
|
|
26
|
+
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
|
|
27
|
+
import { EVENTLOG_META_TABLE, SYNC_STATUS_TABLE } from '../schema/state/sqlite/system-tables/eventlog-tables.ts'
|
|
28
|
+
import type { BackendIdMismatchError, IsOfflineError, SyncBackend } from '../sync/sync.ts'
|
|
29
|
+
import { isRejectedPushError, LeaderAheadError, NonMonotonicBatchError, StaleRebaseGenerationError } from './RejectedPushError.ts'
|
|
39
30
|
import * as SyncState from '../sync/syncstate.ts'
|
|
40
31
|
import { sql } from '../util.ts'
|
|
41
32
|
import * as Eventlog from './eventlog.ts'
|
|
42
33
|
import { rollback } from './materialize-event.ts'
|
|
34
|
+
import type { ShutdownChannel } from './shutdown-channel.ts'
|
|
43
35
|
import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.ts'
|
|
44
36
|
import { LeaderThreadCtx } from './types.ts'
|
|
45
37
|
|
|
38
|
+
/** Serialize value to JSON string for trace attributes */
|
|
39
|
+
const jsonStringify = Schema.encodeSync(Schema.parseJson())
|
|
40
|
+
|
|
46
41
|
type LocalPushQueueItem = [
|
|
47
|
-
event: LiveStoreEvent.EncodedWithMeta,
|
|
48
|
-
deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
|
|
42
|
+
event: LiveStoreEvent.Client.EncodedWithMeta,
|
|
43
|
+
deferred: Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError> | undefined,
|
|
49
44
|
]
|
|
50
45
|
|
|
51
46
|
/**
|
|
@@ -66,11 +61,11 @@ type LocalPushQueueItem = [
|
|
|
66
61
|
* - Maintains events in ascending order.
|
|
67
62
|
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
|
68
63
|
* - Processes events from the queue, applying events in batches.
|
|
69
|
-
* - Controlled by a `
|
|
70
|
-
* - The
|
|
64
|
+
* - Controlled by a mutex (`Semaphore(1)`) to ensure mutual exclusion between local push and backend pull processing.
|
|
65
|
+
* - The backend pull side acquires the mutex before processing and releases it on post-pull completion.
|
|
71
66
|
* - Processes up to `maxBatchSize` events per cycle.
|
|
72
67
|
*
|
|
73
|
-
* Currently we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
|
|
68
|
+
* Currently, we're advancing the state db and eventlog in lockstep, but we could also decouple this in the future
|
|
74
69
|
*
|
|
75
70
|
* Tricky concurrency scenarios:
|
|
76
71
|
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
|
@@ -84,6 +79,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
84
79
|
initialBlockingSyncContext,
|
|
85
80
|
initialSyncState,
|
|
86
81
|
onError,
|
|
82
|
+
onBackendIdMismatch,
|
|
87
83
|
livePull,
|
|
88
84
|
params,
|
|
89
85
|
testing,
|
|
@@ -93,13 +89,60 @@ export const makeLeaderSyncProcessor = ({
|
|
|
93
89
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
94
90
|
/** Initial sync state rehydrated from the persisted eventlog or initial sync state */
|
|
95
91
|
initialSyncState: SyncState.SyncState
|
|
92
|
+
/**
|
|
93
|
+
* What to do when a failure (any cause) occurs (except `BackendIdMismatchError`).
|
|
94
|
+
*
|
|
95
|
+
* - `'shutdown'`: Send the error to the shutdown channel and terminate the sync processor.
|
|
96
|
+
* - `'ignore'`: Continue running.
|
|
97
|
+
*/
|
|
96
98
|
onError: 'shutdown' | 'ignore'
|
|
99
|
+
/**
|
|
100
|
+
* What to do when the sync backend identity has changed (i.e. the backend was reset).
|
|
101
|
+
*
|
|
102
|
+
* - `'reset'`: Clear local databases (eventlog and state) and send an intentional shutdown signal.
|
|
103
|
+
* - `'shutdown'`: Send a shutdown signal without clearing local storage.
|
|
104
|
+
* - `'ignore'`: Continue running with stale data.
|
|
105
|
+
*/
|
|
106
|
+
onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
|
|
97
107
|
params: {
|
|
98
108
|
/**
|
|
109
|
+
* Maximum number of local events to process per batch cycle.
|
|
110
|
+
*
|
|
111
|
+
* This controls how many events from client sessions are applied to the local state
|
|
112
|
+
* in a single iteration before yielding to allow potential backend pulls.
|
|
113
|
+
*
|
|
114
|
+
* **Trade-offs:**
|
|
115
|
+
* - **Lower values (1-5):** More responsive to remote updates since pull processing can
|
|
116
|
+
* interleave more frequently. Better for high-conflict scenarios where rebases are common.
|
|
117
|
+
* Slightly higher per-event overhead due to more frequent transaction commits.
|
|
118
|
+
*
|
|
119
|
+
* - **Higher values (10-50+):** Better throughput for bulk local writes as more events are
|
|
120
|
+
* batched into a single transaction. However, may delay remote update processing and
|
|
121
|
+
* increase rebase complexity if many local events queue up during a slow pull.
|
|
122
|
+
*
|
|
123
|
+
* - **Very high values (100+):** Risk of starvation for pull processing if local pushes
|
|
124
|
+
* arrive continuously. May cause larger rollbacks during rebases. Not recommended
|
|
125
|
+
* unless you have a write-heavy workload with minimal remote synchronization.
|
|
126
|
+
*
|
|
99
127
|
* @default 10
|
|
100
128
|
*/
|
|
101
129
|
localPushBatchSize?: number
|
|
102
130
|
/**
|
|
131
|
+
* Maximum number of events to push to the sync backend per batch.
|
|
132
|
+
*
|
|
133
|
+
* This controls how many events are sent in a single push request to the remote server.
|
|
134
|
+
*
|
|
135
|
+
* **Trade-offs:**
|
|
136
|
+
* - **Lower values (1-10):** Lower latency for each push operation. Faster feedback on
|
|
137
|
+
* push success/failure. Slightly higher network overhead due to more requests.
|
|
138
|
+
*
|
|
139
|
+
* - **Higher values (50-100):** Better network efficiency by amortizing request overhead.
|
|
140
|
+
* Preferred for high-throughput scenarios. May increase latency to first confirmation.
|
|
141
|
+
*
|
|
142
|
+
* - **Very high values (200+):** Risk of hitting server request size limits or timeouts.
|
|
143
|
+
* A single failed request loses the entire batch (will be retried). May cause memory
|
|
144
|
+
* pressure if events accumulate faster than they can be pushed.
|
|
145
|
+
*
|
|
103
146
|
* @default 50
|
|
104
147
|
*/
|
|
105
148
|
backendPushBatchSize?: number
|
|
@@ -114,18 +157,16 @@ export const makeLeaderSyncProcessor = ({
|
|
|
114
157
|
localPushProcessing?: Effect.Effect<void>
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
|
-
}): Effect.Effect<LeaderSyncProcessor,
|
|
160
|
+
}): Effect.Effect<LeaderSyncProcessor, never, Scope.Scope> =>
|
|
118
161
|
Effect.gen(function* () {
|
|
119
|
-
const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.EncodedWithMeta>()
|
|
120
|
-
const localPushBatchSize = params.localPushBatchSize ??
|
|
121
|
-
const backendPushBatchSize = params.backendPushBatchSize ??
|
|
162
|
+
const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.Client.EncodedWithMeta>()
|
|
163
|
+
const localPushBatchSize = params.localPushBatchSize ?? 10
|
|
164
|
+
const backendPushBatchSize = params.backendPushBatchSize ?? 50
|
|
122
165
|
|
|
123
166
|
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
|
124
167
|
|
|
125
|
-
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
|
|
126
|
-
|
|
127
|
-
return eventDef.options.clientOnly
|
|
128
|
-
}
|
|
168
|
+
const isClientEvent = (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) =>
|
|
169
|
+
schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
|
|
129
170
|
|
|
130
171
|
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
|
131
172
|
|
|
@@ -134,7 +175,6 @@ export const makeLeaderSyncProcessor = ({
|
|
|
134
175
|
current: undefined as
|
|
135
176
|
| undefined
|
|
136
177
|
| {
|
|
137
|
-
otelSpan: otel.Span | undefined
|
|
138
178
|
span: Tracer.Span
|
|
139
179
|
devtoolsLatch: Effect.Latch | undefined
|
|
140
180
|
runtime: Runtime.Runtime<LeaderThreadCtx>
|
|
@@ -142,8 +182,8 @@ export const makeLeaderSyncProcessor = ({
|
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
|
|
145
|
-
|
|
146
|
-
const
|
|
185
|
+
// Ensures mutual exclusion between local push and backend pull processing.
|
|
186
|
+
const localPushBackendPullMutex = yield* Effect.makeSemaphore(1)
|
|
147
187
|
|
|
148
188
|
/**
|
|
149
189
|
* Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
|
|
@@ -154,9 +194,9 @@ export const makeLeaderSyncProcessor = ({
|
|
|
154
194
|
*
|
|
155
195
|
* Thus the purpose of the pushHeadRef is the guard the integrity of the local push queue
|
|
156
196
|
*/
|
|
157
|
-
const pushHeadRef = { current: EventSequenceNumber.ROOT }
|
|
158
|
-
const advancePushHead = (eventNum: EventSequenceNumber.
|
|
159
|
-
pushHeadRef.current = EventSequenceNumber.max(pushHeadRef.current, eventNum)
|
|
197
|
+
const pushHeadRef = { current: EventSequenceNumber.Client.ROOT }
|
|
198
|
+
const advancePushHead = (eventNum: EventSequenceNumber.Client.Composite) => {
|
|
199
|
+
pushHeadRef.current = EventSequenceNumber.Client.max(pushHeadRef.current, eventNum)
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
// NOTE: New events are only pushed to sync backend after successful local push processing
|
|
@@ -172,8 +212,8 @@ export const makeLeaderSyncProcessor = ({
|
|
|
172
212
|
|
|
173
213
|
const waitForProcessing = options?.waitForProcessing ?? false
|
|
174
214
|
|
|
175
|
-
if (waitForProcessing) {
|
|
176
|
-
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
|
|
215
|
+
if (waitForProcessing === true) {
|
|
216
|
+
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError | StaleRebaseGenerationError>())
|
|
177
217
|
|
|
178
218
|
const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
|
|
179
219
|
|
|
@@ -188,41 +228,62 @@ export const makeLeaderSyncProcessor = ({
|
|
|
188
228
|
Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
|
|
189
229
|
attributes: {
|
|
190
230
|
batchSize: newEvents.length,
|
|
191
|
-
batch: TRACE_VERBOSE ? newEvents : undefined,
|
|
231
|
+
batch: TRACE_VERBOSE === true ? newEvents : undefined,
|
|
192
232
|
},
|
|
193
|
-
links:
|
|
233
|
+
links:
|
|
234
|
+
ctxRef.current?.span !== undefined
|
|
235
|
+
? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }]
|
|
236
|
+
: undefined,
|
|
194
237
|
}),
|
|
195
238
|
)
|
|
196
239
|
|
|
197
240
|
const pushPartial: LeaderSyncProcessor['pushPartial'] = ({ event: { name, args }, clientId, sessionId }) =>
|
|
198
241
|
Effect.gen(function* () {
|
|
199
|
-
const syncState = yield* syncStateSref
|
|
200
|
-
|
|
242
|
+
const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
|
|
243
|
+
|
|
244
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
245
|
+
operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
|
|
246
|
+
event: {
|
|
247
|
+
name,
|
|
248
|
+
args,
|
|
249
|
+
clientId,
|
|
250
|
+
sessionId,
|
|
251
|
+
seqNum: syncState.localHead,
|
|
252
|
+
},
|
|
253
|
+
})
|
|
201
254
|
|
|
202
|
-
|
|
255
|
+
if (resolution._tag === 'unknown') {
|
|
256
|
+
// Ignore partial pushes for unrecognised events – they are still
|
|
257
|
+
// persisted server-side once a schema update ships.
|
|
258
|
+
return
|
|
259
|
+
}
|
|
203
260
|
|
|
204
|
-
const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
|
|
261
|
+
const eventEncoded = new LiveStoreEvent.Client.EncodedWithMeta({
|
|
205
262
|
name,
|
|
206
263
|
args,
|
|
207
264
|
clientId,
|
|
208
265
|
sessionId,
|
|
209
|
-
...EventSequenceNumber.nextPair({
|
|
266
|
+
...EventSequenceNumber.Client.nextPair({
|
|
267
|
+
seqNum: syncState.localHead,
|
|
268
|
+
isClient: resolution.eventDef.options.clientOnly,
|
|
269
|
+
}),
|
|
210
270
|
})
|
|
211
271
|
|
|
212
272
|
yield* push([eventEncoded])
|
|
213
|
-
}).pipe(
|
|
273
|
+
}).pipe(
|
|
274
|
+
// pushPartial constructs the event sequence number internally, so these errors should never happen.
|
|
275
|
+
Effect.catchIf(isRejectedPushError, Effect.die),
|
|
276
|
+
)
|
|
214
277
|
|
|
215
278
|
// Starts various background loops
|
|
216
279
|
const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
|
|
217
280
|
const span = yield* Effect.currentSpan.pipe(Effect.orDie)
|
|
218
|
-
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
|
219
281
|
const { devtools, shutdownChannel } = yield* LeaderThreadCtx
|
|
220
282
|
const runtime = yield* Effect.runtime<LeaderThreadCtx>()
|
|
221
283
|
|
|
222
284
|
ctxRef.current = {
|
|
223
|
-
otelSpan,
|
|
224
285
|
span,
|
|
225
|
-
devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
|
|
286
|
+
devtoolsLatch: devtools.enabled === true ? devtools.syncBackendLatch : undefined,
|
|
226
287
|
runtime,
|
|
227
288
|
}
|
|
228
289
|
|
|
@@ -232,10 +293,10 @@ export const makeLeaderSyncProcessor = ({
|
|
|
232
293
|
// Rehydrate sync queue
|
|
233
294
|
if (initialSyncState.pending.length > 0) {
|
|
234
295
|
const globalPendingEvents = initialSyncState.pending
|
|
235
|
-
// Don't sync
|
|
296
|
+
// Don't sync client-local events
|
|
236
297
|
.filter((eventEncoded) => {
|
|
237
|
-
const
|
|
238
|
-
return eventDef.options.clientOnly === false
|
|
298
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
299
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
239
300
|
})
|
|
240
301
|
|
|
241
302
|
if (globalPendingEvents.length > 0) {
|
|
@@ -243,19 +304,18 @@ export const makeLeaderSyncProcessor = ({
|
|
|
243
304
|
}
|
|
244
305
|
}
|
|
245
306
|
|
|
307
|
+
const handleBackendIdMismatchError = (error: BackendIdMismatchError) =>
|
|
308
|
+
handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel })
|
|
309
|
+
|
|
246
310
|
const maybeShutdownOnError = (
|
|
247
311
|
cause: Cause.Cause<
|
|
248
|
-
|
|
|
249
|
-
| IntentionalShutdownCause
|
|
250
|
-
| IsOfflineError
|
|
251
|
-
| InvalidPushError
|
|
252
|
-
| InvalidPullError
|
|
312
|
+
| UnknownError
|
|
253
313
|
| MaterializeError
|
|
254
314
|
>,
|
|
255
315
|
) =>
|
|
256
316
|
Effect.gen(function* () {
|
|
257
317
|
if (onError === 'ignore') {
|
|
258
|
-
if (LS_DEV) {
|
|
318
|
+
if (LS_DEV === true) {
|
|
259
319
|
yield* Effect.logDebug(
|
|
260
320
|
`Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
|
|
261
321
|
Cause.pretty(cause),
|
|
@@ -264,35 +324,38 @@ export const makeLeaderSyncProcessor = ({
|
|
|
264
324
|
return
|
|
265
325
|
}
|
|
266
326
|
|
|
267
|
-
const errorToSend = Cause.isFailType(cause) ? cause.error :
|
|
327
|
+
const errorToSend = Cause.isFailType(cause) === true ? cause.error : UnknownError.make({ cause })
|
|
268
328
|
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
|
269
329
|
|
|
270
|
-
return yield* Effect.
|
|
330
|
+
return yield* Effect.failCause(cause).pipe(Effect.orDie)
|
|
271
331
|
})
|
|
272
332
|
|
|
273
333
|
yield* backgroundApplyLocalPushes({
|
|
274
|
-
|
|
334
|
+
localPushBackendPullMutex,
|
|
275
335
|
localPushesQueue,
|
|
276
|
-
pullLatch,
|
|
277
336
|
syncStateSref,
|
|
278
337
|
syncBackendPushQueue,
|
|
279
338
|
schema,
|
|
280
339
|
isClientEvent,
|
|
281
|
-
otelSpan,
|
|
282
340
|
connectedClientSessionPullQueues,
|
|
283
341
|
localPushBatchSize,
|
|
284
342
|
testing: {
|
|
285
343
|
delay: testing?.delays?.localPushProcessing,
|
|
286
344
|
},
|
|
287
|
-
}).pipe(
|
|
345
|
+
}).pipe(
|
|
346
|
+
Effect.catchAllCause(maybeShutdownOnError),
|
|
347
|
+
Effect.forkScoped,
|
|
348
|
+
)
|
|
288
349
|
|
|
289
350
|
const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
|
|
290
351
|
const backendPushingEffect = backgroundBackendPushing({
|
|
291
352
|
syncBackendPushQueue,
|
|
292
|
-
otelSpan,
|
|
293
353
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
294
354
|
backendPushBatchSize,
|
|
295
|
-
}).pipe(
|
|
355
|
+
}).pipe(
|
|
356
|
+
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
357
|
+
Effect.catchAllCause(maybeShutdownOnError),
|
|
358
|
+
)
|
|
296
359
|
|
|
297
360
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
298
361
|
|
|
@@ -311,20 +374,21 @@ export const makeLeaderSyncProcessor = ({
|
|
|
311
374
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
312
375
|
}),
|
|
313
376
|
syncStateSref,
|
|
314
|
-
|
|
315
|
-
pullLatch,
|
|
377
|
+
localPushBackendPullMutex,
|
|
316
378
|
livePull,
|
|
317
379
|
dbState,
|
|
318
|
-
otelSpan,
|
|
319
380
|
initialBlockingSyncContext,
|
|
320
381
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
321
382
|
connectedClientSessionPullQueues,
|
|
322
383
|
advancePushHead,
|
|
323
384
|
}).pipe(
|
|
324
385
|
Effect.retry({
|
|
325
|
-
//
|
|
326
|
-
while
|
|
386
|
+
// Retry pulling when we've lost connection to the sync backend
|
|
387
|
+
// We're using `until` with a refinement instead of `while` to narrow `IsOfflineError` out of the error type.
|
|
388
|
+
// See https://github.com/Effect-TS/effect/issues/6122
|
|
389
|
+
until: (error): error is Exclude<typeof error, IsOfflineError> => error._tag !== 'IsOfflineError',
|
|
327
390
|
}),
|
|
391
|
+
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
328
392
|
Effect.catchAllCause(maybeShutdownOnError),
|
|
329
393
|
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
|
330
394
|
// This might be a bug in Effect. Only seems to happen in the browser.
|
|
@@ -355,17 +419,16 @@ export const makeLeaderSyncProcessor = ({
|
|
|
355
419
|
- full new state db snapshot in the "rebase" case
|
|
356
420
|
- downside: importing the snapshot is expensive
|
|
357
421
|
*/
|
|
358
|
-
const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) =>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
422
|
+
const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) =>
|
|
423
|
+
Effect.fromNullable(ctxRef.current?.runtime).pipe(
|
|
424
|
+
Effect.orDieDebugger,
|
|
425
|
+
Effect.flatMap((runtime) =>
|
|
426
|
+
connectedClientSessionPullQueues.makeQueue(cursor).pipe(Effect.provide(runtime))
|
|
427
|
+
)
|
|
428
|
+
)
|
|
362
429
|
|
|
363
430
|
const syncState = Subscribable.make({
|
|
364
|
-
get: Effect.
|
|
365
|
-
const syncState = yield* syncStateSref
|
|
366
|
-
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
367
|
-
return syncState
|
|
368
|
-
}),
|
|
431
|
+
get: syncStateSref.pipe(Effect.flatMap(Effect.fromNullable), Effect.orDieDebugger),
|
|
369
432
|
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
|
370
433
|
})
|
|
371
434
|
|
|
@@ -380,26 +443,22 @@ export const makeLeaderSyncProcessor = ({
|
|
|
380
443
|
})
|
|
381
444
|
|
|
382
445
|
const backgroundApplyLocalPushes = ({
|
|
383
|
-
|
|
446
|
+
localPushBackendPullMutex,
|
|
384
447
|
localPushesQueue,
|
|
385
|
-
pullLatch,
|
|
386
448
|
syncStateSref,
|
|
387
449
|
syncBackendPushQueue,
|
|
388
450
|
schema,
|
|
389
451
|
isClientEvent,
|
|
390
|
-
otelSpan,
|
|
391
452
|
connectedClientSessionPullQueues,
|
|
392
453
|
localPushBatchSize,
|
|
393
454
|
testing,
|
|
394
455
|
}: {
|
|
395
|
-
|
|
396
|
-
localPushesLatch: Effect.Latch
|
|
456
|
+
localPushBackendPullMutex: Effect.Semaphore
|
|
397
457
|
localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
|
|
398
458
|
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
|
399
|
-
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
|
|
459
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
|
|
400
460
|
schema: LiveStoreSchema
|
|
401
|
-
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
|
402
|
-
otelSpan: otel.Span | undefined
|
|
461
|
+
isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
|
|
403
462
|
connectedClientSessionPullQueues: PullQueueSet
|
|
404
463
|
localPushBatchSize: number
|
|
405
464
|
testing: {
|
|
@@ -414,136 +473,149 @@ const backgroundApplyLocalPushes = ({
|
|
|
414
473
|
|
|
415
474
|
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize)
|
|
416
475
|
|
|
417
|
-
//
|
|
418
|
-
yield*
|
|
419
|
-
|
|
420
|
-
// Prevent backend pull processing until this local push is finished
|
|
421
|
-
yield* pullLatch.close
|
|
422
|
-
|
|
423
|
-
const syncState = yield* syncStateSref
|
|
424
|
-
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
476
|
+
// Applies a batch of local pushes, guarded by the localPushBackendPullMutex to ensure mutual exclusion with backend pulling
|
|
477
|
+
yield* Effect.gen(function* () {
|
|
478
|
+
const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
|
|
425
479
|
|
|
426
|
-
|
|
480
|
+
const currentRebaseGeneration = syncState.localHead.rebaseGeneration
|
|
427
481
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
if (newEvents.length === 0) {
|
|
437
|
-
// console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
|
|
438
|
-
// Allow the backend pulling to start
|
|
439
|
-
yield* pullLatch.open
|
|
440
|
-
continue
|
|
441
|
-
}
|
|
482
|
+
// Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
|
|
483
|
+
// It's important that we filter after acquiring the localPushBackendPullMutex, otherwise we might filter with the old generation
|
|
484
|
+
const [droppedItems, filteredItems] = ReadonlyArray.partition(
|
|
485
|
+
batchItems,
|
|
486
|
+
([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= currentRebaseGeneration,
|
|
487
|
+
)
|
|
442
488
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
})
|
|
489
|
+
if (droppedItems.length > 0) {
|
|
490
|
+
yield* Effect.spanEvent(`push:drop-old-generation`, {
|
|
491
|
+
droppedCount: droppedItems.length,
|
|
492
|
+
currentRebaseGeneration,
|
|
493
|
+
})
|
|
449
494
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
495
|
+
/**
|
|
496
|
+
* Dropped pushes may still have a deferred awaiting completion.
|
|
497
|
+
* Fail it so the caller learns the leader advanced and resubmits with the updated generation.
|
|
498
|
+
*/
|
|
499
|
+
yield* Effect.forEach(
|
|
500
|
+
droppedItems.filter(
|
|
501
|
+
(item): item is [LiveStoreEvent.Client.EncodedWithMeta, Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError>] =>
|
|
502
|
+
item[1] !== undefined,
|
|
503
|
+
),
|
|
504
|
+
([eventEncoded, deferred]) =>
|
|
505
|
+
Deferred.fail(
|
|
506
|
+
deferred,
|
|
507
|
+
StaleRebaseGenerationError.make({
|
|
508
|
+
currentRebaseGeneration,
|
|
509
|
+
providedRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
|
510
|
+
sessionId: eventEncoded.sessionId,
|
|
511
|
+
}),
|
|
512
|
+
),
|
|
513
|
+
)
|
|
457
514
|
}
|
|
458
|
-
|
|
459
|
-
|
|
515
|
+
|
|
516
|
+
if (filteredItems.length === 0) {
|
|
517
|
+
return
|
|
460
518
|
}
|
|
461
|
-
case 'reject': {
|
|
462
|
-
otelSpan?.addEvent(`push:reject`, {
|
|
463
|
-
batchSize: newEvents.length,
|
|
464
|
-
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
465
|
-
})
|
|
466
519
|
|
|
467
|
-
|
|
468
|
-
const nextRebaseGeneration = currentRebaseGeneration + 1
|
|
520
|
+
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredItems)
|
|
469
521
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
522
|
+
yield* Effect.annotateCurrentSpan({
|
|
523
|
+
'batchSize': newEvents.length,
|
|
524
|
+
...(TRACE_VERBOSE === true ? { 'newEvents': jsonStringify(newEvents) } : {}),
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
const mergeResult = yield* SyncState.merge({
|
|
528
|
+
syncState,
|
|
529
|
+
payload: { _tag: 'local-push', newEvents },
|
|
530
|
+
isClientEvent,
|
|
531
|
+
isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
|
|
532
|
+
})
|
|
478
533
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// biome-ignore lint/suspicious/noDebugger: debugging
|
|
483
|
-
debugger
|
|
534
|
+
switch (mergeResult._tag) {
|
|
535
|
+
case 'rebase': {
|
|
536
|
+
return yield* Effect.dieDebugger('The leader thread should never have to rebase due to a local push')
|
|
484
537
|
}
|
|
538
|
+
case 'reject': {
|
|
539
|
+
yield* Effect.spanEvent(`push:reject`, {
|
|
540
|
+
batchSize: newEvents.length,
|
|
541
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
542
|
+
})
|
|
485
543
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
|
|
489
|
-
].filter(isNotUndefined)
|
|
544
|
+
// TODO: how to test this?
|
|
545
|
+
const nextRebaseGeneration = currentRebaseGeneration + 1
|
|
490
546
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
547
|
+
const providedNum = newEvents.at(0)!.seqNum
|
|
548
|
+
// All subsequent pushes with same generation should be rejected as well
|
|
549
|
+
// We're also handling the case where the localPushQueue already contains events
|
|
550
|
+
// from the next generation which we preserve in the queue
|
|
551
|
+
const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
|
|
552
|
+
localPushesQueue,
|
|
553
|
+
([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
|
|
554
|
+
)
|
|
497
555
|
|
|
498
|
-
|
|
499
|
-
|
|
556
|
+
// TODO we still need to better understand and handle this scenario
|
|
557
|
+
if (LS_DEV === true && (yield* BucketQueue.size(localPushesQueue)) > 0) {
|
|
558
|
+
console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
|
|
559
|
+
// oxlint-disable-next-line eslint(no-debugger) -- intentional breakpoint for unexpected queue state
|
|
560
|
+
debugger
|
|
561
|
+
}
|
|
500
562
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
case 'advance': {
|
|
506
|
-
break
|
|
507
|
-
}
|
|
508
|
-
default: {
|
|
509
|
-
casesHandled(mergeResult)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
563
|
+
const allDeferredsToReject = [
|
|
564
|
+
...deferreds,
|
|
565
|
+
...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
|
|
566
|
+
].filter(isNotUndefined)
|
|
512
567
|
|
|
513
|
-
|
|
568
|
+
yield* Effect.forEach(allDeferredsToReject, (deferred) =>
|
|
569
|
+
Deferred.fail(
|
|
570
|
+
deferred,
|
|
571
|
+
LeaderAheadError.make({ minimumExpectedNum: mergeResult.expectedMinimumId, providedNum, sessionId: newEvents.at(0)!.sessionId }),
|
|
572
|
+
),
|
|
573
|
+
)
|
|
514
574
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
575
|
+
// In this case we're skipping state update and down/upstream processing
|
|
576
|
+
// We've cleared the local push queue and are now waiting for new local pushes / backend pulls
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
case 'advance': {
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
default: {
|
|
583
|
+
casesHandled(mergeResult)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
519
586
|
|
|
520
|
-
|
|
521
|
-
batchSize: newEvents.length,
|
|
522
|
-
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
523
|
-
})
|
|
587
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
524
588
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
})
|
|
589
|
+
yield* connectedClientSessionPullQueues.offer({
|
|
590
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
|
591
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
|
592
|
+
})
|
|
530
593
|
|
|
531
|
-
|
|
594
|
+
yield* Effect.spanEvent(`push:advance`, {
|
|
595
|
+
batchSize: newEvents.length,
|
|
596
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
597
|
+
})
|
|
532
598
|
|
|
533
|
-
|
|
599
|
+
// Don't sync client-local events
|
|
600
|
+
const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
|
|
601
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
602
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
603
|
+
})
|
|
534
604
|
|
|
535
|
-
|
|
536
|
-
|
|
605
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
|
|
606
|
+
|
|
607
|
+
yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds })
|
|
608
|
+
}).pipe(localPushBackendPullMutex.withPermits(1))
|
|
537
609
|
}
|
|
538
610
|
})
|
|
539
611
|
|
|
540
612
|
type MaterializeEventsBatch = (_: {
|
|
541
|
-
batchItems: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>
|
|
613
|
+
batchItems: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>
|
|
542
614
|
/**
|
|
543
615
|
* The deferreds are used by the caller to know when the mutation has been processed.
|
|
544
616
|
* Indexes are aligned with `batchItems`
|
|
545
617
|
*/
|
|
546
|
-
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
|
618
|
+
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError> | undefined> | undefined
|
|
547
619
|
}) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
|
|
548
620
|
|
|
549
621
|
// TODO how to handle errors gracefully
|
|
@@ -557,7 +629,7 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
|
|
|
557
629
|
|
|
558
630
|
yield* Effect.addFinalizer((exit) =>
|
|
559
631
|
Effect.gen(function* () {
|
|
560
|
-
if (Exit.isSuccess(exit)) return
|
|
632
|
+
if (Exit.isSuccess(exit) === true) return
|
|
561
633
|
|
|
562
634
|
// Rollback in case of an error
|
|
563
635
|
db.execute('ROLLBACK', undefined)
|
|
@@ -586,90 +658,99 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
|
|
|
586
658
|
Effect.tapCauseLogPretty,
|
|
587
659
|
)
|
|
588
660
|
|
|
589
|
-
const backgroundBackendPulling = ({
|
|
661
|
+
const backgroundBackendPulling = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pulling')(function* ({
|
|
590
662
|
isClientEvent,
|
|
591
663
|
restartBackendPushing,
|
|
592
|
-
otelSpan,
|
|
593
664
|
dbState,
|
|
594
665
|
syncStateSref,
|
|
595
|
-
|
|
666
|
+
localPushBackendPullMutex,
|
|
596
667
|
livePull,
|
|
597
|
-
pullLatch,
|
|
598
668
|
devtoolsLatch,
|
|
599
669
|
initialBlockingSyncContext,
|
|
600
670
|
connectedClientSessionPullQueues,
|
|
601
671
|
advancePushHead,
|
|
602
672
|
}: {
|
|
603
|
-
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
|
673
|
+
isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
|
|
604
674
|
restartBackendPushing: (
|
|
605
|
-
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
|
606
|
-
) => Effect.Effect<void,
|
|
607
|
-
otelSpan: otel.Span | undefined
|
|
675
|
+
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
676
|
+
) => Effect.Effect<void, never, LeaderThreadCtx | HttpClient.HttpClient>
|
|
608
677
|
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
|
609
678
|
dbState: SqliteDb
|
|
610
|
-
|
|
611
|
-
pullLatch: Effect.Latch
|
|
679
|
+
localPushBackendPullMutex: Effect.Semaphore
|
|
612
680
|
livePull: boolean
|
|
613
681
|
devtoolsLatch: Effect.Latch | undefined
|
|
614
682
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
615
683
|
connectedClientSessionPullQueues: PullQueueSet
|
|
616
|
-
advancePushHead: (eventNum: EventSequenceNumber.
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
|
|
684
|
+
advancePushHead: (eventNum: EventSequenceNumber.Client.Composite) => void
|
|
685
|
+
}) {
|
|
686
|
+
const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
|
|
620
687
|
|
|
621
|
-
|
|
688
|
+
if (syncBackend === undefined) return
|
|
622
689
|
|
|
623
|
-
|
|
624
|
-
Effect.gen(function* () {
|
|
625
|
-
if (newEvents.length === 0) return
|
|
690
|
+
let pullMutexHeld = false
|
|
626
691
|
|
|
627
|
-
|
|
628
|
-
|
|
692
|
+
const releasePullMutexIfHeld = Effect.gen(function* () {
|
|
693
|
+
if (pullMutexHeld === false) return
|
|
694
|
+
pullMutexHeld = false
|
|
695
|
+
yield* localPushBackendPullMutex.release(1)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
const isPullPaginationComplete = (pageInfo: SyncBackend.PullResPageInfo) => pageInfo._tag === 'NoMore'
|
|
699
|
+
|
|
700
|
+
const onNewPullChunk = (newEvents: LiveStoreEvent.Client.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
|
|
701
|
+
Effect.gen(function* () {
|
|
702
|
+
if (devtoolsLatch !== undefined) {
|
|
703
|
+
yield* devtoolsLatch.await
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (newEvents.length === 0) {
|
|
707
|
+
if (isPullPaginationComplete(pageInfo) === true) {
|
|
708
|
+
yield* releasePullMutexIfHeld
|
|
629
709
|
}
|
|
710
|
+
return
|
|
711
|
+
}
|
|
630
712
|
|
|
631
|
-
|
|
632
|
-
|
|
713
|
+
// Prevent more local pushes from being processed until this pull pagination sequence is finished.
|
|
714
|
+
if (pullMutexHeld === false) {
|
|
715
|
+
yield* localPushBackendPullMutex.take(1)
|
|
716
|
+
pullMutexHeld = true
|
|
717
|
+
}
|
|
633
718
|
|
|
634
|
-
|
|
635
|
-
yield*
|
|
719
|
+
const chunkExit = yield* Effect.gen(function* () {
|
|
720
|
+
const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
|
|
636
721
|
|
|
637
|
-
|
|
638
|
-
|
|
722
|
+
yield* Effect.annotateCurrentSpan({
|
|
723
|
+
'merge.newEventsCount': newEvents.length,
|
|
724
|
+
...(TRACE_VERBOSE === true ? { 'merge.newEvents': jsonStringify(newEvents) } : {}),
|
|
725
|
+
})
|
|
639
726
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
727
|
+
const mergeResult = yield* SyncState.merge({
|
|
728
|
+
syncState,
|
|
729
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
|
|
730
|
+
isClientEvent,
|
|
731
|
+
isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
|
|
732
|
+
ignoreClientEvents: true,
|
|
733
|
+
})
|
|
647
734
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
otelSpan?.addEvent(`pull:unexpected-error`, {
|
|
652
|
-
newEventsCount: newEvents.length,
|
|
653
|
-
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
654
|
-
})
|
|
655
|
-
return yield* new UnexpectedError({ cause: mergeResult.message })
|
|
656
|
-
}
|
|
735
|
+
if (mergeResult._tag === 'reject') {
|
|
736
|
+
return yield* Effect.dieDebugger('The leader thread should never reject upstream advances')
|
|
737
|
+
}
|
|
657
738
|
|
|
658
739
|
const newBackendHead = newEvents.at(-1)!.seqNum
|
|
659
740
|
|
|
660
741
|
Eventlog.updateBackendHead(dbEventlog, newBackendHead)
|
|
661
742
|
|
|
662
743
|
if (mergeResult._tag === 'rebase') {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
744
|
+
yield* Effect.spanEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
|
|
745
|
+
newEventsCount: newEvents.length,
|
|
746
|
+
...(TRACE_VERBOSE === true ? { newEvents: jsonStringify(newEvents) } : {}),
|
|
747
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
|
748
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
668
749
|
})
|
|
669
750
|
|
|
670
751
|
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
671
|
-
const
|
|
672
|
-
return eventDef.options.clientOnly === false
|
|
752
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
|
753
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
673
754
|
})
|
|
674
755
|
yield* restartBackendPushing(globalRebasedPendingEvents)
|
|
675
756
|
|
|
@@ -686,15 +767,15 @@ const backgroundBackendPulling = ({
|
|
|
686
767
|
leaderHead: mergeResult.newSyncState.localHead,
|
|
687
768
|
})
|
|
688
769
|
} else {
|
|
689
|
-
|
|
770
|
+
yield* Effect.spanEvent(`pull:advance`, {
|
|
690
771
|
newEventsCount: newEvents.length,
|
|
691
|
-
|
|
772
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
692
773
|
})
|
|
693
774
|
|
|
694
775
|
// Ensure push fiber is active after advance by restarting with current pending (non-client) events
|
|
695
776
|
const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
696
|
-
const
|
|
697
|
-
return eventDef.options.clientOnly === false
|
|
777
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
|
778
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
698
779
|
})
|
|
699
780
|
yield* restartBackendPushing(globalPendingEvents)
|
|
700
781
|
|
|
@@ -708,10 +789,10 @@ const backgroundBackendPulling = ({
|
|
|
708
789
|
// `newEvents` instead which we filter via `mergeResult.confirmedEvents`
|
|
709
790
|
const confirmedNewEvents = newEvents.filter((event) =>
|
|
710
791
|
mergeResult.confirmedEvents.some((confirmedEvent) =>
|
|
711
|
-
EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
|
|
792
|
+
EventSequenceNumber.Client.isEqual(event.seqNum, confirmedEvent.seqNum),
|
|
712
793
|
),
|
|
713
794
|
)
|
|
714
|
-
yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(
|
|
795
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(Effect.orDieDebugger)
|
|
715
796
|
}
|
|
716
797
|
}
|
|
717
798
|
|
|
@@ -723,138 +804,128 @@ const backgroundBackendPulling = ({
|
|
|
723
804
|
yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined })
|
|
724
805
|
|
|
725
806
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
807
|
+
}).pipe(Effect.exit)
|
|
726
808
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
})
|
|
809
|
+
if (Exit.isFailure(chunkExit) === true) {
|
|
810
|
+
yield* releasePullMutexIfHeld
|
|
811
|
+
return yield* Effect.failCause(chunkExit.cause)
|
|
812
|
+
}
|
|
732
813
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
814
|
+
if (isPullPaginationComplete(pageInfo) === true) {
|
|
815
|
+
yield* releasePullMutexIfHeld
|
|
816
|
+
}
|
|
817
|
+
})
|
|
736
818
|
|
|
737
|
-
|
|
819
|
+
const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
|
|
820
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
|
|
738
821
|
|
|
739
|
-
|
|
740
|
-
// TODO only take from queue while connected
|
|
741
|
-
Stream.tap(({ batch, pageInfo }) =>
|
|
742
|
-
Effect.gen(function* () {
|
|
743
|
-
// yield* Effect.spanEvent('batch', {
|
|
744
|
-
// attributes: {
|
|
745
|
-
// batchSize: batch.length,
|
|
746
|
-
// batch: TRACE_VERBOSE ? batch : undefined,
|
|
747
|
-
// },
|
|
748
|
-
// })
|
|
749
|
-
// NOTE we only want to take process events when the sync backend is connected
|
|
750
|
-
// (e.g. needed for simulating being offline)
|
|
751
|
-
// TODO remove when there's a better way to handle this in stream above
|
|
752
|
-
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
753
|
-
yield* onNewPullChunk(
|
|
754
|
-
batch.map((_) =>
|
|
755
|
-
LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
|
756
|
-
syncMetadata: _.metadata,
|
|
757
|
-
// TODO we can't really know the materializer result here yet beyond the first event batch item as we need to materialize it one by one first
|
|
758
|
-
// This is a bug and needs to be fixed https://github.com/livestorejs/livestore/issues/503#issuecomment-3114533165
|
|
759
|
-
materializerHashLeader: hashMaterializerResult(LiveStoreEvent.encodedFromGlobal(_.eventEncoded)),
|
|
760
|
-
materializerHashSession: Option.none(),
|
|
761
|
-
}),
|
|
762
|
-
),
|
|
763
|
-
pageInfo,
|
|
764
|
-
)
|
|
765
|
-
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
|
766
|
-
}),
|
|
767
|
-
),
|
|
768
|
-
Stream.runDrain,
|
|
769
|
-
Effect.interruptible,
|
|
770
|
-
)
|
|
822
|
+
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
|
771
823
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
824
|
+
yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
|
|
825
|
+
// TODO only take from queue while connected
|
|
826
|
+
Stream.tap(({ batch, pageInfo }) =>
|
|
827
|
+
Effect.gen(function* () {
|
|
828
|
+
// NOTE we only want to take process events when the sync backend is connected
|
|
829
|
+
// (e.g. needed for simulating being offline)
|
|
830
|
+
// TODO remove when there's a better way to handle this in stream above
|
|
831
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
832
|
+
yield* onNewPullChunk(
|
|
833
|
+
batch.map((_) =>
|
|
834
|
+
LiveStoreEvent.Client.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
|
835
|
+
syncMetadata: _.metadata,
|
|
836
|
+
// TODO we can't really know the materializer result here yet beyond the first event batch item as we need to materialize it one by one first
|
|
837
|
+
// This is a bug and needs to be fixed https://github.com/livestorejs/livestore/issues/503#issuecomment-3114533165
|
|
838
|
+
materializerHashLeader: hashMaterializerResult(LiveStoreEvent.Global.toClientEncoded(_.eventEncoded)),
|
|
839
|
+
materializerHashSession: Option.none(),
|
|
840
|
+
}),
|
|
841
|
+
),
|
|
842
|
+
pageInfo,
|
|
843
|
+
)
|
|
844
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
|
845
|
+
}),
|
|
846
|
+
),
|
|
847
|
+
Stream.runDrain,
|
|
848
|
+
Effect.interruptible,
|
|
849
|
+
Effect.ensuring(releasePullMutexIfHeld),
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
// Should only ever happen when livePull is false
|
|
853
|
+
yield* Effect.logDebug('backend-pulling finished', { livePull })
|
|
854
|
+
})
|
|
775
855
|
|
|
776
|
-
const backgroundBackendPushing = ({
|
|
856
|
+
const backgroundBackendPushing = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pushing')(function* ({
|
|
777
857
|
syncBackendPushQueue,
|
|
778
|
-
otelSpan,
|
|
779
858
|
devtoolsLatch,
|
|
780
859
|
backendPushBatchSize,
|
|
781
860
|
}: {
|
|
782
|
-
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
|
|
783
|
-
otelSpan: otel.Span | undefined
|
|
861
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
|
|
784
862
|
devtoolsLatch: Effect.Latch | undefined
|
|
785
863
|
backendPushBatchSize: number
|
|
786
|
-
})
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (syncBackend === undefined) return
|
|
864
|
+
}) {
|
|
865
|
+
const { syncBackend } = yield* LeaderThreadCtx
|
|
866
|
+
if (syncBackend === undefined) return
|
|
790
867
|
|
|
791
|
-
|
|
792
|
-
|
|
868
|
+
while (true) {
|
|
869
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
793
870
|
|
|
794
|
-
|
|
871
|
+
const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
|
|
795
872
|
|
|
796
|
-
|
|
873
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
797
874
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
875
|
+
if (devtoolsLatch !== undefined) {
|
|
876
|
+
yield* devtoolsLatch.await
|
|
877
|
+
}
|
|
801
878
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
879
|
+
yield* Effect.spanEvent('backend-push', {
|
|
880
|
+
batchSize: queueItems.length,
|
|
881
|
+
...(TRACE_VERBOSE === true ? { batch: jsonStringify(queueItems) } : {}),
|
|
882
|
+
})
|
|
806
883
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const isRetryable = (err: InvalidPushError | IsOfflineError) =>
|
|
815
|
-
err._tag === 'InvalidPushError' && err.cause._tag === 'LiveStore.UnexpectedError'
|
|
816
|
-
|
|
817
|
-
// Input: InvalidPushError | IsOfflineError, Output: Duration
|
|
818
|
-
const retrySchedule: Schedule.Schedule<Duration.DurationInput, InvalidPushError | IsOfflineError> =
|
|
819
|
-
Schedule.exponential(Duration.seconds(1)).pipe(
|
|
820
|
-
Schedule.andThenEither(Schedule.spaced(Duration.seconds(30))), // clamp at 30 second intervals
|
|
821
|
-
Schedule.compose(Schedule.elapsed),
|
|
822
|
-
Schedule.whileInput(isRetryable),
|
|
823
|
-
)
|
|
884
|
+
// Push with declarative retry/backoff using Effect schedules
|
|
885
|
+
// - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
|
|
886
|
+
// - Delay clamped at 30s (continues retrying at 30s)
|
|
887
|
+
// - Resets automatically after successful push
|
|
888
|
+
// TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
|
|
889
|
+
yield* Effect.gen(function* () {
|
|
890
|
+
const iteration = yield* Schedule.CurrentIterationMetadata
|
|
824
891
|
|
|
825
|
-
yield*
|
|
826
|
-
const iteration = yield* Schedule.CurrentIterationMetadata
|
|
892
|
+
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
|
|
827
893
|
|
|
828
|
-
|
|
894
|
+
const retries = iteration.recurrence
|
|
895
|
+
if (retries > 0 && pushResult._tag === 'Right') {
|
|
896
|
+
yield* Effect.spanEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
|
|
897
|
+
}
|
|
829
898
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
899
|
+
if (pushResult._tag === 'Left') {
|
|
900
|
+
yield* Effect.spanEvent('backend-push-error', {
|
|
901
|
+
error: pushResult.left.toString(),
|
|
902
|
+
retries,
|
|
903
|
+
batchSize: queueItems.length,
|
|
904
|
+
})
|
|
905
|
+
const error = pushResult.left
|
|
906
|
+
if (error._tag === 'ServerAheadError') {
|
|
907
|
+
// It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
|
|
908
|
+
yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
|
|
909
|
+
return yield* Effect.never
|
|
833
910
|
}
|
|
834
911
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return yield* error
|
|
852
|
-
}
|
|
853
|
-
}).pipe(Effect.retry(retrySchedule))
|
|
854
|
-
}
|
|
855
|
-
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
|
|
912
|
+
return yield* error
|
|
913
|
+
}
|
|
914
|
+
}).pipe(
|
|
915
|
+
// Retry transient errors
|
|
916
|
+
Effect.retry({
|
|
917
|
+
schedule: Schedule.exponential(Duration.seconds(1)).pipe(
|
|
918
|
+
Schedule.modifyDelay((_, delay) => Duration.min(delay, Duration.seconds(30))) // Cap delay at 30s intervals.
|
|
919
|
+
),
|
|
920
|
+
while: (error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError',
|
|
921
|
+
}),
|
|
922
|
+
// This is needed to narrow the Error type. Our retry policy runs indefinitely, but Effect.retry does not narrow the Error type.
|
|
923
|
+
Effect.catchIf((error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError', Effect.die),
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
}, Effect.interruptible)
|
|
856
927
|
|
|
857
|
-
const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.
|
|
928
|
+
const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Composite) => {
|
|
858
929
|
// Since we're using the session changeset rows to query for the current head,
|
|
859
930
|
// we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
|
|
860
931
|
db.execute(sql`DELETE FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE seqNumGlobal < ${newHead.global}`)
|
|
@@ -862,16 +933,16 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSeque
|
|
|
862
933
|
|
|
863
934
|
interface PullQueueSet {
|
|
864
935
|
makeQueue: (
|
|
865
|
-
cursor: EventSequenceNumber.
|
|
936
|
+
cursor: EventSequenceNumber.Client.Composite,
|
|
866
937
|
) => Effect.Effect<
|
|
867
938
|
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
|
|
868
|
-
|
|
939
|
+
never,
|
|
869
940
|
Scope.Scope | LeaderThreadCtx
|
|
870
941
|
>
|
|
871
942
|
offer: (item: {
|
|
872
943
|
payload: typeof SyncState.PayloadUpstream.Type
|
|
873
|
-
leaderHead: EventSequenceNumber.
|
|
874
|
-
}) => Effect.Effect<void,
|
|
944
|
+
leaderHead: EventSequenceNumber.Client.Composite
|
|
945
|
+
}) => Effect.Effect<void, never>
|
|
875
946
|
}
|
|
876
947
|
|
|
877
948
|
const makePullQueueSet = Effect.gen(function* () {
|
|
@@ -901,17 +972,17 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
901
972
|
|
|
902
973
|
const payloadsSinceCursor = Array.from(cachedPayloads.entries())
|
|
903
974
|
.flatMap(([seqNumStr, payloads]) =>
|
|
904
|
-
payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.fromString(seqNumStr) })),
|
|
975
|
+
payloads.map((payload) => ({ payload, seqNum: EventSequenceNumber.Client.fromString(seqNumStr) })),
|
|
905
976
|
)
|
|
906
|
-
.filter(({ seqNum }) => EventSequenceNumber.isGreaterThan(seqNum, cursor))
|
|
907
|
-
.toSorted((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
|
|
977
|
+
.filter(({ seqNum }) => EventSequenceNumber.Client.isGreaterThan(seqNum, cursor))
|
|
978
|
+
.toSorted((a, b) => EventSequenceNumber.Client.compare(a.seqNum, b.seqNum))
|
|
908
979
|
.map(({ payload }) => {
|
|
909
980
|
if (payload._tag === 'upstream-advance') {
|
|
910
981
|
return {
|
|
911
982
|
payload: {
|
|
912
983
|
_tag: 'upstream-advance' as const,
|
|
913
984
|
newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
|
|
914
|
-
EventSequenceNumber.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
|
|
985
|
+
EventSequenceNumber.Client.isGreaterThanOrEqual(cursor, eventEncoded.seqNum),
|
|
915
986
|
),
|
|
916
987
|
},
|
|
917
988
|
}
|
|
@@ -957,8 +1028,8 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
957
1028
|
|
|
958
1029
|
const offer: PullQueueSet['offer'] = (item) =>
|
|
959
1030
|
Effect.gen(function* () {
|
|
960
|
-
const seqNumStr = EventSequenceNumber.toString(item.leaderHead)
|
|
961
|
-
if (cachedPayloads.has(seqNumStr)) {
|
|
1031
|
+
const seqNumStr = EventSequenceNumber.Client.toString(item.leaderHead)
|
|
1032
|
+
if (cachedPayloads.has(seqNumStr) === true) {
|
|
962
1033
|
cachedPayloads.get(seqNumStr)!.push(item.payload)
|
|
963
1034
|
} else {
|
|
964
1035
|
cachedPayloads.set(seqNumStr, [item.payload])
|
|
@@ -982,29 +1053,108 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
982
1053
|
}
|
|
983
1054
|
})
|
|
984
1055
|
|
|
1056
|
+
/**
|
|
1057
|
+
* Validate a client-provided batch before it is admitted to the leader queue.
|
|
1058
|
+
* Ensures the numbers form a strictly increasing chain and that the first
|
|
1059
|
+
* event sits ahead of the current push head.
|
|
1060
|
+
*/
|
|
985
1061
|
const validatePushBatch = (
|
|
986
|
-
batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
|
987
|
-
pushHead: EventSequenceNumber.
|
|
1062
|
+
batch: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
1063
|
+
pushHead: EventSequenceNumber.Client.Composite,
|
|
988
1064
|
) =>
|
|
989
1065
|
Effect.gen(function* () {
|
|
990
1066
|
if (batch.length === 0) {
|
|
991
1067
|
return
|
|
992
1068
|
}
|
|
993
1069
|
|
|
994
|
-
//
|
|
1070
|
+
// Defensive check: callers should already provide a strictly increasing sequence
|
|
1071
|
+
// of event numbers.
|
|
995
1072
|
for (let i = 1; i < batch.length; i++) {
|
|
996
|
-
if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1073
|
+
if (EventSequenceNumber.Client.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum) === true) {
|
|
1074
|
+
return yield* NonMonotonicBatchError.make({
|
|
1075
|
+
precedingSeqNum: batch[i - 1]!.seqNum,
|
|
1076
|
+
violatingSeqNum: batch[i]!.seqNum,
|
|
1077
|
+
violationIndex: i,
|
|
1078
|
+
sessionId: batch[i]!.sessionId,
|
|
1079
|
+
})
|
|
1000
1080
|
}
|
|
1001
1081
|
}
|
|
1002
1082
|
|
|
1003
|
-
//
|
|
1004
|
-
if (EventSequenceNumber.isGreaterThanOrEqual(pushHead, batch[0]!.seqNum)) {
|
|
1083
|
+
// Reject stale batches whose first event is at or behind the leader's push head.
|
|
1084
|
+
if (EventSequenceNumber.Client.isGreaterThanOrEqual(pushHead, batch[0]!.seqNum) === true) {
|
|
1005
1085
|
return yield* LeaderAheadError.make({
|
|
1006
1086
|
minimumExpectedNum: pushHead,
|
|
1007
1087
|
providedNum: batch[0]!.seqNum,
|
|
1088
|
+
sessionId: batch[0]!.sessionId,
|
|
1008
1089
|
})
|
|
1009
1090
|
}
|
|
1010
1091
|
})
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Handles a BackendIdMismatchError based on the configured behavior.
|
|
1095
|
+
* This occurs when the sync backend has been reset and has a new identity.
|
|
1096
|
+
*/
|
|
1097
|
+
const handleBackendIdMismatch = Effect.fn('@livestore/common:LeaderSyncProcessor:handleBackendIdMismatch')(function* ({
|
|
1098
|
+
error,
|
|
1099
|
+
onBackendIdMismatch,
|
|
1100
|
+
shutdownChannel,
|
|
1101
|
+
}: {
|
|
1102
|
+
error: BackendIdMismatchError
|
|
1103
|
+
onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
|
|
1104
|
+
shutdownChannel: ShutdownChannel
|
|
1105
|
+
}) {
|
|
1106
|
+
const { dbEventlog, dbState } = yield* LeaderThreadCtx
|
|
1107
|
+
|
|
1108
|
+
if (onBackendIdMismatch === 'reset') {
|
|
1109
|
+
yield* Effect.logWarning(
|
|
1110
|
+
'Sync backend identity changed (backend was reset). Clearing local storage and shutting down.',
|
|
1111
|
+
error,
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
// Clear local databases so the client can start fresh on next boot
|
|
1115
|
+
yield* clearLocalDatabases({ dbEventlog, dbState })
|
|
1116
|
+
|
|
1117
|
+
// Send shutdown signal with special reason
|
|
1118
|
+
yield* shutdownChannel.send(IntentionalShutdownCause.make({ reason: 'backend-id-mismatch' })).pipe(Effect.orDie)
|
|
1119
|
+
|
|
1120
|
+
return yield* Effect.die(error)
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (onBackendIdMismatch === 'shutdown') {
|
|
1124
|
+
yield* Effect.logWarning(
|
|
1125
|
+
'Sync backend identity changed (backend was reset). Shutting down without clearing local storage.',
|
|
1126
|
+
error,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
yield* shutdownChannel.send(error).pipe(Effect.orDie)
|
|
1130
|
+
|
|
1131
|
+
return yield* Effect.die(error)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ignore mode
|
|
1135
|
+
if (LS_DEV === true) {
|
|
1136
|
+
yield* Effect.logDebug(
|
|
1137
|
+
'Ignoring BackendIdMismatchError (sync backend was reset but client continues with stale data)',
|
|
1138
|
+
error,
|
|
1139
|
+
)
|
|
1140
|
+
}
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Clears local databases (eventlog and state) so the client can start fresh on next boot.
|
|
1145
|
+
* This is used when the sync backend identity has changed (i.e. backend was reset).
|
|
1146
|
+
*/
|
|
1147
|
+
const clearLocalDatabases = ({ dbEventlog, dbState }: { dbEventlog: SqliteDb; dbState: SqliteDb }) =>
|
|
1148
|
+
Effect.sync(() => {
|
|
1149
|
+
// Clear eventlog tables
|
|
1150
|
+
dbEventlog.execute(sql`DELETE FROM ${EVENTLOG_META_TABLE}`)
|
|
1151
|
+
dbEventlog.execute(sql`DELETE FROM ${SYNC_STATUS_TABLE}`)
|
|
1152
|
+
|
|
1153
|
+
// Drop all state tables - they'll be recreated on next boot
|
|
1154
|
+
const tables = dbState.select<{ name: string }>(
|
|
1155
|
+
sql`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,
|
|
1156
|
+
)
|
|
1157
|
+
for (const { name } of tables) {
|
|
1158
|
+
dbState.execute(`DROP TABLE IF EXISTS "${name}"`)
|
|
1159
|
+
}
|
|
1160
|
+
})
|