@livestore/common 0.4.0 → 0.5.0-dev.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/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/MaterializationJournal.d.ts +44 -0
- package/dist/MaterializationJournal.d.ts.map +1 -0
- package/dist/MaterializationJournal.js +112 -0
- package/dist/MaterializationJournal.js.map +1 -0
- package/dist/StateHead.d.ts +26 -0
- package/dist/StateHead.d.ts.map +1 -0
- package/dist/StateHead.js +50 -0
- package/dist/StateHead.js.map +1 -0
- package/dist/WorkerTransportError.d.ts +3 -3
- package/dist/WorkerTransportError.d.ts.map +1 -1
- package/dist/WorkerTransportError.js +3 -3
- package/dist/WorkerTransportError.js.map +1 -1
- package/dist/__tests__/fixture.d.ts +8 -8
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +4 -4
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/adapter-types.d.ts +33 -57
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +24 -41
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/bounded-collections.js.map +1 -1
- package/dist/debug-info.d.ts +56 -56
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +32 -44
- package/dist/debug-info.js.map +1 -1
- package/dist/defs.d.ts +9 -9
- package/dist/defs.js +2 -2
- package/dist/devtools/devtools-compatibility.test.js +2 -1
- package/dist/devtools/devtools-compatibility.test.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +522 -334
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-client-session.js +80 -78
- package/dist/devtools/devtools-messages-client-session.js.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +27 -27
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +6 -6
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +410 -348
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +105 -93
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/devtools/devtools-messages.js +3 -3
- package/dist/devtools/devtools-sessioninfo.d.ts +18 -19
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
- package/dist/devtools/devtools-sessioninfo.js +8 -5
- package/dist/devtools/devtools-sessioninfo.js.map +1 -1
- package/dist/devtools/mod.d.ts +4 -5
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +12 -8
- package/dist/devtools/mod.js.map +1 -1
- package/dist/errors.d.ts +45 -69
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +19 -17
- 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 +22 -19
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +87 -18
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +548 -480
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/RejectedPushError.d.ts +72 -41
- package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
- package/dist/leader-thread/RejectedPushError.js +25 -6
- package/dist/leader-thread/RejectedPushError.js.map +1 -1
- package/dist/leader-thread/connection.d.ts +1 -0
- package/dist/leader-thread/connection.d.ts.map +1 -1
- package/dist/leader-thread/connection.js +11 -10
- package/dist/leader-thread/connection.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +15 -5
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +19 -19
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +36 -20
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +15 -5
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +62 -58
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.js +9 -9
- package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts +2 -1
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +12 -8
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.js +9 -9
- package/dist/leader-thread/recreate-db.d.ts +5 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +34 -59
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -6
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -3
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/stream-events.d.ts +4 -7
- package/dist/leader-thread/stream-events.d.ts.map +1 -1
- package/dist/leader-thread/stream-events.js +16 -21
- package/dist/leader-thread/stream-events.js.map +1 -1
- package/dist/leader-thread/types.d.ts +38 -73
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js +10 -6
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/logging.d.ts +5 -30
- package/dist/logging.d.ts.map +1 -1
- package/dist/logging.js +1 -32
- package/dist/logging.js.map +1 -1
- package/dist/make-client-session.d.ts +2 -3
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +5 -5
- package/dist/make-client-session.js.map +1 -1
- package/dist/materializer-helper.d.ts +0 -1
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +9 -37
- package/dist/materializer-helper.js.map +1 -1
- package/dist/otel.d.ts +2 -3
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +2 -7
- package/dist/otel.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +3 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +42 -33
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef/define.d.ts +6 -6
- package/dist/schema/EventDef/define.d.ts.map +1 -1
- package/dist/schema/EventDef/define.js +5 -5
- package/dist/schema/EventDef/define.js.map +1 -1
- package/dist/schema/EventDef/define.test.d.ts +2 -0
- package/dist/schema/EventDef/define.test.d.ts.map +1 -0
- package/dist/schema/EventDef/define.test.js +23 -0
- package/dist/schema/EventDef/define.test.js.map +1 -0
- package/dist/schema/EventDef/deprecated.d.ts +8 -13
- package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
- package/dist/schema/EventDef/deprecated.js +12 -13
- package/dist/schema/EventDef/deprecated.js.map +1 -1
- package/dist/schema/EventDef/deprecated.test.js +3 -3
- package/dist/schema/EventDef/deprecated.test.js.map +1 -1
- package/dist/schema/EventDef/event-def.d.ts +1 -1
- package/dist/schema/EventDef/event-def.d.ts.map +1 -1
- package/dist/schema/EventDef/facts.d.ts.map +1 -1
- package/dist/schema/EventDef/materializer.d.ts +2 -2
- package/dist/schema/EventDef/materializer.d.ts.map +1 -1
- package/dist/schema/EventDef/mod.js +5 -5
- package/dist/schema/EventSequenceNumber/client.d.ts +12 -17
- package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber/client.js +20 -24
- package/dist/schema/EventSequenceNumber/client.js.map +1 -1
- package/dist/schema/EventSequenceNumber/global.d.ts +2 -2
- package/dist/schema/EventSequenceNumber/global.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber/global.js +1 -1
- package/dist/schema/EventSequenceNumber/global.js.map +1 -1
- package/dist/schema/EventSequenceNumber/mod.js +2 -2
- package/dist/schema/EventSequenceNumber.test.js +6 -6
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent/client.d.ts +58 -187
- package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/client.js +33 -30
- package/dist/schema/LiveStoreEvent/client.js.map +1 -1
- package/dist/schema/LiveStoreEvent/client.test.js +13 -4
- package/dist/schema/LiveStoreEvent/client.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent/for-event-def.d.ts +7 -7
- package/dist/schema/LiveStoreEvent/for-event-def.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/global.d.ts +6 -6
- package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/global.js +2 -2
- package/dist/schema/LiveStoreEvent/global.js.map +1 -1
- package/dist/schema/LiveStoreEvent/input.d.ts +6 -6
- package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/input.js +3 -3
- package/dist/schema/LiveStoreEvent/input.js.map +1 -1
- package/dist/schema/LiveStoreEvent/mod.js +4 -4
- package/dist/schema/events.js +1 -1
- package/dist/schema/mod.d.ts +1 -0
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +11 -10
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +6 -5
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/mod.js +2 -2
- package/dist/schema/state/sqlite/client-document-def.d.ts +13 -18
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +30 -27
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +53 -18
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts +13 -13
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.js +13 -15
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.test.js +19 -43
- package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +73 -33
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +176 -75
- 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 +1 -1
- package/dist/schema/state/sqlite/column-spec.js.map +1 -1
- package/dist/schema/state/sqlite/column-spec.test.js +4 -4
- package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js +183 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts +2 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js +142 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +3 -8
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -73
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +9 -9
- 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 +9 -13
- 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 +28 -17
- 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 +13 -11
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +6 -4
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js +6 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/mod.js +2 -2
- package/dist/schema/state/sqlite/mod.d.ts +1 -3
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +11 -10
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/persistence.d.ts +11 -0
- package/dist/schema/state/sqlite/persistence.d.ts.map +1 -0
- package/dist/schema/state/sqlite/persistence.js +10 -0
- package/dist/schema/state/sqlite/persistence.js.map +1 -0
- package/dist/schema/state/sqlite/query-builder/api.d.ts +7 -7
- 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 +22 -23
- 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 +18 -16
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +364 -598
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/mod.js +2 -2
- package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.js +2 -2
- package/dist/schema/state/sqlite/schema-helpers.js.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.test.js +2 -2
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts +102 -134
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +5 -5
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables/mod.js +2 -2
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +380 -115
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables/state-tables.js +47 -6
- package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +17 -20
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +72 -6
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +207 -37
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts.map +1 -1
- package/dist/schema/unknown-events.js +3 -2
- package/dist/schema/unknown-events.js.map +1 -1
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +28 -6
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -1
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/common.js +2 -2
- package/dist/schema-management/migrations.d.ts +4 -4
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +9 -9
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-schema.d.ts.map +1 -1
- package/dist/schema-management/validate-schema.js +1 -1
- package/dist/session-id-symbol.d.ts +29 -0
- package/dist/session-id-symbol.d.ts.map +1 -0
- package/dist/session-id-symbol.js +44 -0
- package/dist/session-id-symbol.js.map +1 -0
- package/dist/sql-queries/index.js +3 -3
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +11 -13
- 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 +1 -1
- package/dist/sql-queries/types.d.ts +6 -7
- package/dist/sql-queries/types.d.ts.map +1 -1
- package/dist/sql-queries/types.js.map +1 -1
- package/dist/sqlite-db-helper.d.ts +23 -3
- package/dist/sqlite-db-helper.d.ts.map +1 -1
- package/dist/sqlite-db-helper.js +39 -4
- package/dist/sqlite-db-helper.js.map +1 -1
- package/dist/sqlite-types.d.ts +3 -6
- package/dist/sqlite-types.d.ts.map +1 -1
- package/dist/sqlite-types.js +3 -3
- package/dist/sqlite-types.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +41 -19
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +200 -70
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +12 -18
- package/dist/sync/errors.d.ts.map +1 -1
- package/dist/sync/errors.js +3 -3
- package/dist/sync/errors.js.map +1 -1
- package/dist/sync/index.js +6 -6
- package/dist/sync/mock-sync-backend.d.ts +2 -3
- package/dist/sync/mock-sync-backend.d.ts.map +1 -1
- package/dist/sync/mock-sync-backend.js +58 -37
- package/dist/sync/mock-sync-backend.js.map +1 -1
- package/dist/sync/mock-sync-backend.test.d.ts +2 -0
- package/dist/sync/mock-sync-backend.test.d.ts.map +1 -0
- package/dist/sync/mock-sync-backend.test.js +49 -0
- package/dist/sync/mock-sync-backend.test.js.map +1 -0
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +3 -3
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -1
- package/dist/sync/next/history-dag-common.d.ts +5 -1
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +53 -32
- 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 +5 -4
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/mod.js +5 -5
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +2 -2
- package/dist/sync/next/test/compact-events.calculator.test.js +6 -6
- package/dist/sync/next/test/compact-events.test.js +4 -4
- package/dist/sync/next/test/event-fixtures.d.ts +8 -8
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +6 -6
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/next/test/history-dag-common.test.d.ts +2 -0
- package/dist/sync/next/test/history-dag-common.test.d.ts.map +1 -0
- package/dist/sync/next/test/history-dag-common.test.js +73 -0
- package/dist/sync/next/test/history-dag-common.test.js.map +1 -0
- package/dist/sync/next/test/mod.js +1 -1
- package/dist/sync/sync-backend-kv.d.ts.map +1 -1
- package/dist/sync/sync-backend-kv.js +5 -3
- package/dist/sync/sync-backend-kv.js.map +1 -1
- package/dist/sync/sync-backend.d.ts +15 -15
- package/dist/sync/sync-backend.d.ts.map +1 -1
- package/dist/sync/sync-backend.js +10 -6
- package/dist/sync/sync-backend.js.map +1 -1
- package/dist/sync/sync.d.ts +1 -1
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +2 -2
- package/dist/sync/syncstate.d.ts +100 -176
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +36 -39
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +103 -64
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/transport-chunking.d.ts +15 -16
- package/dist/sync/transport-chunking.d.ts.map +1 -1
- package/dist/sync/transport-chunking.js +23 -32
- package/dist/sync/transport-chunking.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +2 -2
- package/dist/testing/event-factory.d.ts.map +1 -1
- package/dist/testing/event-factory.js +2 -2
- package/dist/testing/event-factory.js.map +1 -1
- package/dist/testing/mod.js +1 -1
- package/dist/util.d.ts +11 -4
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +20 -2
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +40 -57
- package/src/ClientSessionLeaderThreadProxy.ts +1 -1
- package/src/MaterializationJournal.ts +184 -0
- package/src/StateHead.ts +80 -0
- package/src/WorkerTransportError.ts +4 -4
- package/src/__tests__/fixture.ts +3 -3
- package/src/adapter-types.ts +28 -61
- package/src/debug-info.ts +53 -70
- package/src/defs.ts +2 -2
- package/src/devtools/devtools-compatibility.test.ts +1 -0
- package/src/devtools/devtools-messages-client-session.ts +56 -65
- package/src/devtools/devtools-messages-common.ts +12 -11
- package/src/devtools/devtools-messages-leader.ts +71 -69
- package/src/devtools/devtools-sessioninfo.ts +13 -11
- package/src/devtools/mod.ts +4 -5
- package/src/errors.ts +46 -34
- package/src/index.ts +8 -1
- package/src/leader-thread/LeaderSyncProcessor.ts +777 -650
- package/src/leader-thread/RejectedPushError.ts +40 -27
- package/src/leader-thread/connection.ts +12 -8
- package/src/leader-thread/eventlog.ts +20 -12
- package/src/leader-thread/leader-worker-devtools.ts +38 -20
- package/src/leader-thread/make-leader-thread-layer.test.ts +7 -7
- package/src/leader-thread/make-leader-thread-layer.ts +100 -71
- package/src/leader-thread/materialize-event.ts +13 -8
- package/src/leader-thread/recreate-db.ts +41 -75
- package/src/leader-thread/shutdown-channel.ts +3 -14
- package/src/leader-thread/stream-events.ts +102 -101
- package/src/leader-thread/types.ts +29 -62
- package/src/logging.ts +5 -52
- package/src/make-client-session.ts +6 -6
- package/src/materializer-helper.ts +7 -36
- package/src/otel.ts +5 -14
- package/src/rematerialize-from-eventlog.ts +116 -98
- package/src/schema/EventDef/define.test.ts +28 -0
- package/src/schema/EventDef/define.ts +20 -19
- package/src/schema/EventDef/deprecated.test.ts +1 -8
- package/src/schema/EventDef/deprecated.ts +17 -20
- package/src/schema/EventDef/event-def.ts +2 -4
- package/src/schema/EventSequenceNumber/client.ts +20 -24
- package/src/schema/EventSequenceNumber/global.ts +1 -1
- package/src/schema/EventSequenceNumber.test.ts +5 -5
- package/src/schema/LiveStoreEvent/client.test.ts +17 -4
- package/src/schema/LiveStoreEvent/client.ts +36 -35
- package/src/schema/LiveStoreEvent/for-event-def.ts +7 -7
- package/src/schema/LiveStoreEvent/global.ts +1 -1
- package/src/schema/LiveStoreEvent/input.ts +6 -6
- package/src/schema/mod.ts +1 -0
- package/src/schema/schema.ts +4 -3
- package/src/schema/state/sqlite/client-document-def.test.ts +55 -20
- package/src/schema/state/sqlite/client-document-def.ts +95 -80
- package/src/schema/state/sqlite/column-annotations.test.ts +26 -45
- package/src/schema/state/sqlite/column-annotations.ts +26 -30
- package/src/schema/state/sqlite/column-def.test.ts +213 -77
- package/src/schema/state/sqlite/column-def.ts +87 -37
- package/src/schema/state/sqlite/column-spec.test.ts +2 -2
- package/src/schema/state/sqlite/column-spec.ts +3 -2
- package/src/schema/state/sqlite/db-schema/ast/fingerprint-digest.ts +6 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.test.ts +209 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.ts +225 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -81
- package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +123 -63
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +35 -16
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +26 -31
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +35 -21
- package/src/schema/state/sqlite/db-schema/has-json-string-encoding.ts +9 -0
- package/src/schema/state/sqlite/mod.ts +4 -5
- package/src/schema/state/sqlite/persistence.ts +11 -0
- package/src/schema/state/sqlite/query-builder/api.ts +9 -9
- package/src/schema/state/sqlite/query-builder/astToSql.ts +24 -26
- package/src/schema/state/sqlite/query-builder/impl.test.ts +433 -669
- package/src/schema/state/sqlite/query-builder/impl.ts +19 -15
- package/src/schema/state/sqlite/schema-helpers.ts +1 -1
- package/src/schema/state/sqlite/system-tables/eventlog-tables.ts +2 -2
- package/src/schema/state/sqlite/system-tables/state-tables.ts +54 -3
- package/src/schema/state/sqlite/table-def.test.ts +270 -66
- package/src/schema/state/sqlite/table-def.ts +112 -45
- package/src/schema/unknown-events.ts +3 -2
- package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +35 -4
- package/src/schema-management/migrations.ts +7 -6
- package/src/session-id-symbol.ts +59 -0
- package/src/sql-queries/sql-queries.ts +9 -13
- package/src/sql-queries/types.ts +6 -7
- package/src/sqlite-db-helper.ts +64 -2
- package/src/sqlite-types.ts +5 -8
- package/src/sync/ClientSessionSyncProcessor.ts +303 -119
- package/src/sync/errors.ts +14 -12
- package/src/sync/mock-sync-backend.test.ts +78 -0
- package/src/sync/mock-sync-backend.ts +93 -47
- package/src/sync/next/history-dag-common.ts +60 -34
- package/src/sync/next/history-dag.ts +4 -1
- package/src/sync/next/test/compact-events.calculator.test.ts +2 -2
- package/src/sync/next/test/event-fixtures.ts +1 -1
- package/src/sync/next/test/history-dag-common.test.ts +87 -0
- package/src/sync/sync-backend-kv.ts +4 -2
- package/src/sync/sync-backend.ts +15 -15
- package/src/sync/sync.ts +1 -1
- package/src/sync/syncstate.test.ts +191 -140
- package/src/sync/syncstate.ts +63 -53
- package/src/sync/transport-chunking.ts +34 -39
- package/src/util.ts +19 -9
- package/src/version.ts +1 -2
- package/dist/schema/state/sqlite/db-schema/hash.d.ts +0 -2
- package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +0 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +0 -15
- package/dist/schema/state/sqlite/db-schema/hash.js.map +0 -1
- package/dist/sql-queries/misc.d.ts +0 -2
- package/dist/sql-queries/misc.d.ts.map +0 -1
- package/dist/sql-queries/misc.js +0 -2
- package/dist/sql-queries/misc.js.map +0 -1
- package/src/schema/state/sqlite/db-schema/hash.ts +0 -13
- package/src/sql-queries/misc.ts +0 -2
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { casesHandled,
|
|
2
|
-
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
|
1
|
+
import { casesHandled, LS_DEV, TRACE_VERBOSE } from '@livestore/utils'
|
|
3
2
|
import {
|
|
4
|
-
|
|
3
|
+
type HttpClient,
|
|
4
|
+
type Latch,
|
|
5
|
+
type Scope,
|
|
6
|
+
type Tracer,
|
|
5
7
|
Cause,
|
|
8
|
+
Context,
|
|
6
9
|
Deferred,
|
|
7
10
|
Duration,
|
|
8
11
|
Effect,
|
|
@@ -10,38 +13,47 @@ import {
|
|
|
10
13
|
FiberHandle,
|
|
11
14
|
Layer,
|
|
12
15
|
Option,
|
|
16
|
+
Predicate,
|
|
13
17
|
Queue,
|
|
18
|
+
Random,
|
|
14
19
|
ReadonlyArray,
|
|
20
|
+
References,
|
|
21
|
+
Result,
|
|
15
22
|
Schedule,
|
|
16
23
|
Schema,
|
|
24
|
+
Semaphore,
|
|
17
25
|
Stream,
|
|
18
26
|
Subscribable,
|
|
19
27
|
SubscriptionRef,
|
|
28
|
+
TxQueue,
|
|
20
29
|
} from '@livestore/utils/effect'
|
|
21
30
|
|
|
22
|
-
import {
|
|
31
|
+
import { MaterializeError, type SqliteDb, UnknownError } from '../adapter-types.ts'
|
|
32
|
+
import type { UnknownEventError } from '../errors.ts'
|
|
23
33
|
import { IntentionalShutdownCause } from '../errors.ts'
|
|
24
34
|
import { makeMaterializerHash } from '../materializer-helper.ts'
|
|
25
35
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
|
26
36
|
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
|
|
27
37
|
import { EVENTLOG_META_TABLE, SYNC_STATUS_TABLE } from '../schema/state/sqlite/system-tables/eventlog-tables.ts'
|
|
38
|
+
import * as StateHead from '../StateHead.ts'
|
|
28
39
|
import type { BackendIdMismatchError, IsOfflineError, SyncBackend } from '../sync/sync.ts'
|
|
29
|
-
import { isRejectedPushError, LeaderAheadError, NonMonotonicBatchError, StaleRebaseGenerationError } from './RejectedPushError.ts'
|
|
30
40
|
import * as SyncState from '../sync/syncstate.ts'
|
|
31
41
|
import { sql } from '../util.ts'
|
|
32
42
|
import * as Eventlog from './eventlog.ts'
|
|
33
43
|
import { rollback } from './materialize-event.ts'
|
|
44
|
+
import {
|
|
45
|
+
isRejectedPushError,
|
|
46
|
+
LeaderAheadError,
|
|
47
|
+
NonContiguousBatchError,
|
|
48
|
+
NonMonotonicBatchError,
|
|
49
|
+
type RejectedPushError,
|
|
50
|
+
StaleRebaseGenerationError,
|
|
51
|
+
} from './RejectedPushError.ts'
|
|
34
52
|
import type { ShutdownChannel } from './shutdown-channel.ts'
|
|
35
|
-
import type { InitialBlockingSyncContext,
|
|
36
|
-
import { LeaderThreadCtx } from './types.ts'
|
|
37
|
-
|
|
38
|
-
/** Serialize value to JSON string for trace attributes */
|
|
39
|
-
const jsonStringify = Schema.encodeSync(Schema.parseJson())
|
|
53
|
+
import type { InitialBlockingSyncContext, MaterializeEvent } from './types.ts'
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
deferred: Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError> | undefined,
|
|
44
|
-
]
|
|
55
|
+
export const TypeId = '~@livestore/common/LeaderSyncProcessor' as const
|
|
56
|
+
export type TypeId = typeof TypeId
|
|
45
57
|
|
|
46
58
|
/**
|
|
47
59
|
* The LeaderSyncProcessor manages synchronization of events between
|
|
@@ -73,29 +85,60 @@ type LocalPushQueueItem = [
|
|
|
73
85
|
*
|
|
74
86
|
* See ClientSessionSyncProcessor for how the leader and session sync processors are similar/different.
|
|
75
87
|
*/
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
export class LeaderSyncProcessor extends Context.Service<LeaderSyncProcessor, Service>()(
|
|
89
|
+
'@livestore/common/LeaderSyncProcessor',
|
|
90
|
+
) {}
|
|
91
|
+
|
|
92
|
+
export interface Service {
|
|
93
|
+
readonly [TypeId]: TypeId
|
|
94
|
+
/** Used by client sessions to subscribe to upstream sync state changes */
|
|
95
|
+
readonly pull: (args: {
|
|
96
|
+
cursor: EventSequenceNumber.Client.Composite
|
|
97
|
+
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type }>
|
|
98
|
+
/** The `pullQueue` API can be used instead of `pull` when more convenient */
|
|
99
|
+
readonly pullQueue: (args: {
|
|
100
|
+
cursor: EventSequenceNumber.Client.Composite
|
|
101
|
+
}) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, never, Scope.Scope>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Used by client sessions to push events to the leader thread.
|
|
105
|
+
* The effect only finishes when the local push has been processed (i.e. succeeded or was rejected).
|
|
106
|
+
* This doesn't mean the events have been pushed to the sync backend.
|
|
107
|
+
*/
|
|
108
|
+
readonly push: (
|
|
109
|
+
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
|
110
|
+
batch: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
111
|
+
) => Effect.Effect<void, RejectedPushError>
|
|
112
|
+
|
|
113
|
+
/** Currently only used by devtools which don't provide their own event numbers */
|
|
114
|
+
readonly pushPartial: (args: {
|
|
115
|
+
event: LiveStoreEvent.Input.Encoded
|
|
116
|
+
clientId: string
|
|
117
|
+
sessionId: string
|
|
118
|
+
}) => Effect.Effect<void, UnknownEventError>
|
|
119
|
+
|
|
120
|
+
readonly boot: Effect.Effect<
|
|
121
|
+
{ initialLeaderHead: EventSequenceNumber.Client.Composite },
|
|
122
|
+
never,
|
|
123
|
+
Scope.Scope | HttpClient.HttpClient
|
|
124
|
+
>
|
|
125
|
+
readonly syncState: Subscribable.Subscribable<SyncState.SyncState>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface Options {
|
|
129
|
+
readonly schema: LiveStoreSchema
|
|
130
|
+
/** Complete runtime capabilities supplied before the processor is constructed. */
|
|
131
|
+
readonly runtime: Runtime
|
|
132
|
+
readonly initialBlockingSyncContext: InitialBlockingSyncContext
|
|
90
133
|
/** Initial sync state rehydrated from the persisted eventlog or initial sync state */
|
|
91
|
-
initialSyncState: SyncState.SyncState
|
|
134
|
+
readonly initialSyncState: SyncState.SyncState
|
|
92
135
|
/**
|
|
93
136
|
* What to do when a failure (any cause) occurs (except `BackendIdMismatchError`).
|
|
94
137
|
*
|
|
95
138
|
* - `'shutdown'`: Send the error to the shutdown channel and terminate the sync processor.
|
|
96
139
|
* - `'ignore'`: Continue running.
|
|
97
140
|
*/
|
|
98
|
-
onError: 'shutdown' | 'ignore'
|
|
141
|
+
readonly onError: 'shutdown' | 'ignore'
|
|
99
142
|
/**
|
|
100
143
|
* What to do when the sync backend identity has changed (i.e. the backend was reset).
|
|
101
144
|
*
|
|
@@ -103,8 +146,8 @@ export const makeLeaderSyncProcessor = ({
|
|
|
103
146
|
* - `'shutdown'`: Send a shutdown signal without clearing local storage.
|
|
104
147
|
* - `'ignore'`: Continue running with stale data.
|
|
105
148
|
*/
|
|
106
|
-
onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
|
|
107
|
-
params: {
|
|
149
|
+
readonly onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
|
|
150
|
+
readonly params: {
|
|
108
151
|
/**
|
|
109
152
|
* Maximum number of local events to process per batch cycle.
|
|
110
153
|
*
|
|
@@ -126,7 +169,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
126
169
|
*
|
|
127
170
|
* @default 10
|
|
128
171
|
*/
|
|
129
|
-
localPushBatchSize?: number
|
|
172
|
+
readonly localPushBatchSize?: number
|
|
130
173
|
/**
|
|
131
174
|
* Maximum number of events to push to the sync backend per batch.
|
|
132
175
|
*
|
|
@@ -145,372 +188,162 @@ export const makeLeaderSyncProcessor = ({
|
|
|
145
188
|
*
|
|
146
189
|
* @default 50
|
|
147
190
|
*/
|
|
148
|
-
backendPushBatchSize?: number
|
|
191
|
+
readonly backendPushBatchSize?: number
|
|
149
192
|
}
|
|
150
193
|
/**
|
|
151
194
|
* Whether the sync backend should reactively pull new events from the sync backend
|
|
152
195
|
* When `false`, the sync processor will only do an initial pull
|
|
153
196
|
*/
|
|
154
|
-
livePull: boolean
|
|
155
|
-
testing: {
|
|
156
|
-
delays?: {
|
|
157
|
-
localPushProcessing?: Effect.Effect<void>
|
|
197
|
+
readonly livePull: boolean
|
|
198
|
+
readonly testing: {
|
|
199
|
+
readonly delays?: {
|
|
200
|
+
readonly localPushProcessing?: Effect.Effect<void>
|
|
201
|
+
readonly backendPullReconnect?: (fallbackMs: number) => number
|
|
158
202
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const localPushBatchSize = params.localPushBatchSize ?? 10
|
|
164
|
-
const backendPushBatchSize = params.backendPushBatchSize ?? 50
|
|
165
|
-
|
|
166
|
-
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
|
167
|
-
|
|
168
|
-
const isClientEvent = (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) =>
|
|
169
|
-
schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
|
|
170
|
-
|
|
171
|
-
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
|
172
|
-
|
|
173
|
-
// This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
|
|
174
|
-
const ctxRef = {
|
|
175
|
-
current: undefined as
|
|
176
|
-
| undefined
|
|
177
|
-
| {
|
|
178
|
-
span: Tracer.Span
|
|
179
|
-
devtoolsLatch: Effect.Latch | undefined
|
|
180
|
-
runtime: Runtime.Runtime<LeaderThreadCtx>
|
|
181
|
-
},
|
|
203
|
+
readonly hooks?: {
|
|
204
|
+
readonly localPushAdmitted?: (events: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>) => Effect.Effect<void>
|
|
205
|
+
/** Observes connectivity after the offline wait has subscribed, allowing tests to synchronize without sleeps. */
|
|
206
|
+
readonly backendPullReconnectWait?: (state: { isConnected: boolean; fallbackMs: number }) => Effect.Effect<void>
|
|
182
207
|
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
183
210
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const pushHeadRef = { current: EventSequenceNumber.Client.ROOT }
|
|
198
|
-
const advancePushHead = (eventNum: EventSequenceNumber.Client.Composite) => {
|
|
199
|
-
pushHeadRef.current = EventSequenceNumber.Client.max(pushHeadRef.current, eventNum)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// NOTE: New events are only pushed to sync backend after successful local push processing
|
|
203
|
-
const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
|
|
204
|
-
Effect.gen(function* () {
|
|
205
|
-
if (newEvents.length === 0) return
|
|
206
|
-
|
|
207
|
-
// console.debug('push', newEvents)
|
|
208
|
-
|
|
209
|
-
yield* validatePushBatch(newEvents, pushHeadRef.current)
|
|
210
|
-
|
|
211
|
-
advancePushHead(newEvents.at(-1)!.seqNum)
|
|
212
|
-
|
|
213
|
-
const waitForProcessing = options?.waitForProcessing ?? false
|
|
214
|
-
|
|
215
|
-
if (waitForProcessing === true) {
|
|
216
|
-
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError | StaleRebaseGenerationError>())
|
|
217
|
-
|
|
218
|
-
const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
|
|
211
|
+
/**
|
|
212
|
+
* Runtime capabilities used by sync processing. Keeping these explicit prevents the processor from
|
|
213
|
+
* depending on the outward-facing leader aggregate that contains the processor itself.
|
|
214
|
+
*/
|
|
215
|
+
interface Runtime {
|
|
216
|
+
readonly dbState: SqliteDb
|
|
217
|
+
readonly dbEventlog: SqliteDb
|
|
218
|
+
readonly materializeEvent: MaterializeEvent
|
|
219
|
+
readonly syncBackend: SyncBackend.SyncBackend | undefined
|
|
220
|
+
readonly shutdownChannel: ShutdownChannel
|
|
221
|
+
readonly devtoolsLatch: Latch.Latch | undefined
|
|
222
|
+
readonly span: Tracer.Span | undefined
|
|
223
|
+
}
|
|
219
224
|
|
|
220
|
-
|
|
225
|
+
export const make = Effect.fnUntraced(function* ({
|
|
226
|
+
schema,
|
|
227
|
+
runtime,
|
|
228
|
+
initialBlockingSyncContext,
|
|
229
|
+
initialSyncState,
|
|
230
|
+
onError,
|
|
231
|
+
onBackendIdMismatch,
|
|
232
|
+
livePull,
|
|
233
|
+
params,
|
|
234
|
+
testing,
|
|
235
|
+
}: Options) {
|
|
236
|
+
const stateHead = yield* StateHead.StateHead
|
|
237
|
+
const { dbState, dbEventlog, devtoolsLatch, materializeEvent, shutdownChannel, span, syncBackend } = runtime
|
|
238
|
+
const syncBackendPushQueue = yield* TxQueue.unbounded<LiveStoreEvent.Client.EncodedWithMeta>()
|
|
239
|
+
const localPushBatchSize = params.localPushBatchSize ?? 10
|
|
240
|
+
const backendPushBatchSize = params.backendPushBatchSize ?? 50
|
|
241
|
+
|
|
242
|
+
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
|
243
|
+
|
|
244
|
+
const isClientOnlyEvent = (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) =>
|
|
245
|
+
schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
|
|
246
|
+
|
|
247
|
+
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
|
248
|
+
const materializeEventsBatch = makeMaterializeEventsBatch({ dbState, dbEventlog, materializeEvent })
|
|
249
|
+
|
|
250
|
+
type LocalPushQueueItem = [
|
|
251
|
+
event: LiveStoreEvent.Client.EncodedWithMeta,
|
|
252
|
+
deferred: Deferred.Deferred<void, LeaderAheadError | StaleRebaseGenerationError>,
|
|
253
|
+
]
|
|
254
|
+
const localPushesQueue = yield* TxQueue.unbounded<LocalPushQueueItem>()
|
|
255
|
+
// Reservations cover admitted pushes from validation until they are applied or rejected. The Set's
|
|
256
|
+
// insertion order mirrors admission order, so its last relevant item is the optimistic sequence head.
|
|
257
|
+
const reservedLocalPushItems = new Set<LocalPushQueueItem>()
|
|
258
|
+
// Ensures mutual exclusion between local push and backend pull processing.
|
|
259
|
+
const localPushBackendPullMutex = yield* Semaphore.make(1)
|
|
260
|
+
// Serializes validation, queue admission, and prefix-fence reconciliation.
|
|
261
|
+
const pushAdmissionSemaphore = yield* Semaphore.make(1)
|
|
221
262
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
batch: TRACE_VERBOSE === true ? newEvents : undefined,
|
|
232
|
-
},
|
|
233
|
-
links:
|
|
234
|
-
ctxRef.current?.span !== undefined
|
|
235
|
-
? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }]
|
|
236
|
-
: undefined,
|
|
237
|
-
}),
|
|
238
|
-
)
|
|
263
|
+
/**
|
|
264
|
+
* Admission fence for local pushes. Unlike `syncState.localHead`, this advances as soon as an event
|
|
265
|
+
* is queued, so another session cannot claim the same sequence number while that event is waiting
|
|
266
|
+
* to be applied. With authoritative head e0 and reserved pushes e1/e2, this points to e2.
|
|
267
|
+
*
|
|
268
|
+
* All reads and writes are protected by `pushAdmissionSemaphore` so validation and reservation are
|
|
269
|
+
* one atomic operation from the perspective of concurrent sessions and backend pulls.
|
|
270
|
+
*/
|
|
271
|
+
const pushHeadRef = { current: initialSyncState.localHead }
|
|
239
272
|
|
|
240
|
-
|
|
273
|
+
/**
|
|
274
|
+
* A backend pull may advance or rebase authoritative history while local pushes remain reserved.
|
|
275
|
+
* Keep the fence at the newest reservation that is still valid for that history; if none remains,
|
|
276
|
+
* fall back to the authoritative head. Stale reservations are released later by the queue worker.
|
|
277
|
+
*/
|
|
278
|
+
const reconcilePushHead = (authoritativeHead: EventSequenceNumber.Client.Composite) =>
|
|
279
|
+
pushAdmissionSemaphore.withPermits(1)(
|
|
241
280
|
Effect.gen(function* () {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// persisted server-side once a schema update ships.
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const eventEncoded = new LiveStoreEvent.Client.EncodedWithMeta({
|
|
262
|
-
name,
|
|
263
|
-
args,
|
|
264
|
-
clientId,
|
|
265
|
-
sessionId,
|
|
266
|
-
...EventSequenceNumber.Client.nextPair({
|
|
267
|
-
seqNum: syncState.localHead,
|
|
268
|
-
isClient: resolution.eventDef.options.clientOnly,
|
|
269
|
-
}),
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
yield* push([eventEncoded])
|
|
273
|
-
}).pipe(
|
|
274
|
-
// pushPartial constructs the event sequence number internally, so these errors should never happen.
|
|
275
|
-
Effect.catchIf(isRejectedPushError, Effect.die),
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
// Starts various background loops
|
|
279
|
-
const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
|
|
280
|
-
const span = yield* Effect.currentSpan.pipe(Effect.orDie)
|
|
281
|
-
const { devtools, shutdownChannel } = yield* LeaderThreadCtx
|
|
282
|
-
const runtime = yield* Effect.runtime<LeaderThreadCtx>()
|
|
283
|
-
|
|
284
|
-
ctxRef.current = {
|
|
285
|
-
span,
|
|
286
|
-
devtoolsLatch: devtools.enabled === true ? devtools.syncBackendLatch : undefined,
|
|
287
|
-
runtime,
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
|
291
|
-
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
|
292
|
-
|
|
293
|
-
// Rehydrate sync queue
|
|
294
|
-
if (initialSyncState.pending.length > 0) {
|
|
295
|
-
const globalPendingEvents = initialSyncState.pending
|
|
296
|
-
// Don't sync client-local events
|
|
297
|
-
.filter((eventEncoded) => {
|
|
298
|
-
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
299
|
-
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
if (globalPendingEvents.length > 0) {
|
|
303
|
-
yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingEvents)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const handleBackendIdMismatchError = (error: BackendIdMismatchError) =>
|
|
308
|
-
handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel })
|
|
309
|
-
|
|
310
|
-
const maybeShutdownOnError = (
|
|
311
|
-
cause: Cause.Cause<
|
|
312
|
-
| UnknownError
|
|
313
|
-
| MaterializeError
|
|
314
|
-
>,
|
|
315
|
-
) =>
|
|
316
|
-
Effect.gen(function* () {
|
|
317
|
-
if (onError === 'ignore') {
|
|
318
|
-
if (LS_DEV === true) {
|
|
319
|
-
yield* Effect.logDebug(
|
|
320
|
-
`Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
|
|
321
|
-
Cause.pretty(cause),
|
|
322
|
-
)
|
|
323
|
-
}
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const errorToSend = Cause.isFailType(cause) === true ? cause.error : UnknownError.make({ cause })
|
|
328
|
-
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
|
329
|
-
|
|
330
|
-
return yield* Effect.failCause(cause).pipe(Effect.orDie)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
yield* backgroundApplyLocalPushes({
|
|
334
|
-
localPushBackendPullMutex,
|
|
335
|
-
localPushesQueue,
|
|
336
|
-
syncStateSref,
|
|
337
|
-
syncBackendPushQueue,
|
|
338
|
-
schema,
|
|
339
|
-
isClientEvent,
|
|
340
|
-
connectedClientSessionPullQueues,
|
|
341
|
-
localPushBatchSize,
|
|
342
|
-
testing: {
|
|
343
|
-
delay: testing?.delays?.localPushProcessing,
|
|
344
|
-
},
|
|
345
|
-
}).pipe(
|
|
346
|
-
Effect.catchAllCause(maybeShutdownOnError),
|
|
347
|
-
Effect.forkScoped,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
|
|
351
|
-
const backendPushingEffect = backgroundBackendPushing({
|
|
352
|
-
syncBackendPushQueue,
|
|
353
|
-
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
354
|
-
backendPushBatchSize,
|
|
355
|
-
}).pipe(
|
|
356
|
-
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
357
|
-
Effect.catchAllCause(maybeShutdownOnError),
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
361
|
-
|
|
362
|
-
yield* backgroundBackendPulling({
|
|
363
|
-
isClientEvent,
|
|
364
|
-
restartBackendPushing: (filteredRebasedPending) =>
|
|
365
|
-
Effect.gen(function* () {
|
|
366
|
-
// Stop current pushing fiber
|
|
367
|
-
yield* FiberHandle.clear(backendPushingFiberHandle)
|
|
368
|
-
|
|
369
|
-
// Reset the sync backend push queue
|
|
370
|
-
yield* BucketQueue.clear(syncBackendPushQueue)
|
|
371
|
-
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending)
|
|
372
|
-
|
|
373
|
-
// Restart pushing fiber
|
|
374
|
-
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
375
|
-
}),
|
|
376
|
-
syncStateSref,
|
|
377
|
-
localPushBackendPullMutex,
|
|
378
|
-
livePull,
|
|
379
|
-
dbState,
|
|
380
|
-
initialBlockingSyncContext,
|
|
381
|
-
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
382
|
-
connectedClientSessionPullQueues,
|
|
383
|
-
advancePushHead,
|
|
384
|
-
}).pipe(
|
|
385
|
-
Effect.retry({
|
|
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',
|
|
390
|
-
}),
|
|
391
|
-
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
392
|
-
Effect.catchAllCause(maybeShutdownOnError),
|
|
393
|
-
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
|
394
|
-
// This might be a bug in Effect. Only seems to happen in the browser.
|
|
395
|
-
Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
|
|
396
|
-
Effect.forkScoped,
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
return { initialLeaderHead: initialSyncState.localHead }
|
|
400
|
-
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
|
401
|
-
|
|
402
|
-
const pull: LeaderSyncProcessor['pull'] = ({ cursor }) =>
|
|
281
|
+
const latestCurrentGenerationItem = [...reservedLocalPushItems].findLast(
|
|
282
|
+
([event]) => event.seqNum.rebaseGeneration >= authoritativeHead.rebaseGeneration,
|
|
283
|
+
)
|
|
284
|
+
pushHeadRef.current = latestCurrentGenerationItem?.[0].seqNum ?? authoritativeHead
|
|
285
|
+
}).pipe(Effect.uninterruptible),
|
|
286
|
+
)
|
|
287
|
+
/**
|
|
288
|
+
* Stop completed, rejected, or stale queue items from extending the admission fence, then rebuild
|
|
289
|
+
* the fence from any pushes that are still reserved.
|
|
290
|
+
*/
|
|
291
|
+
const releasePushReservations = (
|
|
292
|
+
items: ReadonlyArray<LocalPushQueueItem>,
|
|
293
|
+
authoritativeHead: EventSequenceNumber.Client.Composite,
|
|
294
|
+
) =>
|
|
295
|
+
pushAdmissionSemaphore.withPermits(1)(
|
|
403
296
|
Effect.gen(function* () {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
/*
|
|
409
|
-
Notes for a potential new `LeaderSyncProcessor.pull` implementation:
|
|
410
|
-
|
|
411
|
-
- Doesn't take cursor but is "atomically called" in the leader during the snapshot phase
|
|
412
|
-
- TODO: how is this done "atomically" in the web adapter where the snapshot is read optimistically?
|
|
413
|
-
- Would require a new kind of "boot-phase" API which is stream based:
|
|
414
|
-
- initial message: state snapshot + seq num head
|
|
415
|
-
- subsequent messages: sync state payloads
|
|
416
|
-
|
|
417
|
-
- alternative: instead of session pulling sync state payloads from leader, we could send
|
|
418
|
-
- events in the "advance" case
|
|
419
|
-
- full new state db snapshot in the "rebase" case
|
|
420
|
-
- downside: importing the snapshot is expensive
|
|
421
|
-
*/
|
|
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))
|
|
297
|
+
for (const item of items) reservedLocalPushItems.delete(item)
|
|
298
|
+
const latestCurrentGenerationItem = [...reservedLocalPushItems].findLast(
|
|
299
|
+
([event]) => event.seqNum.rebaseGeneration >= authoritativeHead.rebaseGeneration,
|
|
427
300
|
)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
get: syncStateSref.pipe(Effect.flatMap(Effect.fromNullable), Effect.orDieDebugger),
|
|
432
|
-
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
pull,
|
|
437
|
-
pullQueue,
|
|
438
|
-
push,
|
|
439
|
-
pushPartial,
|
|
440
|
-
boot,
|
|
441
|
-
syncState,
|
|
442
|
-
} satisfies LeaderSyncProcessor
|
|
443
|
-
})
|
|
301
|
+
pushHeadRef.current = latestCurrentGenerationItem?.[0].seqNum ?? authoritativeHead
|
|
302
|
+
}).pipe(Effect.uninterruptible),
|
|
303
|
+
)
|
|
444
304
|
|
|
445
|
-
const backgroundApplyLocalPushes = ({
|
|
446
|
-
localPushBackendPullMutex,
|
|
447
|
-
localPushesQueue,
|
|
448
|
-
syncStateSref,
|
|
449
|
-
syncBackendPushQueue,
|
|
450
|
-
schema,
|
|
451
|
-
isClientEvent,
|
|
452
|
-
connectedClientSessionPullQueues,
|
|
453
|
-
localPushBatchSize,
|
|
454
|
-
testing,
|
|
455
|
-
}: {
|
|
456
|
-
localPushBackendPullMutex: Effect.Semaphore
|
|
457
|
-
localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
|
|
458
|
-
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
|
459
|
-
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
|
|
460
|
-
schema: LiveStoreSchema
|
|
461
|
-
isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
|
|
462
|
-
connectedClientSessionPullQueues: PullQueueSet
|
|
463
|
-
localPushBatchSize: number
|
|
464
|
-
testing: {
|
|
465
|
-
delay: Effect.Effect<void> | undefined
|
|
466
|
-
}
|
|
467
|
-
}) =>
|
|
468
|
-
Effect.gen(function* () {
|
|
305
|
+
const backgroundApplyLocalPushes = Effect.gen(function* () {
|
|
469
306
|
while (true) {
|
|
470
|
-
if (testing.
|
|
471
|
-
yield* testing.
|
|
307
|
+
if (testing.delays?.localPushProcessing !== undefined) {
|
|
308
|
+
yield* testing.delays.localPushProcessing.pipe(Effect.withSpan('localPushProcessingDelay'))
|
|
472
309
|
}
|
|
473
310
|
|
|
474
|
-
const batchItems = yield*
|
|
311
|
+
const batchItems = yield* TxQueue.takeBetween(localPushesQueue, 1, localPushBatchSize)
|
|
475
312
|
|
|
476
313
|
// Applies a batch of local pushes, guarded by the localPushBackendPullMutex to ensure mutual exclusion with backend pulling
|
|
477
314
|
yield* Effect.gen(function* () {
|
|
478
|
-
const syncState = yield* Effect.
|
|
315
|
+
const syncState = yield* Effect.fromNullishOr(yield* SubscriptionRef.get(syncStateSref)).pipe(
|
|
316
|
+
Effect.orDieDebugger,
|
|
317
|
+
)
|
|
479
318
|
|
|
480
319
|
const currentRebaseGeneration = syncState.localHead.rebaseGeneration
|
|
481
320
|
|
|
482
321
|
// Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
|
|
483
322
|
// It's important that we filter after acquiring the localPushBackendPullMutex, otherwise we might filter with the old generation
|
|
484
|
-
const [
|
|
485
|
-
|
|
486
|
-
|
|
323
|
+
const [filteredItems, droppedItems] = ReadonlyArray.partition(batchItems, (batchItem) =>
|
|
324
|
+
batchItem[0].seqNum.rebaseGeneration >= currentRebaseGeneration
|
|
325
|
+
? Result.succeed(batchItem)
|
|
326
|
+
: Result.fail(batchItem),
|
|
487
327
|
)
|
|
488
328
|
|
|
489
329
|
if (droppedItems.length > 0) {
|
|
490
330
|
yield* Effect.spanEvent(`push:drop-old-generation`, {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
331
|
+
droppedCount: droppedItems.length,
|
|
332
|
+
currentRebaseGeneration,
|
|
333
|
+
})
|
|
494
334
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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,
|
|
335
|
+
yield* Effect.forEach(droppedItems, ([eventEncoded, deferred]) =>
|
|
336
|
+
Deferred.fail(
|
|
337
|
+
deferred,
|
|
338
|
+
StaleRebaseGenerationError.make({
|
|
339
|
+
currentRebaseGeneration,
|
|
340
|
+
providedRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
|
510
341
|
sessionId: eventEncoded.sessionId,
|
|
511
|
-
|
|
512
|
-
|
|
342
|
+
}),
|
|
343
|
+
),
|
|
513
344
|
)
|
|
345
|
+
|
|
346
|
+
yield* releasePushReservations(droppedItems, syncState.localHead)
|
|
514
347
|
}
|
|
515
348
|
|
|
516
349
|
if (filteredItems.length === 0) {
|
|
@@ -520,14 +353,14 @@ const backgroundApplyLocalPushes = ({
|
|
|
520
353
|
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredItems)
|
|
521
354
|
|
|
522
355
|
yield* Effect.annotateCurrentSpan({
|
|
523
|
-
|
|
524
|
-
|
|
356
|
+
batchSize: newEvents.length,
|
|
357
|
+
...(TRACE_VERBOSE === true ? { newEvents: jsonStringify(newEvents) } : {}),
|
|
525
358
|
})
|
|
526
359
|
|
|
527
360
|
const mergeResult = yield* SyncState.merge({
|
|
528
361
|
syncState,
|
|
529
362
|
payload: { _tag: 'local-push', newEvents },
|
|
530
|
-
|
|
363
|
+
isClientOnlyEvent,
|
|
531
364
|
isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
|
|
532
365
|
})
|
|
533
366
|
|
|
@@ -548,27 +381,39 @@ const backgroundApplyLocalPushes = ({
|
|
|
548
381
|
// All subsequent pushes with same generation should be rejected as well
|
|
549
382
|
// We're also handling the case where the localPushQueue already contains events
|
|
550
383
|
// from the next generation which we preserve in the queue
|
|
551
|
-
const remainingEventsMatchingGeneration = yield*
|
|
384
|
+
const remainingEventsMatchingGeneration = yield* takePrefixUntil(
|
|
552
385
|
localPushesQueue,
|
|
553
386
|
([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= nextRebaseGeneration,
|
|
554
387
|
)
|
|
555
388
|
|
|
556
389
|
// TODO we still need to better understand and handle this scenario
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
390
|
+
const remainingLocalPushes = yield* snapshotTxQueue(localPushesQueue)
|
|
391
|
+
if (LS_DEV === true && remainingLocalPushes.length > 0) {
|
|
392
|
+
console.log('localPushesQueue is not empty', remainingLocalPushes.length)
|
|
393
|
+
// oxlint-disable-next-line no-debugger -- intentional breakpoint for unexpected queue state
|
|
560
394
|
debugger
|
|
561
395
|
}
|
|
562
396
|
|
|
563
397
|
const allDeferredsToReject = [
|
|
564
398
|
...deferreds,
|
|
565
399
|
...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
|
|
566
|
-
]
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
// The rejected batch and its drained same-generation suffix will never advance leader state,
|
|
403
|
+
// so release their sequence-number reservations before clients retry from the authoritative head.
|
|
404
|
+
yield* releasePushReservations(
|
|
405
|
+
[...filteredItems, ...remainingEventsMatchingGeneration],
|
|
406
|
+
syncState.localHead,
|
|
407
|
+
)
|
|
567
408
|
|
|
568
409
|
yield* Effect.forEach(allDeferredsToReject, (deferred) =>
|
|
569
410
|
Deferred.fail(
|
|
570
411
|
deferred,
|
|
571
|
-
LeaderAheadError.make({
|
|
412
|
+
LeaderAheadError.make({
|
|
413
|
+
minimumExpectedNum: mergeResult.expectedMinimumId,
|
|
414
|
+
providedNum,
|
|
415
|
+
sessionId: newEvents.at(0)!.sessionId,
|
|
416
|
+
}),
|
|
572
417
|
),
|
|
573
418
|
)
|
|
574
419
|
|
|
@@ -584,346 +429,553 @@ const backgroundApplyLocalPushes = ({
|
|
|
584
429
|
}
|
|
585
430
|
}
|
|
586
431
|
|
|
432
|
+
// For a local-push advance, `newEvents` and the appended pending suffix describe the same logical
|
|
433
|
+
// events but serve different roles and may be distinct instances. Materialize the retained pending
|
|
434
|
+
// instances so their rollback metadata remains available if a later backend event causes a rebase.
|
|
435
|
+
const acceptedPendingEvents = mergeResult.newSyncState.pending.slice(syncState.pending.length)
|
|
436
|
+
if (acceptedPendingEvents.length !== mergeResult.newEvents.length) {
|
|
437
|
+
return yield* Effect.dieDebugger('Local push events must be retained in pending state')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
yield* materializeEventsBatch({ batchItems: acceptedPendingEvents })
|
|
441
|
+
|
|
587
442
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
588
443
|
|
|
589
444
|
yield* connectedClientSessionPullQueues.offer({
|
|
590
|
-
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents:
|
|
445
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: acceptedPendingEvents }),
|
|
591
446
|
leaderHead: mergeResult.newSyncState.localHead,
|
|
592
447
|
})
|
|
593
448
|
|
|
594
449
|
yield* Effect.spanEvent(`push:advance`, {
|
|
595
450
|
batchSize: newEvents.length,
|
|
596
|
-
|
|
451
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
597
452
|
})
|
|
598
453
|
|
|
599
|
-
// Don't sync client-
|
|
600
|
-
const
|
|
601
|
-
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
602
|
-
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
603
|
-
})
|
|
454
|
+
// Don't sync client-only events
|
|
455
|
+
const globalOrUnknownEvents = acceptedPendingEvents.filter((e) => !isClientOnlyEvent(e))
|
|
604
456
|
|
|
605
|
-
yield*
|
|
457
|
+
yield* TxQueue.offerAll(syncBackendPushQueue, globalOrUnknownEvents)
|
|
606
458
|
|
|
607
|
-
yield*
|
|
459
|
+
yield* releasePushReservations(filteredItems, mergeResult.newSyncState.localHead)
|
|
460
|
+
|
|
461
|
+
// A push is acknowledged only after the complete batch is materialized, published in
|
|
462
|
+
// leader sync state, exposed to sessions, and queued for backend propagation.
|
|
463
|
+
yield* Effect.forEach(deferreds, (deferred) => Deferred.succeed(deferred, void 0))
|
|
608
464
|
}).pipe(localPushBackendPullMutex.withPermits(1))
|
|
609
465
|
}
|
|
610
466
|
})
|
|
611
467
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
468
|
+
const backgroundBackendPulling = Effect.fn('@livestore/common:LeaderSyncProcessor:backend-pulling')(function* ({
|
|
469
|
+
restartBackendPushing,
|
|
470
|
+
onPullSuccess,
|
|
471
|
+
}: {
|
|
472
|
+
restartBackendPushing: (
|
|
473
|
+
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
474
|
+
) => Effect.Effect<void, never, HttpClient.HttpClient>
|
|
475
|
+
onPullSuccess: () => void
|
|
476
|
+
}) {
|
|
477
|
+
if (syncBackend === undefined) return
|
|
478
|
+
|
|
479
|
+
let pullMutexHeld = false
|
|
480
|
+
|
|
481
|
+
const releasePullMutexIfHeld = Effect.gen(function* () {
|
|
482
|
+
if (pullMutexHeld === false) return
|
|
483
|
+
pullMutexHeld = false
|
|
484
|
+
yield* localPushBackendPullMutex.release(1)
|
|
485
|
+
})
|
|
625
486
|
|
|
626
|
-
|
|
627
|
-
db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
628
|
-
dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
487
|
+
const isPullPaginationComplete = (pageInfo: SyncBackend.PullResPageInfo) => pageInfo._tag === 'NoMore'
|
|
629
488
|
|
|
630
|
-
|
|
489
|
+
const onNewPullChunk = (
|
|
490
|
+
newEvents: LiveStoreEvent.Client.EncodedWithMeta[],
|
|
491
|
+
pageInfo: SyncBackend.PullResPageInfo,
|
|
492
|
+
) =>
|
|
631
493
|
Effect.gen(function* () {
|
|
632
|
-
if (
|
|
494
|
+
if (devtoolsLatch !== undefined) {
|
|
495
|
+
yield* devtoolsLatch.await
|
|
496
|
+
}
|
|
633
497
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
498
|
+
if (newEvents.length === 0) {
|
|
499
|
+
if (isPullPaginationComplete(pageInfo) === true) {
|
|
500
|
+
yield* releasePullMutexIfHeld
|
|
501
|
+
}
|
|
502
|
+
return
|
|
503
|
+
}
|
|
639
504
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
505
|
+
// Prevent more local pushes from being processed until this pull pagination sequence is finished.
|
|
506
|
+
if (pullMutexHeld === false) {
|
|
507
|
+
yield* localPushBackendPullMutex.take(1)
|
|
508
|
+
pullMutexHeld = true
|
|
509
|
+
}
|
|
644
510
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
511
|
+
const chunkExit = yield* Effect.gen(function* () {
|
|
512
|
+
const syncState = yield* Effect.fromNullishOr(yield* SubscriptionRef.get(syncStateSref)).pipe(
|
|
513
|
+
Effect.orDieDebugger,
|
|
514
|
+
)
|
|
649
515
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
Effect.scoped,
|
|
655
|
-
Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
|
|
656
|
-
attributes: { batchSize: batchItems.length },
|
|
657
|
-
}),
|
|
658
|
-
Effect.tapCauseLogPretty,
|
|
659
|
-
)
|
|
516
|
+
yield* Effect.annotateCurrentSpan({
|
|
517
|
+
'merge.newEventsCount': newEvents.length,
|
|
518
|
+
...(TRACE_VERBOSE === true ? { 'merge.newEvents': jsonStringify(newEvents) } : {}),
|
|
519
|
+
})
|
|
660
520
|
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
devtoolsLatch,
|
|
669
|
-
initialBlockingSyncContext,
|
|
670
|
-
connectedClientSessionPullQueues,
|
|
671
|
-
advancePushHead,
|
|
672
|
-
}: {
|
|
673
|
-
isClientEvent: (eventEncoded: LiveStoreEvent.Client.EncodedWithMeta) => boolean
|
|
674
|
-
restartBackendPushing: (
|
|
675
|
-
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
676
|
-
) => Effect.Effect<void, never, LeaderThreadCtx | HttpClient.HttpClient>
|
|
677
|
-
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
|
678
|
-
dbState: SqliteDb
|
|
679
|
-
localPushBackendPullMutex: Effect.Semaphore
|
|
680
|
-
livePull: boolean
|
|
681
|
-
devtoolsLatch: Effect.Latch | undefined
|
|
682
|
-
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
683
|
-
connectedClientSessionPullQueues: PullQueueSet
|
|
684
|
-
advancePushHead: (eventNum: EventSequenceNumber.Client.Composite) => void
|
|
685
|
-
}) {
|
|
686
|
-
const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
|
|
521
|
+
const mergeResult = yield* SyncState.merge({
|
|
522
|
+
syncState,
|
|
523
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
|
|
524
|
+
isClientOnlyEvent,
|
|
525
|
+
isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
|
|
526
|
+
ignoreClientOnlyEvents: true,
|
|
527
|
+
})
|
|
687
528
|
|
|
688
|
-
|
|
529
|
+
if (mergeResult._tag === 'reject') {
|
|
530
|
+
return yield* Effect.dieDebugger('The leader thread should never reject upstream advances')
|
|
531
|
+
}
|
|
689
532
|
|
|
690
|
-
|
|
533
|
+
const newBackendHead = newEvents.at(-1)!.seqNum
|
|
691
534
|
|
|
692
|
-
|
|
693
|
-
if (pullMutexHeld === false) return
|
|
694
|
-
pullMutexHeld = false
|
|
695
|
-
yield* localPushBackendPullMutex.release(1)
|
|
696
|
-
})
|
|
535
|
+
Eventlog.updateBackendHead(dbEventlog, newBackendHead)
|
|
697
536
|
|
|
698
|
-
|
|
537
|
+
if (mergeResult._tag === 'rebase') {
|
|
538
|
+
yield* Effect.spanEvent(`pull:rebase[${mergeResult.newSyncState.localHead.rebaseGeneration}]`, {
|
|
539
|
+
newEventsCount: newEvents.length,
|
|
540
|
+
...(TRACE_VERBOSE === true ? { newEvents: jsonStringify(newEvents) } : {}),
|
|
541
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
|
542
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
543
|
+
})
|
|
699
544
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
545
|
+
const globalOrUnknownRebasedPendingEvents = mergeResult.newSyncState.pending.filter(
|
|
546
|
+
(e) => !isClientOnlyEvent(e),
|
|
547
|
+
)
|
|
548
|
+
yield* restartBackendPushing(globalOrUnknownRebasedPendingEvents)
|
|
549
|
+
|
|
550
|
+
if (mergeResult.rollbackEvents.length > 0) {
|
|
551
|
+
yield* rollback({
|
|
552
|
+
dbState,
|
|
553
|
+
dbEventlog,
|
|
554
|
+
eventNumsToRollback: mergeResult.rollbackEvents.map((_) => _.seqNum),
|
|
555
|
+
})
|
|
556
|
+
yield* stateHead
|
|
557
|
+
.set(mergeResult.rollbackEvents[0]!.parentSeqNum)
|
|
558
|
+
.pipe(Effect.mapError((cause) => MaterializeError.make({ cause })))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
yield* connectedClientSessionPullQueues.offer({
|
|
562
|
+
payload: SyncState.payloadFromMergeResult(mergeResult),
|
|
563
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
|
564
|
+
})
|
|
565
|
+
} else {
|
|
566
|
+
yield* Effect.spanEvent(`pull:advance`, {
|
|
567
|
+
newEventsCount: newEvents.length,
|
|
568
|
+
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// Ensure push fiber is active after advance by restarting with current pending (non-client-only) events
|
|
572
|
+
const globalOrUnknownPendingEvents = mergeResult.newSyncState.pending.filter((e) => !isClientOnlyEvent(e))
|
|
573
|
+
yield* restartBackendPushing(globalOrUnknownPendingEvents)
|
|
574
|
+
|
|
575
|
+
yield* connectedClientSessionPullQueues.offer({
|
|
576
|
+
payload: SyncState.payloadFromMergeResult(mergeResult),
|
|
577
|
+
leaderHead: mergeResult.newSyncState.localHead,
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
if (mergeResult.confirmedEvents.length > 0) {
|
|
581
|
+
// `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
|
|
582
|
+
// `newEvents` instead which we filter via `mergeResult.confirmedEvents`
|
|
583
|
+
const confirmedNewEvents = newEvents.filter((event) =>
|
|
584
|
+
mergeResult.confirmedEvents.some((confirmedEvent) =>
|
|
585
|
+
EventSequenceNumber.Client.isEqual(event.seqNum, confirmedEvent.seqNum),
|
|
586
|
+
),
|
|
587
|
+
)
|
|
588
|
+
yield* Eventlog.updateSyncMetadataForDb(dbEventlog, confirmedNewEvents).pipe(Effect.orDieDebugger)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
|
|
593
|
+
trimChangesetRows(dbState, newBackendHead)
|
|
594
|
+
|
|
595
|
+
// The backend merge may advance or rebase the authoritative head. Realign the admission
|
|
596
|
+
// fence now so newly arriving pushes are validated against that history, not the pre-pull head.
|
|
597
|
+
yield* reconcilePushHead(mergeResult.newSyncState.localHead)
|
|
598
|
+
|
|
599
|
+
// Apply the merged events to storage before publishing the new sync state below, so readers
|
|
600
|
+
// cannot observe a leader head whose events have not yet been materialized.
|
|
601
|
+
yield* materializeEventsBatch({ batchItems: mergeResult.newEvents })
|
|
602
|
+
|
|
603
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
604
|
+
}).pipe(Effect.exit)
|
|
605
|
+
|
|
606
|
+
if (Exit.isFailure(chunkExit) === true) {
|
|
607
|
+
yield* releasePullMutexIfHeld
|
|
608
|
+
return yield* Effect.failCause(chunkExit.cause)
|
|
609
|
+
}
|
|
705
610
|
|
|
706
|
-
if (newEvents.length === 0) {
|
|
707
611
|
if (isPullPaginationComplete(pageInfo) === true) {
|
|
708
612
|
yield* releasePullMutexIfHeld
|
|
709
613
|
}
|
|
710
|
-
|
|
711
|
-
}
|
|
614
|
+
})
|
|
712
615
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
616
|
+
const syncState = yield* Effect.fromNullishOr(yield* SubscriptionRef.get(syncStateSref)).pipe(Effect.orDieDebugger)
|
|
617
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfoForDb(dbEventlog, {
|
|
618
|
+
remoteHead: syncState.upstreamHead.global,
|
|
619
|
+
})
|
|
718
620
|
|
|
719
|
-
|
|
720
|
-
const syncState = yield* Effect.fromNullable(yield* syncStateSref).pipe(Effect.orDieDebugger)
|
|
621
|
+
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
|
721
622
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
623
|
+
yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
|
|
624
|
+
// TODO only take from queue while connected
|
|
625
|
+
Stream.tap(({ batch, pageInfo }) =>
|
|
626
|
+
Effect.gen(function* () {
|
|
627
|
+
// Any response (including an empty batch) proves backend contact, even with stale connectivity.
|
|
628
|
+
onPullSuccess()
|
|
629
|
+
// NOTE we only want to take process events when the sync backend is connected
|
|
630
|
+
// (e.g. needed for simulating being offline)
|
|
631
|
+
// TODO remove when there's a better way to handle this in stream above
|
|
632
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
633
|
+
yield* onNewPullChunk(
|
|
634
|
+
batch.map((_) =>
|
|
635
|
+
LiveStoreEvent.Client.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
|
636
|
+
syncMetadata: _.metadata,
|
|
637
|
+
// 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
|
|
638
|
+
// This is a bug and needs to be fixed https://github.com/livestorejs/livestore/issues/503#issuecomment-3114533165
|
|
639
|
+
materializerHashLeader: hashMaterializerResult(LiveStoreEvent.Global.toClientEncoded(_.eventEncoded)),
|
|
640
|
+
materializerHashSession: Option.none(),
|
|
641
|
+
}),
|
|
642
|
+
),
|
|
643
|
+
pageInfo,
|
|
644
|
+
)
|
|
645
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
|
646
|
+
}),
|
|
647
|
+
),
|
|
648
|
+
Stream.runDrain,
|
|
649
|
+
Effect.interruptible,
|
|
650
|
+
Effect.ensuring(releasePullMutexIfHeld),
|
|
651
|
+
)
|
|
726
652
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
isClientEvent,
|
|
731
|
-
isEqualEvent: LiveStoreEvent.Client.isEqualEncoded,
|
|
732
|
-
ignoreClientEvents: true,
|
|
733
|
-
})
|
|
653
|
+
// Should only ever happen when livePull is false
|
|
654
|
+
yield* Effect.logDebug('backend-pulling finished', { livePull })
|
|
655
|
+
})
|
|
734
656
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
657
|
+
const backgroundBackendPushing = Effect.gen(function* () {
|
|
658
|
+
if (syncBackend === undefined) return
|
|
738
659
|
|
|
739
|
-
|
|
660
|
+
while (true) {
|
|
661
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
740
662
|
|
|
741
|
-
|
|
663
|
+
const queueItems = yield* TxQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
|
|
742
664
|
|
|
743
|
-
|
|
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) } : {}),
|
|
749
|
-
})
|
|
665
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
750
666
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
})
|
|
755
|
-
yield* restartBackendPushing(globalRebasedPendingEvents)
|
|
667
|
+
if (devtoolsLatch !== undefined) {
|
|
668
|
+
yield* devtoolsLatch.await
|
|
669
|
+
}
|
|
756
670
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
eventNumsToRollback: mergeResult.rollbackEvents.map((_) => _.seqNum),
|
|
762
|
-
})
|
|
763
|
-
}
|
|
671
|
+
yield* Effect.spanEvent('backend-push', {
|
|
672
|
+
batchSize: queueItems.length,
|
|
673
|
+
...(TRACE_VERBOSE === true ? { batch: jsonStringify(queueItems) } : {}),
|
|
674
|
+
})
|
|
764
675
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
...(TRACE_VERBOSE === true ? { mergeResult: jsonStringify(mergeResult) } : {}),
|
|
773
|
-
})
|
|
676
|
+
// Push with declarative retry/backoff using Effect schedules
|
|
677
|
+
// - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
|
|
678
|
+
// - Delay clamped at 30s (continues retrying at 30s)
|
|
679
|
+
// - Resets automatically after successful push
|
|
680
|
+
// TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
|
|
681
|
+
yield* Effect.gen(function* () {
|
|
682
|
+
const iteration = yield* Schedule.CurrentMetadata
|
|
774
683
|
|
|
775
|
-
|
|
776
|
-
const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
777
|
-
const eventDef = schema.eventsDefsMap.get(event.name)
|
|
778
|
-
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
779
|
-
})
|
|
780
|
-
yield* restartBackendPushing(globalPendingEvents)
|
|
684
|
+
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.result)
|
|
781
685
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
686
|
+
const retries = iteration.attempt
|
|
687
|
+
if (retries > 0 && Result.isSuccess(pushResult) === true) {
|
|
688
|
+
yield* Effect.spanEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
|
|
689
|
+
}
|
|
786
690
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
691
|
+
if (Result.isFailure(pushResult) === true) {
|
|
692
|
+
yield* Effect.spanEvent('backend-push-error', {
|
|
693
|
+
error: pushResult.failure.toString(),
|
|
694
|
+
retries,
|
|
695
|
+
batchSize: queueItems.length,
|
|
696
|
+
})
|
|
697
|
+
const error = pushResult.failure
|
|
698
|
+
if (error._tag === 'ServerAheadError') {
|
|
699
|
+
// It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
|
|
700
|
+
yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
|
|
701
|
+
return yield* Effect.never
|
|
796
702
|
}
|
|
703
|
+
|
|
704
|
+
return yield* error
|
|
797
705
|
}
|
|
706
|
+
}).pipe(
|
|
707
|
+
// Retry transient errors
|
|
708
|
+
Effect.retry({
|
|
709
|
+
schedule: Schedule.exponential(Duration.seconds(1)).pipe(
|
|
710
|
+
Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.seconds(30)))), // Cap delay at 30s intervals.
|
|
711
|
+
),
|
|
712
|
+
while: (error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError',
|
|
713
|
+
}),
|
|
714
|
+
// This is needed to narrow the Error type. Our retry policy runs indefinitely, but Effect.retry does not narrow the Error type.
|
|
715
|
+
Effect.catchIf((error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError', Effect.die),
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
}).pipe(Effect.interruptible)
|
|
798
719
|
|
|
799
|
-
|
|
800
|
-
|
|
720
|
+
// NOTE: New events are only pushed to sync backend after successful local push processing
|
|
721
|
+
const push: Service['push'] = (newEvents) =>
|
|
722
|
+
Effect.gen(function* () {
|
|
723
|
+
if (newEvents.length === 0) return
|
|
801
724
|
|
|
802
|
-
|
|
725
|
+
const deferreds = yield* Effect.forEach(newEvents, () =>
|
|
726
|
+
Deferred.make<void, LeaderAheadError | StaleRebaseGenerationError>(),
|
|
727
|
+
)
|
|
803
728
|
|
|
804
|
-
|
|
729
|
+
const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i]] as LocalPushQueueItem)
|
|
805
730
|
|
|
806
|
-
|
|
807
|
-
|
|
731
|
+
// Validation, reservation, enqueueing, and fence advancement form one admission transaction.
|
|
732
|
+
// Serializing them prevents concurrent sessions from both validating against the same head and
|
|
733
|
+
// prevents backend reconciliation from changing the fence midway through admission.
|
|
734
|
+
yield* pushAdmissionSemaphore.withPermits(1)(
|
|
735
|
+
// Cancellation must not leave reservations, queue contents, and the admission fence disagreeing.
|
|
736
|
+
Effect.gen(function* () {
|
|
737
|
+
yield* validatePushBatch(newEvents, pushHeadRef.current, isClientOnlyEvent)
|
|
738
|
+
for (const item of items) reservedLocalPushItems.add(item)
|
|
739
|
+
yield* TxQueue.offerAll(localPushesQueue, items)
|
|
740
|
+
pushHeadRef.current = newEvents.at(-1)!.seqNum
|
|
741
|
+
if (testing.hooks?.localPushAdmitted !== undefined) {
|
|
742
|
+
yield* testing.hooks.localPushAdmitted(newEvents)
|
|
743
|
+
}
|
|
744
|
+
}).pipe(Effect.uninterruptible),
|
|
745
|
+
)
|
|
808
746
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
747
|
+
yield* Effect.forEach(deferreds, Deferred.await)
|
|
748
|
+
}).pipe(
|
|
749
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
|
|
750
|
+
attributes: {
|
|
751
|
+
batchSize: newEvents.length,
|
|
752
|
+
batch: TRACE_VERBOSE === true ? newEvents : undefined,
|
|
753
|
+
},
|
|
754
|
+
links: span !== undefined ? [{ span, attributes: {} }] : undefined,
|
|
755
|
+
}),
|
|
756
|
+
)
|
|
813
757
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
758
|
+
return LeaderSyncProcessor.of({
|
|
759
|
+
[TypeId]: TypeId,
|
|
760
|
+
// Starts various background loops
|
|
761
|
+
boot: Effect.gen(function* () {
|
|
762
|
+
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
|
763
|
+
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
|
818
764
|
|
|
819
|
-
|
|
820
|
-
|
|
765
|
+
// Rehydrate sync queue
|
|
766
|
+
if (initialSyncState.pending.length > 0) {
|
|
767
|
+
const globalOrUnknownPendingEvents = initialSyncState.pending
|
|
768
|
+
// Don't sync client-only events
|
|
769
|
+
.filter((eventEncoded) => !isClientOnlyEvent(eventEncoded))
|
|
821
770
|
|
|
822
|
-
|
|
771
|
+
if (globalOrUnknownPendingEvents.length > 0) {
|
|
772
|
+
yield* TxQueue.offerAll(syncBackendPushQueue, globalOrUnknownPendingEvents)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
823
775
|
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
)
|
|
776
|
+
const handleBackendIdMismatchError = (error: BackendIdMismatchError) =>
|
|
777
|
+
handleBackendIdMismatch({ error, onBackendIdMismatch, shutdownChannel, dbEventlog, dbState })
|
|
851
778
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
779
|
+
const maybeShutdownOnError = (cause: Cause.Cause<UnknownError | MaterializeError>) =>
|
|
780
|
+
Effect.gen(function* () {
|
|
781
|
+
if (onError === 'ignore') {
|
|
782
|
+
if (LS_DEV === true) {
|
|
783
|
+
yield* Effect.logDebug(
|
|
784
|
+
`Ignoring sync error (${Option.getOrUndefined(Cause.findErrorOption(cause))?._tag ?? cause.toString()})`,
|
|
785
|
+
Cause.pretty(cause),
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
return
|
|
789
|
+
}
|
|
855
790
|
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
backendPushBatchSize,
|
|
860
|
-
}: {
|
|
861
|
-
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.Client.EncodedWithMeta>
|
|
862
|
-
devtoolsLatch: Effect.Latch | undefined
|
|
863
|
-
backendPushBatchSize: number
|
|
864
|
-
}) {
|
|
865
|
-
const { syncBackend } = yield* LeaderThreadCtx
|
|
866
|
-
if (syncBackend === undefined) return
|
|
791
|
+
const error = Option.getOrUndefined(Cause.findErrorOption(cause))
|
|
792
|
+
const errorToSend = error === undefined ? UnknownError.make({ cause }) : error
|
|
793
|
+
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
|
867
794
|
|
|
868
|
-
|
|
869
|
-
|
|
795
|
+
return yield* Effect.failCause(cause).pipe(Effect.orDie)
|
|
796
|
+
})
|
|
870
797
|
|
|
871
|
-
|
|
798
|
+
yield* backgroundApplyLocalPushes.pipe(Effect.catchCause(maybeShutdownOnError), Effect.forkScoped)
|
|
872
799
|
|
|
873
|
-
|
|
800
|
+
const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
|
|
801
|
+
const backendPushingEffect = backgroundBackendPushing.pipe(
|
|
802
|
+
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
803
|
+
Effect.catchCause(maybeShutdownOnError),
|
|
804
|
+
)
|
|
874
805
|
|
|
875
|
-
|
|
876
|
-
yield* devtoolsLatch.await
|
|
877
|
-
}
|
|
806
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
878
807
|
|
|
879
|
-
|
|
880
|
-
batchSize: queueItems.length,
|
|
881
|
-
...(TRACE_VERBOSE === true ? { batch: jsonStringify(queueItems) } : {}),
|
|
882
|
-
})
|
|
808
|
+
let offlinePullFallbackMs = 1_000
|
|
883
809
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
810
|
+
yield* backgroundBackendPulling({
|
|
811
|
+
onPullSuccess: () => {
|
|
812
|
+
offlinePullFallbackMs = 1_000
|
|
813
|
+
},
|
|
814
|
+
restartBackendPushing: (filteredRebasedPending) =>
|
|
815
|
+
Effect.gen(function* () {
|
|
816
|
+
// Stop current pushing fiber
|
|
817
|
+
yield* FiberHandle.clear(backendPushingFiberHandle)
|
|
891
818
|
|
|
892
|
-
|
|
819
|
+
// Reset the sync backend push queue
|
|
820
|
+
yield* TxQueue.clear(syncBackendPushQueue)
|
|
821
|
+
yield* TxQueue.offerAll(syncBackendPushQueue, filteredRebasedPending)
|
|
893
822
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
823
|
+
// Restart pushing fiber
|
|
824
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
825
|
+
}),
|
|
826
|
+
}).pipe(
|
|
827
|
+
Effect.tapError((error) => {
|
|
828
|
+
if (error._tag !== 'IsOfflineError' || syncBackend === undefined) return Effect.void
|
|
829
|
+
const isConnected = syncBackend.isConnected
|
|
830
|
+
return Effect.gen(function* () {
|
|
831
|
+
// Wake immediately on reconnection. Probe on a capped fallback timer if connectivity is stale.
|
|
832
|
+
const jitter = yield* Random.nextBetween(0.8, 1.2)
|
|
833
|
+
const fallbackMs =
|
|
834
|
+
testing.delays?.backendPullReconnect?.(offlinePullFallbackMs * jitter) ?? offlinePullFallbackMs * jitter
|
|
835
|
+
offlinePullFallbackMs = Math.min(offlinePullFallbackMs * 2, 30_000)
|
|
836
|
+
// A stale initial true is not a reconnect. Watch false→true even while the fallback timer is pending.
|
|
837
|
+
const waitHook = testing.hooks?.backendPullReconnectWait
|
|
838
|
+
const changes = SubscriptionRef.changes(isConnected)
|
|
839
|
+
const observedChanges =
|
|
840
|
+
waitHook === undefined
|
|
841
|
+
? changes
|
|
842
|
+
: changes.pipe(Stream.tap((value) => waitHook({ isConnected: value, fallbackMs })))
|
|
843
|
+
const reconnected = yield* observedChanges.pipe(
|
|
844
|
+
Stream.dropWhile((value) => value === true),
|
|
845
|
+
Stream.filter((value) => value === true),
|
|
846
|
+
Stream.runHead,
|
|
847
|
+
Effect.as(true),
|
|
848
|
+
Effect.timeoutOrElse({ duration: fallbackMs, orElse: () => Effect.succeed(false) }),
|
|
849
|
+
)
|
|
850
|
+
if (reconnected === true) offlinePullFallbackMs = 1_000
|
|
851
|
+
})
|
|
852
|
+
}),
|
|
853
|
+
Effect.retry({
|
|
854
|
+
// The unbounded offline-only retry keeps IsOfflineError out of the terminal error channel.
|
|
855
|
+
until: (error): error is Exclude<typeof error, IsOfflineError> => error._tag !== 'IsOfflineError',
|
|
856
|
+
}),
|
|
857
|
+
Effect.catchTag('BackendIdMismatchError', handleBackendIdMismatchError),
|
|
858
|
+
Effect.catchCause(maybeShutdownOnError),
|
|
859
|
+
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
|
860
|
+
// This might be a bug in Effect. Only seems to happen in the browser.
|
|
861
|
+
Effect.provideService(References.UnhandledLogLevel, undefined),
|
|
862
|
+
Effect.forkScoped,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
return { initialLeaderHead: initialSyncState.localHead }
|
|
866
|
+
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot')),
|
|
867
|
+
push,
|
|
868
|
+
pushPartial: ({ event: { name, args }, clientId, sessionId }) =>
|
|
869
|
+
Effect.gen(function* () {
|
|
870
|
+
const syncState = yield* Effect.fromNullishOr(yield* SubscriptionRef.get(syncStateSref)).pipe(
|
|
871
|
+
Effect.orDieDebugger,
|
|
872
|
+
)
|
|
898
873
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
874
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
875
|
+
operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
|
|
876
|
+
event: {
|
|
877
|
+
name,
|
|
878
|
+
args,
|
|
879
|
+
clientId,
|
|
880
|
+
sessionId,
|
|
881
|
+
seqNum: syncState.localHead,
|
|
882
|
+
},
|
|
904
883
|
})
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
return
|
|
884
|
+
|
|
885
|
+
if (resolution._tag === 'unknown') {
|
|
886
|
+
// Ignore partial pushes for unrecognised events – they are still
|
|
887
|
+
// persisted server-side once a schema update ships.
|
|
888
|
+
return
|
|
910
889
|
}
|
|
911
890
|
|
|
912
|
-
|
|
891
|
+
const eventEncoded = new LiveStoreEvent.Client.EncodedWithMeta({
|
|
892
|
+
name,
|
|
893
|
+
args,
|
|
894
|
+
clientId,
|
|
895
|
+
sessionId,
|
|
896
|
+
...EventSequenceNumber.Client.nextPair({
|
|
897
|
+
seqNum: syncState.localHead,
|
|
898
|
+
isClientOnly: resolution.eventDef.options.clientOnly,
|
|
899
|
+
}),
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
yield* push([eventEncoded])
|
|
903
|
+
}).pipe(
|
|
904
|
+
// pushPartial constructs the event sequence number internally, so these errors should never happen.
|
|
905
|
+
Effect.catchIf(isRejectedPushError, Effect.die),
|
|
906
|
+
),
|
|
907
|
+
pull: ({ cursor }) =>
|
|
908
|
+
Effect.gen(function* () {
|
|
909
|
+
const queue = yield* connectedClientSessionPullQueues.makeQueue(cursor)
|
|
910
|
+
return Stream.fromQueue(queue)
|
|
911
|
+
}).pipe(Stream.unwrap),
|
|
912
|
+
/*
|
|
913
|
+
Notes for a potential new `LeaderSyncProcessor.pull` implementation:
|
|
914
|
+
|
|
915
|
+
- Doesn't take cursor but is "atomically called" in the leader during the snapshot phase
|
|
916
|
+
- TODO: how is this done "atomically" in the web adapter where the snapshot is read optimistically?
|
|
917
|
+
- Would require a new kind of "boot-phase" API which is stream based:
|
|
918
|
+
- initial message: state snapshot + seq num head
|
|
919
|
+
- subsequent messages: sync state payloads
|
|
920
|
+
|
|
921
|
+
- alternative: instead of session pulling sync state payloads from leader, we could send
|
|
922
|
+
- events in the "advance" case
|
|
923
|
+
- full new state db snapshot in the "rebase" case
|
|
924
|
+
- downside: importing the snapshot is expensive
|
|
925
|
+
*/
|
|
926
|
+
pullQueue: ({ cursor }) => connectedClientSessionPullQueues.makeQueue(cursor),
|
|
927
|
+
syncState: Subscribable.make({
|
|
928
|
+
get: SubscriptionRef.get(syncStateSref).pipe(Effect.flatMap(Effect.fromNullishOr), Effect.orDieDebugger),
|
|
929
|
+
changes: SubscriptionRef.changes(syncStateSref).pipe(Stream.filter(Predicate.isNotUndefined)),
|
|
930
|
+
}),
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
export const layer = (options: Options) => Layer.effect(LeaderSyncProcessor, make(options))
|
|
935
|
+
|
|
936
|
+
// TODO how to handle errors gracefully
|
|
937
|
+
const makeMaterializeEventsBatch =
|
|
938
|
+
({
|
|
939
|
+
dbState,
|
|
940
|
+
dbEventlog,
|
|
941
|
+
materializeEvent,
|
|
942
|
+
}: {
|
|
943
|
+
dbState: SqliteDb
|
|
944
|
+
dbEventlog: SqliteDb
|
|
945
|
+
materializeEvent: MaterializeEvent
|
|
946
|
+
}) =>
|
|
947
|
+
({ batchItems }: { batchItems: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta> }) =>
|
|
948
|
+
Effect.gen(function* () {
|
|
949
|
+
// NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
|
|
950
|
+
dbState.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
951
|
+
dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
|
952
|
+
|
|
953
|
+
yield* Effect.addFinalizer((exit) =>
|
|
954
|
+
Effect.gen(function* () {
|
|
955
|
+
if (Exit.isSuccess(exit) === true) return
|
|
956
|
+
|
|
957
|
+
// Rollback in case of an error
|
|
958
|
+
dbState.execute('ROLLBACK', undefined)
|
|
959
|
+
dbEventlog.execute('ROLLBACK', undefined)
|
|
960
|
+
}),
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
for (let i = 0; i < batchItems.length; i++) {
|
|
964
|
+
const { sessionChangeset, hash } = yield* materializeEvent(batchItems[i]!)
|
|
965
|
+
batchItems[i]!.meta.sessionChangeset = sessionChangeset
|
|
966
|
+
batchItems[i]!.meta.materializerHashLeader = hash
|
|
913
967
|
}
|
|
968
|
+
|
|
969
|
+
dbState.execute('COMMIT', undefined) // Commit the transaction
|
|
970
|
+
dbEventlog.execute('COMMIT', undefined) // Commit the transaction
|
|
914
971
|
}).pipe(
|
|
915
|
-
|
|
916
|
-
Effect.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
),
|
|
920
|
-
while: (error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError',
|
|
972
|
+
Effect.uninterruptible,
|
|
973
|
+
Effect.scoped,
|
|
974
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
|
|
975
|
+
attributes: { batchSize: batchItems.length },
|
|
921
976
|
}),
|
|
922
|
-
|
|
923
|
-
Effect.catchIf((error) => error._tag === 'IsOfflineError' || error._tag === 'UnknownError', Effect.die),
|
|
977
|
+
Effect.tapCauseLogPretty,
|
|
924
978
|
)
|
|
925
|
-
}
|
|
926
|
-
}, Effect.interruptible)
|
|
927
979
|
|
|
928
980
|
const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Composite) => {
|
|
929
981
|
// Since we're using the session changeset rows to query for the current head,
|
|
@@ -934,11 +986,7 @@ const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.Client.Com
|
|
|
934
986
|
interface PullQueueSet {
|
|
935
987
|
makeQueue: (
|
|
936
988
|
cursor: EventSequenceNumber.Client.Composite,
|
|
937
|
-
) => Effect.Effect<
|
|
938
|
-
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>,
|
|
939
|
-
never,
|
|
940
|
-
Scope.Scope | LeaderThreadCtx
|
|
941
|
-
>
|
|
989
|
+
) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, never, Scope.Scope>
|
|
942
990
|
offer: (item: {
|
|
943
991
|
payload: typeof SyncState.PayloadUpstream.Type
|
|
944
992
|
leaderHead: EventSequenceNumber.Client.Composite
|
|
@@ -964,9 +1012,12 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
964
1012
|
|
|
965
1013
|
const makeQueue: PullQueueSet['makeQueue'] = (cursor) =>
|
|
966
1014
|
Effect.gen(function* () {
|
|
967
|
-
const queue = yield*
|
|
968
|
-
|
|
969
|
-
|
|
1015
|
+
const queue = yield* Effect.acquireRelease(
|
|
1016
|
+
Queue.unbounded<{
|
|
1017
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
|
1018
|
+
}>(),
|
|
1019
|
+
Queue.shutdown,
|
|
1020
|
+
)
|
|
970
1021
|
|
|
971
1022
|
yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
|
|
972
1023
|
|
|
@@ -1019,7 +1070,7 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
1019
1070
|
// ]),
|
|
1020
1071
|
// )
|
|
1021
1072
|
|
|
1022
|
-
yield*
|
|
1073
|
+
yield* Queue.offerAll(queue, payloadsSinceCursor)
|
|
1023
1074
|
|
|
1024
1075
|
set.add(queue)
|
|
1025
1076
|
|
|
@@ -1061,6 +1112,7 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
1061
1112
|
const validatePushBatch = (
|
|
1062
1113
|
batch: ReadonlyArray<LiveStoreEvent.Client.EncodedWithMeta>,
|
|
1063
1114
|
pushHead: EventSequenceNumber.Client.Composite,
|
|
1115
|
+
isClientOnlyEvent: (event: LiveStoreEvent.Client.EncodedWithMeta) => boolean,
|
|
1064
1116
|
) =>
|
|
1065
1117
|
Effect.gen(function* () {
|
|
1066
1118
|
if (batch.length === 0) {
|
|
@@ -1088,8 +1140,56 @@ const validatePushBatch = (
|
|
|
1088
1140
|
sessionId: batch[0]!.sessionId,
|
|
1089
1141
|
})
|
|
1090
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
// A rebase replaces the optimistic history that the client built on. Events from an older
|
|
1145
|
+
// generation may now have the wrong parent and must be recreated from the leader's current head.
|
|
1146
|
+
if (batch[0]!.seqNum.rebaseGeneration < pushHead.rebaseGeneration) {
|
|
1147
|
+
return yield* StaleRebaseGenerationError.make({
|
|
1148
|
+
currentRebaseGeneration: pushHead.rebaseGeneration,
|
|
1149
|
+
providedRebaseGeneration: batch[0]!.seqNum.rebaseGeneration,
|
|
1150
|
+
sessionId: batch[0]!.sessionId,
|
|
1151
|
+
})
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Validate the batch as one unbroken chain starting at the admission fence. Checking only that
|
|
1155
|
+
// numbers increase would still allow gaps or events that point at an unrelated parent.
|
|
1156
|
+
let precedingSeqNum = pushHead
|
|
1157
|
+
for (let i = 0; i < batch.length; i++) {
|
|
1158
|
+
const event = batch[i]!
|
|
1159
|
+
// Global events advance the global position; client-only events advance its client-local suffix.
|
|
1160
|
+
const expectedPair = EventSequenceNumber.Client.nextPair({
|
|
1161
|
+
seqNum: precedingSeqNum,
|
|
1162
|
+
isClientOnly: isClientOnlyEvent(event),
|
|
1163
|
+
rebaseGeneration: event.seqNum.rebaseGeneration,
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
if (
|
|
1167
|
+
EventSequenceNumber.Client.isEqual(event.seqNum, expectedPair.seqNum) === false ||
|
|
1168
|
+
isSameSequencePosition(event.parentSeqNum, expectedPair.parentSeqNum) === false
|
|
1169
|
+
) {
|
|
1170
|
+
return yield* NonContiguousBatchError.make({
|
|
1171
|
+
expectedSeqNum: expectedPair.seqNum,
|
|
1172
|
+
providedSeqNum: event.seqNum,
|
|
1173
|
+
expectedParentSeqNum: expectedPair.parentSeqNum,
|
|
1174
|
+
providedParentSeqNum: event.parentSeqNum,
|
|
1175
|
+
violationIndex: i,
|
|
1176
|
+
sessionId: event.sessionId,
|
|
1177
|
+
})
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
precedingSeqNum = event.seqNum
|
|
1181
|
+
}
|
|
1091
1182
|
})
|
|
1092
1183
|
|
|
1184
|
+
/**
|
|
1185
|
+
* Parent linkage identifies a position in the event chain. Rebase generation describes the version
|
|
1186
|
+
* of optimistic history, so it is validated on the event itself rather than as part of parent identity.
|
|
1187
|
+
*/
|
|
1188
|
+
const isSameSequencePosition = (
|
|
1189
|
+
left: EventSequenceNumber.Client.Composite,
|
|
1190
|
+
right: EventSequenceNumber.Client.Composite,
|
|
1191
|
+
) => left.global === right.global && left.client === right.client
|
|
1192
|
+
|
|
1093
1193
|
/**
|
|
1094
1194
|
* Handles a BackendIdMismatchError based on the configured behavior.
|
|
1095
1195
|
* This occurs when the sync backend has been reset and has a new identity.
|
|
@@ -1098,13 +1198,15 @@ const handleBackendIdMismatch = Effect.fn('@livestore/common:LeaderSyncProcessor
|
|
|
1098
1198
|
error,
|
|
1099
1199
|
onBackendIdMismatch,
|
|
1100
1200
|
shutdownChannel,
|
|
1201
|
+
dbEventlog,
|
|
1202
|
+
dbState,
|
|
1101
1203
|
}: {
|
|
1102
1204
|
error: BackendIdMismatchError
|
|
1103
1205
|
onBackendIdMismatch: 'reset' | 'shutdown' | 'ignore'
|
|
1104
1206
|
shutdownChannel: ShutdownChannel
|
|
1207
|
+
dbEventlog: SqliteDb
|
|
1208
|
+
dbState: SqliteDb
|
|
1105
1209
|
}) {
|
|
1106
|
-
const { dbEventlog, dbState } = yield* LeaderThreadCtx
|
|
1107
|
-
|
|
1108
1210
|
if (onBackendIdMismatch === 'reset') {
|
|
1109
1211
|
yield* Effect.logWarning(
|
|
1110
1212
|
'Sync backend identity changed (backend was reset). Clearing local storage and shutting down.',
|
|
@@ -1158,3 +1260,28 @@ const clearLocalDatabases = ({ dbEventlog, dbState }: { dbEventlog: SqliteDb; db
|
|
|
1158
1260
|
dbState.execute(`DROP TABLE IF EXISTS "${name}"`)
|
|
1159
1261
|
}
|
|
1160
1262
|
})
|
|
1263
|
+
|
|
1264
|
+
const snapshotTxQueue = <A>(queue: TxQueue.TxQueue<A>): Effect.Effect<ReadonlyArray<A>> =>
|
|
1265
|
+
Effect.tx(
|
|
1266
|
+
Effect.gen(function* () {
|
|
1267
|
+
const items = yield* TxQueue.clear(queue)
|
|
1268
|
+
yield* TxQueue.offerAll(queue, items)
|
|
1269
|
+
return items
|
|
1270
|
+
}),
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
const takePrefixUntil = <A>(
|
|
1274
|
+
queue: TxQueue.TxQueue<A>,
|
|
1275
|
+
predicate: (value: A) => boolean,
|
|
1276
|
+
): Effect.Effect<ReadonlyArray<A>> =>
|
|
1277
|
+
Effect.tx(
|
|
1278
|
+
Effect.gen(function* () {
|
|
1279
|
+
const items = yield* TxQueue.clear(queue)
|
|
1280
|
+
const [prefix, rest] = ReadonlyArray.splitWhere(items, predicate)
|
|
1281
|
+
yield* TxQueue.offerAll(queue, rest)
|
|
1282
|
+
return prefix
|
|
1283
|
+
}),
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
/** Serialize value to JSON string for trace attributes */
|
|
1287
|
+
const jsonStringify = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))
|