@livestore/common 0.3.0-dev.5 → 0.3.0-dev.51
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/__tests__/fixture.d.ts +83 -221
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +33 -11
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/adapter-types.d.ts +120 -64
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +39 -8
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/debug-info.d.ts +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +1 -0
- package/dist/debug-info.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +390 -0
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -0
- package/dist/devtools/devtools-messages-client-session.js +97 -0
- package/dist/devtools/devtools-messages-client-session.js.map +1 -0
- package/dist/devtools/devtools-messages-common.d.ts +68 -0
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -0
- package/dist/devtools/devtools-messages-common.js +60 -0
- package/dist/devtools/devtools-messages-common.js.map +1 -0
- package/dist/devtools/devtools-messages-leader.d.ts +394 -0
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -0
- package/dist/devtools/devtools-messages-leader.js +147 -0
- package/dist/devtools/devtools-messages-leader.js.map +1 -0
- package/dist/devtools/devtools-messages.d.ts +3 -580
- package/dist/devtools/devtools-messages.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.js +3 -174
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/devtools/devtools-sessioninfo.d.ts +32 -0
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -0
- package/dist/devtools/devtools-sessioninfo.js +36 -0
- package/dist/devtools/devtools-sessioninfo.js.map +1 -0
- package/dist/devtools/mod.d.ts +55 -0
- package/dist/devtools/mod.d.ts.map +1 -0
- package/dist/devtools/mod.js +33 -0
- package/dist/devtools/mod.js.map +1 -0
- package/dist/index.d.ts +7 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -9
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +62 -0
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
- package/dist/leader-thread/LeaderSyncProcessor.js +595 -0
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
- package/dist/leader-thread/connection.d.ts +34 -6
- package/dist/leader-thread/connection.d.ts.map +1 -1
- package/dist/leader-thread/connection.js +22 -7
- package/dist/leader-thread/connection.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +27 -0
- package/dist/leader-thread/eventlog.d.ts.map +1 -0
- package/dist/leader-thread/eventlog.js +119 -0
- package/dist/leader-thread/eventlog.js.map +1 -0
- 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 +155 -80
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +23 -11
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +72 -47
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts +16 -0
- package/dist/leader-thread/materialize-event.d.ts.map +1 -0
- package/dist/leader-thread/materialize-event.js +109 -0
- package/dist/leader-thread/materialize-event.js.map +1 -0
- package/dist/leader-thread/mod.d.ts +1 -1
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -1
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +4 -2
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +33 -31
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -5
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -4
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +87 -40
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js +1 -3
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/make-client-session.d.ts +23 -0
- package/dist/make-client-session.d.ts.map +1 -0
- package/dist/make-client-session.js +57 -0
- package/dist/make-client-session.js.map +1 -0
- package/dist/materializer-helper.d.ts +23 -0
- package/dist/materializer-helper.d.ts.map +1 -0
- package/dist/materializer-helper.js +86 -0
- package/dist/materializer-helper.js.map +1 -0
- package/dist/otel.d.ts +2 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +5 -0
- package/dist/otel.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +14 -0
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -0
- package/dist/rematerialize-from-eventlog.js +64 -0
- package/dist/rematerialize-from-eventlog.js.map +1 -0
- package/dist/schema/EventDef.d.ts +146 -0
- package/dist/schema/EventDef.d.ts.map +1 -0
- package/dist/schema/EventDef.js +58 -0
- package/dist/schema/EventDef.js.map +1 -0
- package/dist/schema/EventId.d.ts +43 -25
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +56 -18
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/EventId.test.d.ts +2 -0
- package/dist/schema/EventId.test.d.ts.map +1 -0
- package/dist/schema/EventId.test.js +11 -0
- package/dist/schema/EventId.test.js.map +1 -0
- package/dist/schema/EventNumber.d.ts +57 -0
- package/dist/schema/EventNumber.d.ts.map +1 -0
- package/dist/schema/EventNumber.js +82 -0
- package/dist/schema/EventNumber.js.map +1 -0
- package/dist/schema/EventNumber.test.d.ts +2 -0
- package/dist/schema/EventNumber.test.d.ts.map +1 -0
- package/dist/schema/EventNumber.test.js +11 -0
- package/dist/schema/EventNumber.test.js.map +1 -0
- package/dist/schema/EventSequenceNumber.d.ts +57 -0
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -0
- package/dist/schema/EventSequenceNumber.js +82 -0
- package/dist/schema/EventSequenceNumber.js.map +1 -0
- package/dist/schema/EventSequenceNumber.test.d.ts +2 -0
- package/dist/schema/EventSequenceNumber.test.d.ts.map +1 -0
- package/dist/schema/EventSequenceNumber.test.js +11 -0
- package/dist/schema/EventSequenceNumber.test.js.map +1 -0
- package/dist/schema/LiveStoreEvent.d.ts +257 -0
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
- package/dist/schema/LiveStoreEvent.js +117 -0
- package/dist/schema/LiveStoreEvent.js.map +1 -0
- package/dist/schema/events.d.ts +2 -0
- package/dist/schema/events.d.ts.map +1 -0
- package/dist/schema/events.js +2 -0
- package/dist/schema/events.js.map +1 -0
- package/dist/schema/mod.d.ts +8 -6
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +8 -6
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +50 -32
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +36 -43
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/mod.d.ts +3 -0
- package/dist/schema/state/mod.d.ts.map +1 -0
- package/dist/schema/state/mod.js +3 -0
- package/dist/schema/state/mod.js.map +1 -0
- package/dist/schema/state/sqlite/client-document-def.d.ts +223 -0
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -0
- package/dist/schema/state/sqlite/client-document-def.js +170 -0
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -0
- package/dist/schema/state/sqlite/client-document-def.test.d.ts +2 -0
- package/dist/schema/state/sqlite/client-document-def.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/client-document-def.test.js +201 -0
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +69 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +71 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/validate.js +12 -0
- package/dist/schema/state/sqlite/db-schema/ast/validate.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +90 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +87 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts +2 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +29 -0
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +90 -0
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +41 -0
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/hash.d.ts +2 -0
- package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/hash.js +14 -0
- package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/mod.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/mod.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/mod.js +3 -0
- package/dist/schema/state/sqlite/db-schema/mod.js.map +1 -0
- package/dist/schema/state/sqlite/mod.d.ts +17 -0
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -0
- package/dist/schema/state/sqlite/mod.js +41 -0
- package/dist/schema/state/sqlite/mod.js.map +1 -0
- package/dist/schema/state/sqlite/query-builder/api.d.ts +294 -0
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -0
- package/dist/schema/state/sqlite/query-builder/api.js +6 -0
- package/dist/schema/state/sqlite/query-builder/api.js.map +1 -0
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +7 -0
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -0
- package/dist/schema/state/sqlite/query-builder/astToSql.js +190 -0
- package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -0
- package/dist/schema/state/sqlite/query-builder/impl.d.ts +7 -0
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -0
- package/dist/schema/state/sqlite/query-builder/impl.js +286 -0
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -0
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +87 -0
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/query-builder/impl.test.js +563 -0
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -0
- package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.d.ts +7 -0
- package/dist/schema/state/sqlite/query-builder/mod.d.ts.map +1 -0
- package/dist/{query-builder → schema/state/sqlite/query-builder}/mod.js +7 -0
- package/dist/schema/state/sqlite/query-builder/mod.js.map +1 -0
- package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -0
- package/dist/schema/{schema-helpers.js → state/sqlite/schema-helpers.js} +1 -1
- package/dist/schema/state/sqlite/schema-helpers.js.map +1 -0
- package/dist/schema/state/sqlite/system-tables.d.ts +574 -0
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -0
- package/dist/schema/state/sqlite/system-tables.js +88 -0
- package/dist/schema/state/sqlite/system-tables.js.map +1 -0
- package/dist/schema/state/sqlite/table-def.d.ts +84 -0
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -0
- package/dist/schema/state/sqlite/table-def.js +36 -0
- package/dist/schema/state/sqlite/table-def.js.map +1 -0
- package/dist/schema-management/common.d.ts +7 -7
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/common.js.map +1 -1
- package/dist/schema-management/migrations.d.ts +6 -6
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +27 -18
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-schema.d.ts +8 -0
- package/dist/schema-management/validate-schema.d.ts.map +1 -0
- package/dist/schema-management/validate-schema.js +39 -0
- package/dist/schema-management/validate-schema.js.map +1 -0
- package/dist/sql-queries/misc.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js.map +1 -1
- package/dist/sql-queries/types.d.ts +2 -1
- package/dist/sql-queries/types.d.ts.map +1 -1
- package/dist/sql-queries/types.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +66 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.js +209 -0
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +38 -35
- package/dist/sync/next/compact-events.js.map +1 -1
- package/dist/sync/next/facts.d.ts +21 -21
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +11 -11
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +9 -7
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +10 -5
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.d.ts +0 -2
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +16 -14
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +10 -8
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +18 -10
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.calculator.test.js +39 -34
- package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +77 -77
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/{mutation-fixtures.d.ts → event-fixtures.d.ts} +38 -28
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
- package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +81 -38
- package/dist/sync/next/test/event-fixtures.js.map +1 -0
- package/dist/sync/next/test/mod.d.ts +1 -1
- package/dist/sync/next/test/mod.d.ts.map +1 -1
- package/dist/sync/next/test/mod.js +1 -1
- package/dist/sync/next/test/mod.js.map +1 -1
- package/dist/sync/sync.d.ts +60 -25
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +10 -6
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +213 -82
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +337 -139
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +310 -286
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts +2 -2
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +4 -4
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/util.d.ts +2 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +13 -6
- package/src/__tests__/fixture.ts +36 -15
- package/src/adapter-types.ts +107 -68
- package/src/debug-info.ts +1 -0
- package/src/devtools/devtools-messages-client-session.ts +142 -0
- package/src/devtools/devtools-messages-common.ts +115 -0
- package/src/devtools/devtools-messages-leader.ts +191 -0
- package/src/devtools/devtools-messages.ts +3 -246
- package/src/devtools/devtools-sessioninfo.ts +101 -0
- package/src/devtools/mod.ts +59 -0
- package/src/index.ts +7 -15
- package/src/leader-thread/LeaderSyncProcessor.ts +940 -0
- package/src/leader-thread/connection.ts +54 -9
- package/src/leader-thread/eventlog.ts +199 -0
- package/src/leader-thread/leader-worker-devtools.ts +227 -104
- package/src/leader-thread/make-leader-thread-layer.ts +128 -78
- package/src/leader-thread/materialize-event.ts +173 -0
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/recreate-db.ts +38 -39
- package/src/leader-thread/shutdown-channel.ts +2 -4
- package/src/leader-thread/types.ts +96 -50
- package/src/make-client-session.ts +136 -0
- package/src/materializer-helper.ts +138 -0
- package/src/otel.ts +8 -0
- package/src/rematerialize-from-eventlog.ts +117 -0
- package/src/schema/EventDef.ts +227 -0
- package/src/schema/EventSequenceNumber.test.ts +12 -0
- package/src/schema/EventSequenceNumber.ts +121 -0
- package/src/schema/LiveStoreEvent.ts +240 -0
- package/src/schema/events.ts +1 -0
- package/src/schema/mod.ts +8 -6
- package/src/schema/schema.ts +88 -84
- package/src/schema/state/mod.ts +2 -0
- package/src/schema/state/sqlite/client-document-def.test.ts +238 -0
- package/src/schema/state/sqlite/client-document-def.ts +444 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +142 -0
- package/src/schema/state/sqlite/db-schema/ast/validate.ts +13 -0
- package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +206 -0
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +35 -0
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +242 -0
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +222 -0
- package/src/schema/state/sqlite/db-schema/hash.ts +14 -0
- package/src/schema/state/sqlite/db-schema/mod.ts +2 -0
- package/src/schema/state/sqlite/mod.ts +73 -0
- package/src/schema/state/sqlite/query-builder/api.ts +440 -0
- package/src/schema/state/sqlite/query-builder/astToSql.ts +232 -0
- package/src/schema/state/sqlite/query-builder/impl.test.ts +617 -0
- package/src/schema/state/sqlite/query-builder/impl.ts +351 -0
- package/src/{query-builder → schema/state/sqlite/query-builder}/mod.ts +7 -0
- package/src/schema/{schema-helpers.ts → state/sqlite/schema-helpers.ts} +1 -1
- package/src/schema/state/sqlite/system-tables.ts +117 -0
- package/src/schema/state/sqlite/table-def.ts +197 -0
- package/src/schema-management/common.ts +7 -7
- package/src/schema-management/migrations.ts +37 -31
- package/src/schema-management/validate-schema.ts +61 -0
- package/src/sql-queries/sql-queries.ts +1 -1
- package/src/sql-queries/sql-query-builder.ts +1 -2
- package/src/sql-queries/types.ts +3 -1
- package/src/sync/ClientSessionSyncProcessor.ts +332 -0
- package/src/sync/index.ts +1 -1
- package/src/sync/next/compact-events.ts +38 -35
- package/src/sync/next/facts.ts +43 -41
- package/src/sync/next/history-dag-common.ts +17 -10
- package/src/sync/next/history-dag.ts +16 -17
- package/src/sync/next/rebase-events.ts +29 -17
- package/src/sync/next/test/compact-events.calculator.test.ts +46 -46
- package/src/sync/next/test/compact-events.test.ts +79 -79
- package/src/sync/next/test/event-fixtures.ts +226 -0
- package/src/sync/next/test/mod.ts +1 -1
- package/src/sync/sync.ts +60 -24
- package/src/sync/syncstate.test.ts +347 -320
- package/src/sync/syncstate.ts +422 -230
- package/src/sync/validate-push-payload.ts +6 -6
- package/src/version.ts +2 -2
- package/dist/derived-mutations.d.ts +0 -109
- package/dist/derived-mutations.d.ts.map +0 -1
- package/dist/derived-mutations.js +0 -54
- package/dist/derived-mutations.js.map +0 -1
- package/dist/derived-mutations.test.d.ts +0 -2
- package/dist/derived-mutations.test.d.ts.map +0 -1
- package/dist/derived-mutations.test.js +0 -93
- package/dist/derived-mutations.test.js.map +0 -1
- package/dist/devtools/devtools-bridge.d.ts +0 -13
- package/dist/devtools/devtools-bridge.d.ts.map +0 -1
- package/dist/devtools/devtools-bridge.js +0 -2
- package/dist/devtools/devtools-bridge.js.map +0 -1
- package/dist/devtools/devtools-window-message.d.ts +0 -29
- package/dist/devtools/devtools-window-message.d.ts.map +0 -1
- package/dist/devtools/devtools-window-message.js +0 -33
- package/dist/devtools/devtools-window-message.js.map +0 -1
- package/dist/devtools/index.d.ts +0 -42
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/index.js +0 -48
- package/dist/devtools/index.js.map +0 -1
- package/dist/init-singleton-tables.d.ts +0 -4
- package/dist/init-singleton-tables.d.ts.map +0 -1
- package/dist/init-singleton-tables.js +0 -16
- package/dist/init-singleton-tables.js.map +0 -1
- package/dist/leader-thread/apply-mutation.d.ts +0 -8
- package/dist/leader-thread/apply-mutation.d.ts.map +0 -1
- package/dist/leader-thread/apply-mutation.js +0 -95
- package/dist/leader-thread/apply-mutation.js.map +0 -1
- package/dist/leader-thread/leader-sync-processor.d.ts +0 -47
- package/dist/leader-thread/leader-sync-processor.d.ts.map +0 -1
- package/dist/leader-thread/leader-sync-processor.js +0 -425
- package/dist/leader-thread/leader-sync-processor.js.map +0 -1
- package/dist/leader-thread/mutationlog.d.ts +0 -10
- package/dist/leader-thread/mutationlog.d.ts.map +0 -1
- package/dist/leader-thread/mutationlog.js +0 -28
- package/dist/leader-thread/mutationlog.js.map +0 -1
- package/dist/leader-thread/pull-queue-set.d.ts +0 -7
- package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
- package/dist/leader-thread/pull-queue-set.js +0 -39
- package/dist/leader-thread/pull-queue-set.js.map +0 -1
- package/dist/mutation.d.ts +0 -13
- package/dist/mutation.d.ts.map +0 -1
- package/dist/mutation.js +0 -57
- package/dist/mutation.js.map +0 -1
- package/dist/query-builder/api.d.ts +0 -190
- package/dist/query-builder/api.d.ts.map +0 -1
- package/dist/query-builder/api.js +0 -8
- package/dist/query-builder/api.js.map +0 -1
- package/dist/query-builder/impl.d.ts +0 -12
- package/dist/query-builder/impl.d.ts.map +0 -1
- package/dist/query-builder/impl.js +0 -244
- package/dist/query-builder/impl.js.map +0 -1
- package/dist/query-builder/impl.test.d.ts +0 -2
- package/dist/query-builder/impl.test.d.ts.map +0 -1
- package/dist/query-builder/impl.test.js +0 -212
- package/dist/query-builder/impl.test.js.map +0 -1
- package/dist/query-builder/mod.d.ts.map +0 -1
- package/dist/query-builder/mod.js.map +0 -1
- package/dist/query-info.d.ts +0 -38
- package/dist/query-info.d.ts.map +0 -1
- package/dist/query-info.js +0 -7
- package/dist/query-info.js.map +0 -1
- package/dist/rehydrate-from-mutationlog.d.ts +0 -14
- package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
- package/dist/rehydrate-from-mutationlog.js +0 -72
- package/dist/rehydrate-from-mutationlog.js.map +0 -1
- package/dist/schema/MutationEvent.d.ts +0 -166
- package/dist/schema/MutationEvent.d.ts.map +0 -1
- package/dist/schema/MutationEvent.js +0 -72
- package/dist/schema/MutationEvent.js.map +0 -1
- package/dist/schema/mutations.d.ts +0 -107
- package/dist/schema/mutations.d.ts.map +0 -1
- package/dist/schema/mutations.js +0 -42
- package/dist/schema/mutations.js.map +0 -1
- package/dist/schema/schema-helpers.d.ts.map +0 -1
- package/dist/schema/schema-helpers.js.map +0 -1
- package/dist/schema/system-tables.d.ts +0 -399
- package/dist/schema/system-tables.d.ts.map +0 -1
- package/dist/schema/system-tables.js +0 -58
- package/dist/schema/system-tables.js.map +0 -1
- package/dist/schema/table-def.d.ts +0 -156
- package/dist/schema/table-def.d.ts.map +0 -1
- package/dist/schema/table-def.js +0 -79
- package/dist/schema/table-def.js.map +0 -1
- package/dist/schema-management/validate-mutation-defs.d.ts +0 -8
- package/dist/schema-management/validate-mutation-defs.d.ts.map +0 -1
- package/dist/schema-management/validate-mutation-defs.js +0 -39
- package/dist/schema-management/validate-mutation-defs.js.map +0 -1
- package/dist/sync/client-session-sync-processor.d.ts +0 -45
- package/dist/sync/client-session-sync-processor.d.ts.map +0 -1
- package/dist/sync/client-session-sync-processor.js +0 -131
- package/dist/sync/client-session-sync-processor.js.map +0 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +0 -1
- package/dist/sync/next/test/mutation-fixtures.js.map +0 -1
- package/src/derived-mutations.test.ts +0 -101
- package/src/derived-mutations.ts +0 -166
- package/src/devtools/devtools-bridge.ts +0 -14
- package/src/devtools/devtools-window-message.ts +0 -27
- package/src/devtools/index.ts +0 -48
- package/src/init-singleton-tables.ts +0 -24
- package/src/leader-thread/apply-mutation.ts +0 -143
- package/src/leader-thread/leader-sync-processor.ts +0 -670
- package/src/leader-thread/mutationlog.ts +0 -46
- package/src/leader-thread/pull-queue-set.ts +0 -58
- package/src/mutation.ts +0 -81
- package/src/query-builder/api.ts +0 -289
- package/src/query-builder/impl.test.ts +0 -239
- package/src/query-builder/impl.ts +0 -285
- package/src/query-info.ts +0 -78
- package/src/rehydrate-from-mutationlog.ts +0 -127
- package/src/schema/EventId.ts +0 -60
- package/src/schema/MutationEvent.ts +0 -180
- package/src/schema/mutations.ts +0 -192
- package/src/schema/system-tables.ts +0 -104
- package/src/schema/table-def.ts +0 -343
- package/src/schema-management/validate-mutation-defs.ts +0 -63
- package/src/sync/client-session-sync-processor.ts +0 -207
- package/src/sync/next/test/mutation-fixtures.ts +0 -224
- package/tsconfig.json +0 -11
- /package/dist/schema/{schema-helpers.d.ts → state/sqlite/schema-helpers.d.ts} +0 -0
@@ -0,0 +1,940 @@
|
|
1
|
+
import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils'
|
2
|
+
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
3
|
+
import {
|
4
|
+
BucketQueue,
|
5
|
+
Deferred,
|
6
|
+
Effect,
|
7
|
+
Exit,
|
8
|
+
FiberHandle,
|
9
|
+
OtelTracer,
|
10
|
+
Queue,
|
11
|
+
ReadonlyArray,
|
12
|
+
Stream,
|
13
|
+
Subscribable,
|
14
|
+
SubscriptionRef,
|
15
|
+
} from '@livestore/utils/effect'
|
16
|
+
import type * as otel from '@opentelemetry/api'
|
17
|
+
|
18
|
+
import type { SqliteDb } from '../adapter-types.js'
|
19
|
+
import { UnexpectedError } from '../adapter-types.js'
|
20
|
+
import type { LiveStoreSchema } from '../schema/mod.js'
|
21
|
+
import { EventSequenceNumber, getEventDef, LiveStoreEvent, SystemTables } from '../schema/mod.js'
|
22
|
+
import { LeaderAheadError } from '../sync/sync.js'
|
23
|
+
import * as SyncState from '../sync/syncstate.js'
|
24
|
+
import { sql } from '../util.js'
|
25
|
+
import * as Eventlog from './eventlog.js'
|
26
|
+
import { rollback } from './materialize-event.js'
|
27
|
+
import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.js'
|
28
|
+
import { LeaderThreadCtx } from './types.js'
|
29
|
+
|
30
|
+
type LocalPushQueueItem = [
|
31
|
+
event: LiveStoreEvent.EncodedWithMeta,
|
32
|
+
deferred: Deferred.Deferred<void, LeaderAheadError> | undefined,
|
33
|
+
/** Used to determine whether the batch has become invalid due to a rejected local push batch */
|
34
|
+
generation: number,
|
35
|
+
]
|
36
|
+
|
37
|
+
/**
|
38
|
+
* The LeaderSyncProcessor manages synchronization of events between
|
39
|
+
* the local state and the sync backend, ensuring efficient and orderly processing.
|
40
|
+
*
|
41
|
+
* In the LeaderSyncProcessor, pulling always has precedence over pushing.
|
42
|
+
*
|
43
|
+
* Responsibilities:
|
44
|
+
* - Queueing incoming local events in a localPushesQueue.
|
45
|
+
* - Broadcasting events to client sessions via pull queues.
|
46
|
+
* - Pushing events to the sync backend.
|
47
|
+
*
|
48
|
+
* Notes:
|
49
|
+
*
|
50
|
+
* local push processing:
|
51
|
+
* - localPushesQueue:
|
52
|
+
* - Maintains events in ascending order.
|
53
|
+
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
54
|
+
* - Processes events from the queue, applying events in batches.
|
55
|
+
* - Controlled by a `Latch` to manage execution flow.
|
56
|
+
* - The latch closes on pull receipt and re-opens post-pull completion.
|
57
|
+
* - Processes up to `maxBatchSize` events per cycle.
|
58
|
+
*
|
59
|
+
* Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
|
60
|
+
*
|
61
|
+
* Tricky concurrency scenarios:
|
62
|
+
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
63
|
+
* Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
|
64
|
+
*
|
65
|
+
*/
|
66
|
+
export const makeLeaderSyncProcessor = ({
|
67
|
+
schema,
|
68
|
+
dbEventlogMissing,
|
69
|
+
dbEventlog,
|
70
|
+
dbState,
|
71
|
+
dbStateMissing,
|
72
|
+
initialBlockingSyncContext,
|
73
|
+
onError,
|
74
|
+
params,
|
75
|
+
testing,
|
76
|
+
}: {
|
77
|
+
schema: LiveStoreSchema
|
78
|
+
/** Only used to know whether we can safely query dbEventlog during setup execution */
|
79
|
+
dbEventlogMissing: boolean
|
80
|
+
dbEventlog: SqliteDb
|
81
|
+
dbState: SqliteDb
|
82
|
+
/** Only used to know whether we can safely query dbState during setup execution */
|
83
|
+
dbStateMissing: boolean
|
84
|
+
initialBlockingSyncContext: InitialBlockingSyncContext
|
85
|
+
onError: 'shutdown' | 'ignore'
|
86
|
+
params: {
|
87
|
+
/**
|
88
|
+
* @default 10
|
89
|
+
*/
|
90
|
+
localPushBatchSize?: number
|
91
|
+
/**
|
92
|
+
* @default 50
|
93
|
+
*/
|
94
|
+
backendPushBatchSize?: number
|
95
|
+
}
|
96
|
+
testing: {
|
97
|
+
delays?: {
|
98
|
+
localPushProcessing?: Effect.Effect<void>
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}): Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope> =>
|
102
|
+
Effect.gen(function* () {
|
103
|
+
const syncBackendPushQueue = yield* BucketQueue.make<LiveStoreEvent.EncodedWithMeta>()
|
104
|
+
const localPushBatchSize = params.localPushBatchSize ?? 10
|
105
|
+
const backendPushBatchSize = params.backendPushBatchSize ?? 50
|
106
|
+
|
107
|
+
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
108
|
+
|
109
|
+
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) => {
|
110
|
+
const { eventDef } = getEventDef(schema, eventEncoded.name)
|
111
|
+
return eventDef.options.clientOnly
|
112
|
+
}
|
113
|
+
|
114
|
+
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Tracks generations of queued local push events.
|
118
|
+
* If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
119
|
+
* even if they would be valid on their own.
|
120
|
+
*/
|
121
|
+
// TODO get rid of this in favour of the `mergeGeneration` event sequence number field
|
122
|
+
const currentLocalPushGenerationRef = { current: 0 }
|
123
|
+
|
124
|
+
type MergeCounter = number
|
125
|
+
const mergeCounterRef = { current: dbStateMissing ? 0 : yield* getMergeCounterFromDb(dbState) }
|
126
|
+
const mergePayloads = new Map<MergeCounter, typeof SyncState.PayloadUpstream.Type>()
|
127
|
+
|
128
|
+
// This context depends on data from `boot`, we should find a better implementation to avoid this ref indirection.
|
129
|
+
const ctxRef = {
|
130
|
+
current: undefined as
|
131
|
+
| undefined
|
132
|
+
| {
|
133
|
+
otelSpan: otel.Span | undefined
|
134
|
+
span: Tracer.Span
|
135
|
+
devtoolsLatch: Effect.Latch | undefined
|
136
|
+
runtime: Runtime.Runtime<LeaderThreadCtx>
|
137
|
+
},
|
138
|
+
}
|
139
|
+
|
140
|
+
const localPushesQueue = yield* BucketQueue.make<LocalPushQueueItem>()
|
141
|
+
const localPushesLatch = yield* Effect.makeLatch(true)
|
142
|
+
const pullLatch = yield* Effect.makeLatch(true)
|
143
|
+
|
144
|
+
/**
|
145
|
+
* Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
|
146
|
+
* events from being pushed in a scenario like this:
|
147
|
+
* - client session A pushes e1
|
148
|
+
* - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
|
149
|
+
* - client session B also pushes e1 (which should be rejected)
|
150
|
+
*
|
151
|
+
* Thus the purpoe of the pushHeadRef is the guard the integrity of the local push queue
|
152
|
+
*/
|
153
|
+
const pushHeadRef = { current: EventSequenceNumber.ROOT }
|
154
|
+
const advancePushHead = (eventNum: EventSequenceNumber.EventSequenceNumber) => {
|
155
|
+
pushHeadRef.current = EventSequenceNumber.max(pushHeadRef.current, eventNum)
|
156
|
+
}
|
157
|
+
|
158
|
+
// NOTE: New events are only pushed to sync backend after successful local push processing
|
159
|
+
const push: LeaderSyncProcessor['push'] = (newEvents, options) =>
|
160
|
+
Effect.gen(function* () {
|
161
|
+
if (newEvents.length === 0) return
|
162
|
+
|
163
|
+
yield* validatePushBatch(newEvents, pushHeadRef.current)
|
164
|
+
|
165
|
+
advancePushHead(newEvents.at(-1)!.seqNum)
|
166
|
+
|
167
|
+
const waitForProcessing = options?.waitForProcessing ?? false
|
168
|
+
const generation = currentLocalPushGenerationRef.current
|
169
|
+
|
170
|
+
if (waitForProcessing) {
|
171
|
+
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make<void, LeaderAheadError>())
|
172
|
+
|
173
|
+
const items = newEvents.map(
|
174
|
+
(eventEncoded, i) => [eventEncoded, deferreds[i], generation] as LocalPushQueueItem,
|
175
|
+
)
|
176
|
+
|
177
|
+
yield* BucketQueue.offerAll(localPushesQueue, items)
|
178
|
+
|
179
|
+
yield* Effect.all(deferreds)
|
180
|
+
} else {
|
181
|
+
const items = newEvents.map((eventEncoded) => [eventEncoded, undefined, generation] as LocalPushQueueItem)
|
182
|
+
yield* BucketQueue.offerAll(localPushesQueue, items)
|
183
|
+
}
|
184
|
+
}).pipe(
|
185
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
|
186
|
+
attributes: {
|
187
|
+
batchSize: newEvents.length,
|
188
|
+
batch: TRACE_VERBOSE ? newEvents : undefined,
|
189
|
+
},
|
190
|
+
links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
|
191
|
+
}),
|
192
|
+
)
|
193
|
+
|
194
|
+
const pushPartial: LeaderSyncProcessor['pushPartial'] = ({ event: { name, args }, clientId, sessionId }) =>
|
195
|
+
Effect.gen(function* () {
|
196
|
+
const syncState = yield* syncStateSref
|
197
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
198
|
+
|
199
|
+
const { eventDef } = getEventDef(schema, name)
|
200
|
+
|
201
|
+
const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
|
202
|
+
name,
|
203
|
+
args,
|
204
|
+
clientId,
|
205
|
+
sessionId,
|
206
|
+
...EventSequenceNumber.nextPair(syncState.localHead, eventDef.options.clientOnly),
|
207
|
+
})
|
208
|
+
|
209
|
+
yield* push([eventEncoded])
|
210
|
+
}).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie))
|
211
|
+
|
212
|
+
// Starts various background loops
|
213
|
+
const boot: LeaderSyncProcessor['boot'] = Effect.gen(function* () {
|
214
|
+
const span = yield* Effect.currentSpan.pipe(Effect.orDie)
|
215
|
+
const otelSpan = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
216
|
+
const { devtools, shutdownChannel } = yield* LeaderThreadCtx
|
217
|
+
const runtime = yield* Effect.runtime<LeaderThreadCtx>()
|
218
|
+
|
219
|
+
ctxRef.current = {
|
220
|
+
otelSpan,
|
221
|
+
span,
|
222
|
+
devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
|
223
|
+
runtime,
|
224
|
+
}
|
225
|
+
|
226
|
+
const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
|
227
|
+
|
228
|
+
const initialBackendHead = dbEventlogMissing
|
229
|
+
? EventSequenceNumber.ROOT.global
|
230
|
+
: Eventlog.getBackendHeadFromDb(dbEventlog)
|
231
|
+
|
232
|
+
if (initialBackendHead > initialLocalHead.global) {
|
233
|
+
return shouldNeverHappen(
|
234
|
+
`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
|
235
|
+
)
|
236
|
+
}
|
237
|
+
|
238
|
+
const pendingEvents = dbEventlogMissing
|
239
|
+
? []
|
240
|
+
: yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventSequenceNumber.clientDefault })
|
241
|
+
|
242
|
+
const initialSyncState = new SyncState.SyncState({
|
243
|
+
pending: pendingEvents,
|
244
|
+
upstreamHead: { global: initialBackendHead, client: EventSequenceNumber.clientDefault },
|
245
|
+
localHead: initialLocalHead,
|
246
|
+
})
|
247
|
+
|
248
|
+
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
249
|
+
yield* SubscriptionRef.set(syncStateSref, initialSyncState)
|
250
|
+
|
251
|
+
// Rehydrate sync queue
|
252
|
+
if (pendingEvents.length > 0) {
|
253
|
+
const globalPendingEvents = pendingEvents
|
254
|
+
// Don't sync clientOnly events
|
255
|
+
.filter((eventEncoded) => {
|
256
|
+
const { eventDef } = getEventDef(schema, eventEncoded.name)
|
257
|
+
return eventDef.options.clientOnly === false
|
258
|
+
})
|
259
|
+
|
260
|
+
if (globalPendingEvents.length > 0) {
|
261
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingEvents)
|
262
|
+
}
|
263
|
+
}
|
264
|
+
|
265
|
+
const shutdownOnError = (cause: unknown) =>
|
266
|
+
Effect.gen(function* () {
|
267
|
+
if (onError === 'shutdown') {
|
268
|
+
yield* shutdownChannel.send(UnexpectedError.make({ cause }))
|
269
|
+
yield* Effect.die(cause)
|
270
|
+
}
|
271
|
+
})
|
272
|
+
|
273
|
+
yield* backgroundApplyLocalPushes({
|
274
|
+
localPushesLatch,
|
275
|
+
localPushesQueue,
|
276
|
+
pullLatch,
|
277
|
+
syncStateSref,
|
278
|
+
syncBackendPushQueue,
|
279
|
+
schema,
|
280
|
+
isClientEvent,
|
281
|
+
otelSpan,
|
282
|
+
currentLocalPushGenerationRef,
|
283
|
+
connectedClientSessionPullQueues,
|
284
|
+
mergeCounterRef,
|
285
|
+
mergePayloads,
|
286
|
+
localPushBatchSize,
|
287
|
+
testing: {
|
288
|
+
delay: testing?.delays?.localPushProcessing,
|
289
|
+
},
|
290
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
291
|
+
|
292
|
+
const backendPushingFiberHandle = yield* FiberHandle.make()
|
293
|
+
const backendPushingEffect = backgroundBackendPushing({
|
294
|
+
syncBackendPushQueue,
|
295
|
+
otelSpan,
|
296
|
+
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
297
|
+
backendPushBatchSize,
|
298
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError))
|
299
|
+
|
300
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
301
|
+
|
302
|
+
yield* backgroundBackendPulling({
|
303
|
+
initialBackendHead,
|
304
|
+
isClientEvent,
|
305
|
+
restartBackendPushing: (filteredRebasedPending) =>
|
306
|
+
Effect.gen(function* () {
|
307
|
+
// Stop current pushing fiber
|
308
|
+
yield* FiberHandle.clear(backendPushingFiberHandle)
|
309
|
+
|
310
|
+
// Reset the sync backend push queue
|
311
|
+
yield* BucketQueue.clear(syncBackendPushQueue)
|
312
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending)
|
313
|
+
|
314
|
+
// Restart pushing fiber
|
315
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
316
|
+
}),
|
317
|
+
syncStateSref,
|
318
|
+
localPushesLatch,
|
319
|
+
pullLatch,
|
320
|
+
otelSpan,
|
321
|
+
initialBlockingSyncContext,
|
322
|
+
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
323
|
+
connectedClientSessionPullQueues,
|
324
|
+
mergeCounterRef,
|
325
|
+
mergePayloads,
|
326
|
+
advancePushHead,
|
327
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped)
|
328
|
+
|
329
|
+
return { initialLeaderHead: initialLocalHead }
|
330
|
+
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
331
|
+
|
332
|
+
const pull: LeaderSyncProcessor['pull'] = ({ cursor }) =>
|
333
|
+
Effect.gen(function* () {
|
334
|
+
const queue = yield* pullQueue({ cursor })
|
335
|
+
return Stream.fromQueue(queue)
|
336
|
+
}).pipe(Stream.unwrapScoped)
|
337
|
+
|
338
|
+
const pullQueue: LeaderSyncProcessor['pullQueue'] = ({ cursor }) => {
|
339
|
+
const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized')
|
340
|
+
return Effect.gen(function* () {
|
341
|
+
const queue = yield* connectedClientSessionPullQueues.makeQueue
|
342
|
+
const payloadsSinceCursor = Array.from(mergePayloads.entries())
|
343
|
+
.map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
|
344
|
+
.filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
|
345
|
+
.toSorted((a, b) => a.mergeCounter - b.mergeCounter)
|
346
|
+
.map(({ payload, mergeCounter }) => {
|
347
|
+
if (payload._tag === 'upstream-advance') {
|
348
|
+
return {
|
349
|
+
payload: {
|
350
|
+
_tag: 'upstream-advance' as const,
|
351
|
+
newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) =>
|
352
|
+
EventSequenceNumber.isGreaterThanOrEqual(cursor.eventNum, eventEncoded.seqNum),
|
353
|
+
),
|
354
|
+
},
|
355
|
+
mergeCounter,
|
356
|
+
}
|
357
|
+
} else {
|
358
|
+
return { payload, mergeCounter }
|
359
|
+
}
|
360
|
+
})
|
361
|
+
|
362
|
+
yield* queue.offerAll(payloadsSinceCursor)
|
363
|
+
|
364
|
+
return queue
|
365
|
+
}).pipe(Effect.provide(runtime))
|
366
|
+
}
|
367
|
+
|
368
|
+
const syncState = Subscribable.make({
|
369
|
+
get: Effect.gen(function* () {
|
370
|
+
const syncState = yield* syncStateSref
|
371
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
372
|
+
return syncState
|
373
|
+
}),
|
374
|
+
changes: syncStateSref.changes.pipe(Stream.filter(isNotUndefined)),
|
375
|
+
})
|
376
|
+
|
377
|
+
return {
|
378
|
+
pull,
|
379
|
+
pullQueue,
|
380
|
+
push,
|
381
|
+
pushPartial,
|
382
|
+
boot,
|
383
|
+
syncState,
|
384
|
+
getMergeCounter: () => mergeCounterRef.current,
|
385
|
+
} satisfies LeaderSyncProcessor
|
386
|
+
})
|
387
|
+
|
388
|
+
const backgroundApplyLocalPushes = ({
|
389
|
+
localPushesLatch,
|
390
|
+
localPushesQueue,
|
391
|
+
pullLatch,
|
392
|
+
syncStateSref,
|
393
|
+
syncBackendPushQueue,
|
394
|
+
schema,
|
395
|
+
isClientEvent,
|
396
|
+
otelSpan,
|
397
|
+
currentLocalPushGenerationRef,
|
398
|
+
connectedClientSessionPullQueues,
|
399
|
+
mergeCounterRef,
|
400
|
+
mergePayloads,
|
401
|
+
localPushBatchSize,
|
402
|
+
testing,
|
403
|
+
}: {
|
404
|
+
pullLatch: Effect.Latch
|
405
|
+
localPushesLatch: Effect.Latch
|
406
|
+
localPushesQueue: BucketQueue.BucketQueue<LocalPushQueueItem>
|
407
|
+
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
408
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
|
409
|
+
schema: LiveStoreSchema
|
410
|
+
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
411
|
+
otelSpan: otel.Span | undefined
|
412
|
+
currentLocalPushGenerationRef: { current: number }
|
413
|
+
connectedClientSessionPullQueues: PullQueueSet
|
414
|
+
mergeCounterRef: { current: number }
|
415
|
+
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
416
|
+
localPushBatchSize: number
|
417
|
+
testing: {
|
418
|
+
delay: Effect.Effect<void> | undefined
|
419
|
+
}
|
420
|
+
}) =>
|
421
|
+
Effect.gen(function* () {
|
422
|
+
while (true) {
|
423
|
+
if (testing.delay !== undefined) {
|
424
|
+
yield* testing.delay.pipe(Effect.withSpan('localPushProcessingDelay'))
|
425
|
+
}
|
426
|
+
|
427
|
+
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize)
|
428
|
+
|
429
|
+
// Wait for the backend pulling to finish
|
430
|
+
yield* localPushesLatch.await
|
431
|
+
|
432
|
+
// Prevent backend pull processing until this local push is finished
|
433
|
+
yield* pullLatch.close
|
434
|
+
|
435
|
+
// Since the generation might have changed since enqueuing, we need to filter out items with older generation
|
436
|
+
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
437
|
+
const filteredBatchItems = batchItems
|
438
|
+
.filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
|
439
|
+
.map(([eventEncoded, deferred]) => [eventEncoded, deferred] as const)
|
440
|
+
|
441
|
+
if (filteredBatchItems.length === 0) {
|
442
|
+
// console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
|
443
|
+
// Allow the backend pulling to start
|
444
|
+
yield* pullLatch.open
|
445
|
+
continue
|
446
|
+
}
|
447
|
+
|
448
|
+
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredBatchItems)
|
449
|
+
|
450
|
+
const syncState = yield* syncStateSref
|
451
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
452
|
+
|
453
|
+
const mergeResult = SyncState.merge({
|
454
|
+
syncState,
|
455
|
+
payload: { _tag: 'local-push', newEvents },
|
456
|
+
isClientEvent,
|
457
|
+
isEqualEvent: LiveStoreEvent.isEqualEncoded,
|
458
|
+
})
|
459
|
+
|
460
|
+
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
461
|
+
|
462
|
+
switch (mergeResult._tag) {
|
463
|
+
case 'unexpected-error': {
|
464
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:unexpected-error`, {
|
465
|
+
batchSize: newEvents.length,
|
466
|
+
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
467
|
+
})
|
468
|
+
return yield* Effect.fail(mergeResult.cause)
|
469
|
+
}
|
470
|
+
case 'rebase': {
|
471
|
+
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
472
|
+
}
|
473
|
+
case 'reject': {
|
474
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:reject`, {
|
475
|
+
batchSize: newEvents.length,
|
476
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
477
|
+
})
|
478
|
+
|
479
|
+
// TODO: how to test this?
|
480
|
+
currentLocalPushGenerationRef.current++
|
481
|
+
|
482
|
+
const nextGeneration = currentLocalPushGenerationRef.current
|
483
|
+
|
484
|
+
const providedNum = newEvents.at(0)!.seqNum
|
485
|
+
// All subsequent pushes with same generation should be rejected as well
|
486
|
+
// We're also handling the case where the localPushQueue already contains events
|
487
|
+
// from the next generation which we preserve in the queue
|
488
|
+
const remainingEventsMatchingGeneration = yield* BucketQueue.takeSplitWhere(
|
489
|
+
localPushesQueue,
|
490
|
+
(item) => item[2] >= nextGeneration,
|
491
|
+
)
|
492
|
+
|
493
|
+
// TODO we still need to better understand and handle this scenario
|
494
|
+
if (LS_DEV && (yield* BucketQueue.size(localPushesQueue)) > 0) {
|
495
|
+
console.log('localPushesQueue is not empty', yield* BucketQueue.size(localPushesQueue))
|
496
|
+
debugger
|
497
|
+
}
|
498
|
+
|
499
|
+
const allDeferredsToReject = [
|
500
|
+
...deferreds,
|
501
|
+
...remainingEventsMatchingGeneration.map(([_, deferred]) => deferred),
|
502
|
+
].filter(isNotUndefined)
|
503
|
+
|
504
|
+
yield* Effect.forEach(allDeferredsToReject, (deferred) =>
|
505
|
+
Deferred.fail(
|
506
|
+
deferred,
|
507
|
+
LeaderAheadError.make({
|
508
|
+
minimumExpectedNum: mergeResult.expectedMinimumId,
|
509
|
+
providedNum,
|
510
|
+
// nextGeneration,
|
511
|
+
}),
|
512
|
+
),
|
513
|
+
)
|
514
|
+
|
515
|
+
// Allow the backend pulling to start
|
516
|
+
yield* pullLatch.open
|
517
|
+
|
518
|
+
// In this case we're skipping state update and down/upstream processing
|
519
|
+
// We've cleared the local push queue and are now waiting for new local pushes / backend pulls
|
520
|
+
continue
|
521
|
+
}
|
522
|
+
case 'advance': {
|
523
|
+
break
|
524
|
+
}
|
525
|
+
default: {
|
526
|
+
casesHandled(mergeResult)
|
527
|
+
}
|
528
|
+
}
|
529
|
+
|
530
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
531
|
+
|
532
|
+
yield* connectedClientSessionPullQueues.offer({
|
533
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
534
|
+
mergeCounter,
|
535
|
+
})
|
536
|
+
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
537
|
+
|
538
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:advance`, {
|
539
|
+
batchSize: newEvents.length,
|
540
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
541
|
+
})
|
542
|
+
|
543
|
+
// Don't sync clientOnly events
|
544
|
+
const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
|
545
|
+
const { eventDef } = getEventDef(schema, eventEncoded.name)
|
546
|
+
return eventDef.options.clientOnly === false
|
547
|
+
})
|
548
|
+
|
549
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
|
550
|
+
|
551
|
+
yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds })
|
552
|
+
|
553
|
+
// Allow the backend pulling to start
|
554
|
+
yield* pullLatch.open
|
555
|
+
}
|
556
|
+
})
|
557
|
+
|
558
|
+
type MaterializeEventsBatch = (_: {
|
559
|
+
batchItems: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>
|
560
|
+
/**
|
561
|
+
* The deferreds are used by the caller to know when the mutation has been processed.
|
562
|
+
* Indexes are aligned with `batchItems`
|
563
|
+
*/
|
564
|
+
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
565
|
+
}) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
|
566
|
+
|
567
|
+
// TODO how to handle errors gracefully
|
568
|
+
const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
|
569
|
+
Effect.gen(function* () {
|
570
|
+
const { dbState: db, dbEventlog, materializeEvent } = yield* LeaderThreadCtx
|
571
|
+
|
572
|
+
// NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
|
573
|
+
db.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
574
|
+
dbEventlog.execute('BEGIN TRANSACTION', undefined) // Start the transaction
|
575
|
+
|
576
|
+
yield* Effect.addFinalizer((exit) =>
|
577
|
+
Effect.gen(function* () {
|
578
|
+
if (Exit.isSuccess(exit)) return
|
579
|
+
|
580
|
+
// Rollback in case of an error
|
581
|
+
db.execute('ROLLBACK', undefined)
|
582
|
+
dbEventlog.execute('ROLLBACK', undefined)
|
583
|
+
}),
|
584
|
+
)
|
585
|
+
|
586
|
+
for (let i = 0; i < batchItems.length; i++) {
|
587
|
+
const { sessionChangeset } = yield* materializeEvent(batchItems[i]!)
|
588
|
+
batchItems[i]!.meta.sessionChangeset = sessionChangeset
|
589
|
+
|
590
|
+
if (deferreds?.[i] !== undefined) {
|
591
|
+
yield* Deferred.succeed(deferreds[i]!, void 0)
|
592
|
+
}
|
593
|
+
}
|
594
|
+
|
595
|
+
db.execute('COMMIT', undefined) // Commit the transaction
|
596
|
+
dbEventlog.execute('COMMIT', undefined) // Commit the transaction
|
597
|
+
}).pipe(
|
598
|
+
Effect.uninterruptible,
|
599
|
+
Effect.scoped,
|
600
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:materializeEventItems', {
|
601
|
+
attributes: { batchSize: batchItems.length },
|
602
|
+
}),
|
603
|
+
Effect.tapCauseLogPretty,
|
604
|
+
UnexpectedError.mapToUnexpectedError,
|
605
|
+
)
|
606
|
+
|
607
|
+
const backgroundBackendPulling = ({
|
608
|
+
initialBackendHead,
|
609
|
+
isClientEvent,
|
610
|
+
restartBackendPushing,
|
611
|
+
otelSpan,
|
612
|
+
syncStateSref,
|
613
|
+
localPushesLatch,
|
614
|
+
pullLatch,
|
615
|
+
devtoolsLatch,
|
616
|
+
initialBlockingSyncContext,
|
617
|
+
connectedClientSessionPullQueues,
|
618
|
+
mergeCounterRef,
|
619
|
+
mergePayloads,
|
620
|
+
advancePushHead,
|
621
|
+
}: {
|
622
|
+
initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
|
623
|
+
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
624
|
+
restartBackendPushing: (
|
625
|
+
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
626
|
+
) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | HttpClient.HttpClient>
|
627
|
+
otelSpan: otel.Span | undefined
|
628
|
+
syncStateSref: SubscriptionRef.SubscriptionRef<SyncState.SyncState | undefined>
|
629
|
+
localPushesLatch: Effect.Latch
|
630
|
+
pullLatch: Effect.Latch
|
631
|
+
devtoolsLatch: Effect.Latch | undefined
|
632
|
+
initialBlockingSyncContext: InitialBlockingSyncContext
|
633
|
+
connectedClientSessionPullQueues: PullQueueSet
|
634
|
+
mergeCounterRef: { current: number }
|
635
|
+
mergePayloads: Map<number, typeof SyncState.PayloadUpstream.Type>
|
636
|
+
advancePushHead: (eventNum: EventSequenceNumber.EventSequenceNumber) => void
|
637
|
+
}) =>
|
638
|
+
Effect.gen(function* () {
|
639
|
+
const { syncBackend, dbState: db, dbEventlog, schema } = yield* LeaderThreadCtx
|
640
|
+
|
641
|
+
if (syncBackend === undefined) return
|
642
|
+
|
643
|
+
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], remaining: number) =>
|
644
|
+
Effect.gen(function* () {
|
645
|
+
if (newEvents.length === 0) return
|
646
|
+
|
647
|
+
if (devtoolsLatch !== undefined) {
|
648
|
+
yield* devtoolsLatch.await
|
649
|
+
}
|
650
|
+
|
651
|
+
// Prevent more local pushes from being processed until this pull is finished
|
652
|
+
yield* localPushesLatch.close
|
653
|
+
|
654
|
+
// Wait for pending local pushes to finish
|
655
|
+
yield* pullLatch.await
|
656
|
+
|
657
|
+
const syncState = yield* syncStateSref
|
658
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
659
|
+
|
660
|
+
const mergeResult = SyncState.merge({
|
661
|
+
syncState,
|
662
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
|
663
|
+
isClientEvent,
|
664
|
+
isEqualEvent: LiveStoreEvent.isEqualEncoded,
|
665
|
+
ignoreClientEvents: true,
|
666
|
+
})
|
667
|
+
|
668
|
+
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef)
|
669
|
+
|
670
|
+
if (mergeResult._tag === 'reject') {
|
671
|
+
return shouldNeverHappen('The leader thread should never reject upstream advances')
|
672
|
+
} else if (mergeResult._tag === 'unexpected-error') {
|
673
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:unexpected-error`, {
|
674
|
+
newEventsCount: newEvents.length,
|
675
|
+
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
676
|
+
})
|
677
|
+
return yield* Effect.fail(mergeResult.cause)
|
678
|
+
}
|
679
|
+
|
680
|
+
const newBackendHead = newEvents.at(-1)!.seqNum
|
681
|
+
|
682
|
+
Eventlog.updateBackendHead(dbEventlog, newBackendHead)
|
683
|
+
|
684
|
+
if (mergeResult._tag === 'rebase') {
|
685
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:rebase`, {
|
686
|
+
newEventsCount: newEvents.length,
|
687
|
+
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
688
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
689
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
690
|
+
})
|
691
|
+
|
692
|
+
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
693
|
+
const { eventDef } = getEventDef(schema, event.name)
|
694
|
+
return eventDef.options.clientOnly === false
|
695
|
+
})
|
696
|
+
yield* restartBackendPushing(globalRebasedPendingEvents)
|
697
|
+
|
698
|
+
if (mergeResult.rollbackEvents.length > 0) {
|
699
|
+
yield* rollback({
|
700
|
+
dbState: db,
|
701
|
+
dbEventlog,
|
702
|
+
eventNumsToRollback: mergeResult.rollbackEvents.map((_) => _.seqNum),
|
703
|
+
})
|
704
|
+
}
|
705
|
+
|
706
|
+
yield* connectedClientSessionPullQueues.offer({
|
707
|
+
payload: SyncState.PayloadUpstreamRebase.make({
|
708
|
+
newEvents: mergeResult.newEvents,
|
709
|
+
rollbackEvents: mergeResult.rollbackEvents,
|
710
|
+
}),
|
711
|
+
mergeCounter,
|
712
|
+
})
|
713
|
+
mergePayloads.set(
|
714
|
+
mergeCounter,
|
715
|
+
SyncState.PayloadUpstreamRebase.make({
|
716
|
+
newEvents: mergeResult.newEvents,
|
717
|
+
rollbackEvents: mergeResult.rollbackEvents,
|
718
|
+
}),
|
719
|
+
)
|
720
|
+
} else {
|
721
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:advance`, {
|
722
|
+
newEventsCount: newEvents.length,
|
723
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
724
|
+
})
|
725
|
+
|
726
|
+
yield* connectedClientSessionPullQueues.offer({
|
727
|
+
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }),
|
728
|
+
mergeCounter,
|
729
|
+
})
|
730
|
+
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }))
|
731
|
+
|
732
|
+
if (mergeResult.confirmedEvents.length > 0) {
|
733
|
+
// `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
|
734
|
+
// `newEvents` instead which we filter via `mergeResult.confirmedEvents`
|
735
|
+
const confirmedNewEvents = newEvents.filter((event) =>
|
736
|
+
mergeResult.confirmedEvents.some((confirmedEvent) =>
|
737
|
+
EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
|
738
|
+
),
|
739
|
+
)
|
740
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
|
741
|
+
}
|
742
|
+
}
|
743
|
+
|
744
|
+
// Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
|
745
|
+
trimChangesetRows(db, newBackendHead)
|
746
|
+
|
747
|
+
advancePushHead(mergeResult.newSyncState.localHead)
|
748
|
+
|
749
|
+
yield* materializeEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined })
|
750
|
+
|
751
|
+
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
752
|
+
|
753
|
+
// Allow local pushes to be processed again
|
754
|
+
if (remaining === 0) {
|
755
|
+
yield* localPushesLatch.open
|
756
|
+
}
|
757
|
+
})
|
758
|
+
|
759
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead)
|
760
|
+
|
761
|
+
yield* syncBackend.pull(cursorInfo).pipe(
|
762
|
+
// TODO only take from queue while connected
|
763
|
+
Stream.tap(({ batch, remaining }) =>
|
764
|
+
Effect.gen(function* () {
|
765
|
+
// yield* Effect.spanEvent('batch', {
|
766
|
+
// attributes: {
|
767
|
+
// batchSize: batch.length,
|
768
|
+
// batch: TRACE_VERBOSE ? batch : undefined,
|
769
|
+
// },
|
770
|
+
// })
|
771
|
+
|
772
|
+
// NOTE we only want to take process events when the sync backend is connected
|
773
|
+
// (e.g. needed for simulating being offline)
|
774
|
+
// TODO remove when there's a better way to handle this in stream above
|
775
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
776
|
+
|
777
|
+
yield* onNewPullChunk(
|
778
|
+
batch.map((_) => LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, _.metadata)),
|
779
|
+
remaining,
|
780
|
+
)
|
781
|
+
|
782
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
|
783
|
+
}),
|
784
|
+
),
|
785
|
+
Stream.runDrain,
|
786
|
+
Effect.interruptible,
|
787
|
+
)
|
788
|
+
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
|
789
|
+
|
790
|
+
const backgroundBackendPushing = ({
|
791
|
+
syncBackendPushQueue,
|
792
|
+
otelSpan,
|
793
|
+
devtoolsLatch,
|
794
|
+
backendPushBatchSize,
|
795
|
+
}: {
|
796
|
+
syncBackendPushQueue: BucketQueue.BucketQueue<LiveStoreEvent.EncodedWithMeta>
|
797
|
+
otelSpan: otel.Span | undefined
|
798
|
+
devtoolsLatch: Effect.Latch | undefined
|
799
|
+
backendPushBatchSize: number
|
800
|
+
}) =>
|
801
|
+
Effect.gen(function* () {
|
802
|
+
const { syncBackend } = yield* LeaderThreadCtx
|
803
|
+
if (syncBackend === undefined) return
|
804
|
+
|
805
|
+
while (true) {
|
806
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
807
|
+
|
808
|
+
const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize)
|
809
|
+
|
810
|
+
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
811
|
+
|
812
|
+
if (devtoolsLatch !== undefined) {
|
813
|
+
yield* devtoolsLatch.await
|
814
|
+
}
|
815
|
+
|
816
|
+
otelSpan?.addEvent('backend-push', {
|
817
|
+
batchSize: queueItems.length,
|
818
|
+
batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
|
819
|
+
})
|
820
|
+
|
821
|
+
// TODO handle push errors (should only happen during concurrent pull+push)
|
822
|
+
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
|
823
|
+
|
824
|
+
if (pushResult._tag === 'Left') {
|
825
|
+
if (LS_DEV) {
|
826
|
+
yield* Effect.logDebug('handled backend-push-error', { error: pushResult.left.toString() })
|
827
|
+
}
|
828
|
+
otelSpan?.addEvent('backend-push-error', { error: pushResult.left.toString() })
|
829
|
+
// wait for interrupt caused by background pulling which will then restart pushing
|
830
|
+
return yield* Effect.never
|
831
|
+
}
|
832
|
+
}
|
833
|
+
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
|
834
|
+
|
835
|
+
const trimChangesetRows = (db: SqliteDb, newHead: EventSequenceNumber.EventSequenceNumber) => {
|
836
|
+
// Since we're using the session changeset rows to query for the current head,
|
837
|
+
// we're keeping at least one row for the current head, and thus are using `<` instead of `<=`
|
838
|
+
db.execute(sql`DELETE FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE seqNumGlobal < ${newHead.global}`)
|
839
|
+
}
|
840
|
+
|
841
|
+
interface PullQueueSet {
|
842
|
+
makeQueue: Effect.Effect<
|
843
|
+
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
844
|
+
UnexpectedError,
|
845
|
+
Scope.Scope | LeaderThreadCtx
|
846
|
+
>
|
847
|
+
offer: (item: {
|
848
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
849
|
+
mergeCounter: number
|
850
|
+
}) => Effect.Effect<void, UnexpectedError>
|
851
|
+
}
|
852
|
+
|
853
|
+
const makePullQueueSet = Effect.gen(function* () {
|
854
|
+
const set = new Set<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>>()
|
855
|
+
|
856
|
+
yield* Effect.addFinalizer(() =>
|
857
|
+
Effect.gen(function* () {
|
858
|
+
for (const queue of set) {
|
859
|
+
yield* Queue.shutdown(queue)
|
860
|
+
}
|
861
|
+
|
862
|
+
set.clear()
|
863
|
+
}),
|
864
|
+
)
|
865
|
+
|
866
|
+
const makeQueue: PullQueueSet['makeQueue'] = Effect.gen(function* () {
|
867
|
+
const queue = yield* Queue.unbounded<{
|
868
|
+
payload: typeof SyncState.PayloadUpstream.Type
|
869
|
+
mergeCounter: number
|
870
|
+
}>().pipe(Effect.acquireRelease(Queue.shutdown))
|
871
|
+
|
872
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)))
|
873
|
+
|
874
|
+
set.add(queue)
|
875
|
+
|
876
|
+
return queue
|
877
|
+
})
|
878
|
+
|
879
|
+
const offer: PullQueueSet['offer'] = (item) =>
|
880
|
+
Effect.gen(function* () {
|
881
|
+
// Short-circuit if the payload is an empty upstream advance
|
882
|
+
if (item.payload._tag === 'upstream-advance' && item.payload.newEvents.length === 0) {
|
883
|
+
return
|
884
|
+
}
|
885
|
+
|
886
|
+
for (const queue of set) {
|
887
|
+
yield* Queue.offer(queue, item)
|
888
|
+
}
|
889
|
+
})
|
890
|
+
|
891
|
+
return {
|
892
|
+
makeQueue,
|
893
|
+
offer,
|
894
|
+
}
|
895
|
+
})
|
896
|
+
|
897
|
+
const incrementMergeCounter = (mergeCounterRef: { current: number }) =>
|
898
|
+
Effect.gen(function* () {
|
899
|
+
const { dbState } = yield* LeaderThreadCtx
|
900
|
+
mergeCounterRef.current++
|
901
|
+
dbState.execute(
|
902
|
+
sql`INSERT OR REPLACE INTO ${SystemTables.LEADER_MERGE_COUNTER_TABLE} (id, mergeCounter) VALUES (0, ${mergeCounterRef.current})`,
|
903
|
+
)
|
904
|
+
return mergeCounterRef.current
|
905
|
+
})
|
906
|
+
|
907
|
+
const getMergeCounterFromDb = (dbState: SqliteDb) =>
|
908
|
+
Effect.gen(function* () {
|
909
|
+
const result = dbState.select<{ mergeCounter: number }>(
|
910
|
+
sql`SELECT mergeCounter FROM ${SystemTables.LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
|
911
|
+
)
|
912
|
+
return result[0]?.mergeCounter ?? 0
|
913
|
+
})
|
914
|
+
|
915
|
+
const validatePushBatch = (
|
916
|
+
batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
917
|
+
pushHead: EventSequenceNumber.EventSequenceNumber,
|
918
|
+
) =>
|
919
|
+
Effect.gen(function* () {
|
920
|
+
if (batch.length === 0) {
|
921
|
+
return
|
922
|
+
}
|
923
|
+
|
924
|
+
// Make sure batch is monotonically increasing
|
925
|
+
for (let i = 1; i < batch.length; i++) {
|
926
|
+
if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
|
927
|
+
shouldNeverHappen(
|
928
|
+
`Events must be ordered in monotonically ascending order by eventNum. Received: [${batch.map((e) => EventSequenceNumber.toString(e.seqNum)).join(', ')}]`,
|
929
|
+
)
|
930
|
+
}
|
931
|
+
}
|
932
|
+
|
933
|
+
// Make sure smallest sequence number is > pushHead
|
934
|
+
if (EventSequenceNumber.isGreaterThanOrEqual(pushHead, batch[0]!.seqNum)) {
|
935
|
+
return yield* LeaderAheadError.make({
|
936
|
+
minimumExpectedNum: pushHead,
|
937
|
+
providedNum: batch[0]!.seqNum,
|
938
|
+
})
|
939
|
+
}
|
940
|
+
})
|