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