@livestore/common 0.0.0-snapshot-2ef046b02334f52613d31dbe06af53487685edc0 → 0.0.0-snapshot-8115ad48d5a57244358c943ecc92bb0a30274b87
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 +34 -13
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +20 -2
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts +1 -1
- package/dist/bounded-collections.d.ts.map +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 +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +45 -45
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +11 -11
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +25 -12
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +146 -98
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/{apply-mutation.d.ts → apply-event.d.ts} +7 -7
- package/dist/leader-thread/apply-event.d.ts.map +1 -0
- package/dist/leader-thread/{apply-mutation.js → apply-event.js} +45 -45
- package/dist/leader-thread/apply-event.js.map +1 -0
- 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 +123 -0
- package/dist/leader-thread/eventlog.js.map +1 -0
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +21 -19
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +16 -4
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +23 -16
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- 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.map +1 -1
- package/dist/leader-thread/recreate-db.js +6 -7
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +14 -15
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/materializer-helper.d.ts +23 -0
- package/dist/materializer-helper.d.ts.map +1 -0
- package/dist/materializer-helper.js +70 -0
- package/dist/materializer-helper.js.map +1 -0
- package/dist/query-builder/api.d.ts +56 -51
- package/dist/query-builder/api.d.ts.map +1 -1
- package/dist/query-builder/api.js +3 -5
- package/dist/query-builder/api.js.map +1 -1
- package/dist/query-builder/astToSql.d.ts.map +1 -1
- package/dist/query-builder/astToSql.js +59 -37
- package/dist/query-builder/astToSql.js.map +1 -1
- package/dist/query-builder/impl.d.ts +2 -3
- package/dist/query-builder/impl.d.ts.map +1 -1
- package/dist/query-builder/impl.js +48 -46
- package/dist/query-builder/impl.js.map +1 -1
- package/dist/query-builder/impl.test.d.ts +86 -1
- package/dist/query-builder/impl.test.d.ts.map +1 -1
- package/dist/query-builder/impl.test.js +223 -36
- package/dist/query-builder/impl.test.js.map +1 -1
- package/dist/rehydrate-from-eventlog.d.ts +15 -0
- package/dist/rehydrate-from-eventlog.d.ts.map +1 -0
- package/dist/{rehydrate-from-mutationlog.js → rehydrate-from-eventlog.js} +26 -25
- package/dist/rehydrate-from-eventlog.js.map +1 -0
- package/dist/schema/EventDef.d.ts +136 -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 +7 -2
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +18 -3
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/{MutationEvent.d.ts → LiveStoreEvent.d.ts} +56 -56
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
- package/dist/schema/{MutationEvent.js → LiveStoreEvent.js} +25 -25
- package/dist/schema/LiveStoreEvent.js.map +1 -0
- package/dist/schema/client-document-def.d.ts +223 -0
- package/dist/schema/client-document-def.d.ts.map +1 -0
- package/dist/schema/client-document-def.js +170 -0
- package/dist/schema/client-document-def.js.map +1 -0
- package/dist/schema/client-document-def.test.d.ts +2 -0
- package/dist/schema/client-document-def.test.d.ts.map +1 -0
- package/dist/schema/client-document-def.test.js +201 -0
- package/dist/schema/client-document-def.test.js.map +1 -0
- package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
- 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 +4 -3
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +4 -3
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +26 -22
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +45 -43
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/sqlite-state.d.ts +12 -0
- package/dist/schema/sqlite-state.d.ts.map +1 -0
- package/dist/schema/sqlite-state.js +36 -0
- package/dist/schema/sqlite-state.js.map +1 -0
- package/dist/schema/system-tables.d.ts +67 -98
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +62 -48
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema/table-def.d.ts +26 -96
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +14 -64
- package/dist/schema/table-def.js.map +1 -1
- package/dist/schema/view.d.ts +3 -0
- package/dist/schema/view.d.ts.map +1 -0
- package/dist/schema/view.js +3 -0
- package/dist/schema/view.js.map +1 -0
- package/dist/schema-management/common.d.ts +4 -4
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +6 -6
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-mutation-defs.d.ts +3 -3
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
- package/dist/schema-management/validate-mutation-defs.js +17 -17
- package/dist/schema-management/validate-mutation-defs.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +7 -7
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +33 -30
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/facts.d.ts +19 -19
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +2 -2
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +3 -3
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +1 -1
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.js +1 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +7 -7
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +5 -5
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.calculator.test.js +38 -33
- package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +71 -71
- 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} +25 -25
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
- package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +60 -25
- 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 +3 -3
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/syncstate.d.ts +30 -30
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +73 -40
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +175 -184
- 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.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/fixture.ts +36 -15
- package/src/adapter-types.ts +33 -13
- package/src/debug-info.ts +1 -0
- package/src/devtools/devtools-messages-leader.ts +13 -13
- package/src/index.ts +2 -5
- package/src/leader-thread/LeaderSyncProcessor.ts +210 -138
- package/src/leader-thread/{apply-mutation.ts → apply-event.ts} +61 -61
- package/src/leader-thread/eventlog.ts +199 -0
- package/src/leader-thread/leader-worker-devtools.ts +22 -19
- package/src/leader-thread/make-leader-thread-layer.ts +51 -29
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/recreate-db.ts +6 -8
- package/src/leader-thread/types.ts +15 -16
- package/src/materializer-helper.ts +110 -0
- package/src/query-builder/api.ts +77 -103
- package/src/query-builder/astToSql.ts +68 -39
- package/src/query-builder/impl.test.ts +239 -42
- package/src/query-builder/impl.ts +72 -56
- package/src/{rehydrate-from-mutationlog.ts → rehydrate-from-eventlog.ts} +35 -38
- package/src/schema/EventDef.ts +216 -0
- package/src/schema/EventId.ts +23 -4
- package/src/schema/{MutationEvent.ts → LiveStoreEvent.ts} +68 -69
- package/src/schema/client-document-def.test.ts +239 -0
- package/src/schema/client-document-def.ts +444 -0
- package/src/schema/db-schema/dsl/mod.ts +0 -1
- package/src/schema/events.ts +1 -0
- package/src/schema/mod.ts +4 -3
- package/src/schema/schema.ts +78 -68
- package/src/schema/sqlite-state.ts +62 -0
- package/src/schema/system-tables.ts +42 -53
- package/src/schema/table-def.ts +51 -209
- package/src/schema/view.ts +2 -0
- package/src/schema-management/common.ts +4 -4
- package/src/schema-management/migrations.ts +8 -9
- package/src/schema-management/validate-mutation-defs.ts +22 -24
- package/src/sync/ClientSessionSyncProcessor.ts +41 -36
- package/src/sync/next/facts.ts +31 -32
- package/src/sync/next/history-dag-common.ts +4 -4
- package/src/sync/next/history-dag.ts +1 -1
- package/src/sync/next/rebase-events.ts +13 -13
- package/src/sync/next/test/compact-events.calculator.test.ts +45 -45
- package/src/sync/next/test/compact-events.test.ts +73 -73
- package/src/sync/next/test/event-fixtures.ts +219 -0
- package/src/sync/next/test/mod.ts +1 -1
- package/src/sync/sync.ts +3 -3
- package/src/sync/syncstate.test.ts +180 -189
- package/src/sync/syncstate.ts +162 -100
- package/src/sync/validate-push-payload.ts +2 -2
- package/src/version.ts +1 -1
- package/tsconfig.json +1 -0
- 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/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.map +0 -1
- package/dist/leader-thread/apply-mutation.js.map +0 -1
- package/dist/leader-thread/mutationlog.d.ts +0 -27
- package/dist/leader-thread/mutationlog.d.ts.map +0 -1
- package/dist/leader-thread/mutationlog.js +0 -124
- package/dist/leader-thread/mutationlog.js.map +0 -1
- package/dist/mutation.d.ts +0 -20
- package/dist/mutation.d.ts.map +0 -1
- package/dist/mutation.js +0 -68
- package/dist/mutation.js.map +0 -1
- package/dist/query-info.d.ts +0 -41
- 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 -15
- package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
- package/dist/rehydrate-from-mutationlog.js.map +0 -1
- package/dist/schema/MutationEvent.d.ts.map +0 -1
- package/dist/schema/MutationEvent.js.map +0 -1
- package/dist/schema/mutations.d.ts +0 -115
- 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/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 -170
- package/src/init-singleton-tables.ts +0 -24
- package/src/leader-thread/mutationlog.ts +0 -202
- package/src/mutation.ts +0 -108
- package/src/query-info.ts +0 -83
- package/src/schema/mutations.ts +0 -193
- package/src/sync/next/test/mutation-fixtures.ts +0 -228
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,cAAc,mCAAmC,CAAA;AACjD,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAA;AACzC,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,cAAc,mCAAmC,CAAA;AACjD,cAAc,0BAA0B,CAAA;AACxC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,iBAAiB,CAAA;AAC/B,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAA;AAC7C,cAAc,iBAAiB,CAAA;AAC/B,cAAc,0BAA0B,CAAA;AACxC,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,OAAO,KAAK,SAAS,MAAM,qBAAqB,CAAA;AAChD,cAAc,WAAW,CAAA"}
|
|
@@ -4,18 +4,16 @@ import type { SqliteDb } from '../adapter-types.js';
|
|
|
4
4
|
import { UnexpectedError } from '../adapter-types.js';
|
|
5
5
|
import type { LiveStoreSchema } from '../schema/mod.js';
|
|
6
6
|
import type { InitialBlockingSyncContext, LeaderSyncProcessor } from './types.js';
|
|
7
|
-
export declare const BACKEND_PUSH_BATCH_SIZE = 50;
|
|
8
|
-
export declare const LOCAL_PUSH_BATCH_SIZE = 10;
|
|
9
7
|
/**
|
|
10
|
-
* The LeaderSyncProcessor manages synchronization of
|
|
8
|
+
* The LeaderSyncProcessor manages synchronization of events between
|
|
11
9
|
* the local state and the sync backend, ensuring efficient and orderly processing.
|
|
12
10
|
*
|
|
13
11
|
* In the LeaderSyncProcessor, pulling always has precedence over pushing.
|
|
14
12
|
*
|
|
15
13
|
* Responsibilities:
|
|
16
|
-
* - Queueing incoming local
|
|
17
|
-
* - Broadcasting
|
|
18
|
-
* - Pushing
|
|
14
|
+
* - Queueing incoming local events in a localPushesQueue.
|
|
15
|
+
* - Broadcasting events to client sessions via pull queues.
|
|
16
|
+
* - Pushing events to the sync backend.
|
|
19
17
|
*
|
|
20
18
|
* Notes:
|
|
21
19
|
*
|
|
@@ -23,27 +21,42 @@ export declare const LOCAL_PUSH_BATCH_SIZE = 10;
|
|
|
23
21
|
* - localPushesQueue:
|
|
24
22
|
* - Maintains events in ascending order.
|
|
25
23
|
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
|
26
|
-
* - Processes events from the queue, applying
|
|
24
|
+
* - Processes events from the queue, applying events in batches.
|
|
27
25
|
* - Controlled by a `Latch` to manage execution flow.
|
|
28
26
|
* - The latch closes on pull receipt and re-opens post-pull completion.
|
|
29
27
|
* - Processes up to `maxBatchSize` events per cycle.
|
|
30
28
|
*
|
|
31
|
-
* Currently we're advancing the db read model and
|
|
29
|
+
* Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
|
|
32
30
|
*
|
|
33
31
|
* Tricky concurrency scenarios:
|
|
34
32
|
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
|
35
33
|
* Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
|
|
36
34
|
*
|
|
37
35
|
*/
|
|
38
|
-
export declare const makeLeaderSyncProcessor: ({ schema,
|
|
36
|
+
export declare const makeLeaderSyncProcessor: ({ schema, dbEventlogMissing, dbEventlog, dbReadModel, dbReadModelMissing, initialBlockingSyncContext, onError, params, testing, }: {
|
|
39
37
|
schema: LiveStoreSchema;
|
|
40
|
-
/** Only used to know whether we can safely query
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
/** Only used to know whether we can safely query dbEventlog during setup execution */
|
|
39
|
+
dbEventlogMissing: boolean;
|
|
40
|
+
dbEventlog: SqliteDb;
|
|
43
41
|
dbReadModel: SqliteDb;
|
|
44
42
|
/** Only used to know whether we can safely query dbReadModel during setup execution */
|
|
45
43
|
dbReadModelMissing: boolean;
|
|
46
44
|
initialBlockingSyncContext: InitialBlockingSyncContext;
|
|
47
45
|
onError: "shutdown" | "ignore";
|
|
46
|
+
params: {
|
|
47
|
+
/**
|
|
48
|
+
* @default 10
|
|
49
|
+
*/
|
|
50
|
+
localPushBatchSize?: number;
|
|
51
|
+
/**
|
|
52
|
+
* @default 50
|
|
53
|
+
*/
|
|
54
|
+
backendPushBatchSize?: number;
|
|
55
|
+
};
|
|
56
|
+
testing: {
|
|
57
|
+
delays?: {
|
|
58
|
+
localPushProcessing?: Effect.Effect<void>;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
48
61
|
}) => Effect.Effect<LeaderSyncProcessor, UnexpectedError, Scope.Scope>;
|
|
49
62
|
//# sourceMappingURL=LeaderSyncProcessor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LeaderSyncProcessor.d.ts","sourceRoot":"","sources":["../../src/leader-thread/LeaderSyncProcessor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,KAAK,EAAU,MAAM,yBAAyB,CAAA;AACjF,OAAO,EAGL,MAAM,EASP,MAAM,yBAAyB,CAAA;AAGhC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAavD,OAAO,KAAK,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"LeaderSyncProcessor.d.ts","sourceRoot":"","sources":["../../src/leader-thread/LeaderSyncProcessor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,KAAK,EAAU,MAAM,yBAAyB,CAAA;AACjF,OAAO,EAGL,MAAM,EASP,MAAM,yBAAyB,CAAA;AAGhC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAavD,OAAO,KAAK,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAUjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,uBAAuB,GAAI,mIAUrC;IACD,MAAM,EAAE,eAAe,CAAA;IACvB,sFAAsF;IACtF,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,QAAQ,CAAA;IACpB,WAAW,EAAE,QAAQ,CAAA;IACrB,uFAAuF;IACvF,kBAAkB,EAAE,OAAO,CAAA;IAC3B,0BAA0B,EAAE,0BAA0B,CAAA;IACtD,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAA;IAC9B,MAAM,EAAE;QACN;;WAEG;QACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B;;WAEG;QACH,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAC9B,CAAA;IACD,OAAO,EAAE;QACP,MAAM,CAAC,EAAE;YACP,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;SAC1C,CAAA;KACF,CAAA;CACF,KAAG,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,eAAe,EAAE,KAAK,CAAC,KAAK,CA2R/D,CAAA"}
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE } from '@livestore/utils';
|
|
2
2
|
import { BucketQueue, Deferred, Effect, Exit, FiberHandle, OtelTracer, Queue, ReadonlyArray, Stream, Subscribable, SubscriptionRef, } from '@livestore/utils/effect';
|
|
3
3
|
import { UnexpectedError } from '../adapter-types.js';
|
|
4
|
-
import { EventId,
|
|
4
|
+
import { EventId, getEventDef, LEADER_MERGE_COUNTER_TABLE, LiveStoreEvent, SESSION_CHANGESET_META_TABLE, } from '../schema/mod.js';
|
|
5
5
|
import { LeaderAheadError } from '../sync/sync.js';
|
|
6
6
|
import * as SyncState from '../sync/syncstate.js';
|
|
7
7
|
import { sql } from '../util.js';
|
|
8
|
-
import { rollback } from './apply-
|
|
9
|
-
import * as
|
|
8
|
+
import { rollback } from './apply-event.js';
|
|
9
|
+
import * as Eventlog from './eventlog.js';
|
|
10
10
|
import { LeaderThreadCtx } from './types.js';
|
|
11
|
-
export const BACKEND_PUSH_BATCH_SIZE = 50;
|
|
12
|
-
export const LOCAL_PUSH_BATCH_SIZE = 10;
|
|
13
11
|
/**
|
|
14
|
-
* The LeaderSyncProcessor manages synchronization of
|
|
12
|
+
* The LeaderSyncProcessor manages synchronization of events between
|
|
15
13
|
* the local state and the sync backend, ensuring efficient and orderly processing.
|
|
16
14
|
*
|
|
17
15
|
* In the LeaderSyncProcessor, pulling always has precedence over pushing.
|
|
18
16
|
*
|
|
19
17
|
* Responsibilities:
|
|
20
|
-
* - Queueing incoming local
|
|
21
|
-
* - Broadcasting
|
|
22
|
-
* - Pushing
|
|
18
|
+
* - Queueing incoming local events in a localPushesQueue.
|
|
19
|
+
* - Broadcasting events to client sessions via pull queues.
|
|
20
|
+
* - Pushing events to the sync backend.
|
|
23
21
|
*
|
|
24
22
|
* Notes:
|
|
25
23
|
*
|
|
@@ -27,24 +25,26 @@ export const LOCAL_PUSH_BATCH_SIZE = 10;
|
|
|
27
25
|
* - localPushesQueue:
|
|
28
26
|
* - Maintains events in ascending order.
|
|
29
27
|
* - Uses `Deferred` objects to resolve/reject events based on application success.
|
|
30
|
-
* - Processes events from the queue, applying
|
|
28
|
+
* - Processes events from the queue, applying events in batches.
|
|
31
29
|
* - Controlled by a `Latch` to manage execution flow.
|
|
32
30
|
* - The latch closes on pull receipt and re-opens post-pull completion.
|
|
33
31
|
* - Processes up to `maxBatchSize` events per cycle.
|
|
34
32
|
*
|
|
35
|
-
* Currently we're advancing the db read model and
|
|
33
|
+
* Currently we're advancing the db read model and eventlog in lockstep, but we could also decouple this in the future
|
|
36
34
|
*
|
|
37
35
|
* Tricky concurrency scenarios:
|
|
38
36
|
* - Queued local push batches becoming invalid due to a prior local push item being rejected.
|
|
39
37
|
* Solution: Introduce a generation number for local push batches which is used to filter out old batches items in case of rejection.
|
|
40
38
|
*
|
|
41
39
|
*/
|
|
42
|
-
export const makeLeaderSyncProcessor = ({ schema,
|
|
40
|
+
export const makeLeaderSyncProcessor = ({ schema, dbEventlogMissing, dbEventlog, dbReadModel, dbReadModelMissing, initialBlockingSyncContext, onError, params, testing, }) => Effect.gen(function* () {
|
|
43
41
|
const syncBackendPushQueue = yield* BucketQueue.make();
|
|
42
|
+
const localPushBatchSize = params.localPushBatchSize ?? 10;
|
|
43
|
+
const backendPushBatchSize = params.backendPushBatchSize ?? 50;
|
|
44
44
|
const syncStateSref = yield* SubscriptionRef.make(undefined);
|
|
45
|
-
const isClientEvent = (
|
|
46
|
-
const
|
|
47
|
-
return
|
|
45
|
+
const isClientEvent = (eventEncoded) => {
|
|
46
|
+
const eventDef = getEventDef(schema, eventEncoded.name);
|
|
47
|
+
return eventDef.eventDef.options.clientOnly;
|
|
48
48
|
};
|
|
49
49
|
const connectedClientSessionPullQueues = yield* makePullQueueSet;
|
|
50
50
|
/**
|
|
@@ -52,6 +52,7 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
52
52
|
* If a local-push batch is rejected, all subsequent push queue items with the same generation are also rejected,
|
|
53
53
|
* even if they would be valid on their own.
|
|
54
54
|
*/
|
|
55
|
+
// TODO get rid of this in favour of the `mergeGeneration` event id field
|
|
55
56
|
const currentLocalPushGenerationRef = { current: 0 };
|
|
56
57
|
const mergeCounterRef = { current: dbReadModelMissing ? 0 : yield* getMergeCounterFromDb(dbReadModel) };
|
|
57
58
|
const mergePayloads = new Map();
|
|
@@ -62,43 +63,57 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
62
63
|
const localPushesQueue = yield* BucketQueue.make();
|
|
63
64
|
const localPushesLatch = yield* Effect.makeLatch(true);
|
|
64
65
|
const pullLatch = yield* Effect.makeLatch(true);
|
|
66
|
+
/**
|
|
67
|
+
* Additionally to the `syncStateSref` we also need the `pushHeadRef` in order to prevent old/duplicate
|
|
68
|
+
* events from being pushed in a scenario like this:
|
|
69
|
+
* - client session A pushes e1
|
|
70
|
+
* - leader sync processor takes a bit and hasn't yet taken e1 from the localPushesQueue
|
|
71
|
+
* - client session B also pushes e1 (which should be rejected)
|
|
72
|
+
*
|
|
73
|
+
* Thus the purpoe of the pushHeadRef is the guard the integrity of the local push queue
|
|
74
|
+
*/
|
|
75
|
+
const pushHeadRef = { current: EventId.ROOT };
|
|
76
|
+
const advancePushHead = (eventId) => {
|
|
77
|
+
pushHeadRef.current = EventId.max(pushHeadRef.current, eventId);
|
|
78
|
+
};
|
|
65
79
|
// NOTE: New events are only pushed to sync backend after successful local push processing
|
|
66
80
|
const push = (newEvents, options) => Effect.gen(function* () {
|
|
67
|
-
// TODO validate batch
|
|
68
81
|
if (newEvents.length === 0)
|
|
69
82
|
return;
|
|
83
|
+
yield* validatePushBatch(newEvents, pushHeadRef.current);
|
|
84
|
+
advancePushHead(newEvents.at(-1).id);
|
|
70
85
|
const waitForProcessing = options?.waitForProcessing ?? false;
|
|
71
86
|
const generation = currentLocalPushGenerationRef.current;
|
|
72
87
|
if (waitForProcessing) {
|
|
73
88
|
const deferreds = yield* Effect.forEach(newEvents, () => Deferred.make());
|
|
74
|
-
const items = newEvents.map((
|
|
89
|
+
const items = newEvents.map((eventEncoded, i) => [eventEncoded, deferreds[i], generation]);
|
|
75
90
|
yield* BucketQueue.offerAll(localPushesQueue, items);
|
|
76
91
|
yield* Effect.all(deferreds);
|
|
77
92
|
}
|
|
78
93
|
else {
|
|
79
|
-
const items = newEvents.map((
|
|
94
|
+
const items = newEvents.map((eventEncoded) => [eventEncoded, undefined, generation]);
|
|
80
95
|
yield* BucketQueue.offerAll(localPushesQueue, items);
|
|
81
96
|
}
|
|
82
|
-
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:
|
|
97
|
+
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:push', {
|
|
83
98
|
attributes: {
|
|
84
99
|
batchSize: newEvents.length,
|
|
85
100
|
batch: TRACE_VERBOSE ? newEvents : undefined,
|
|
86
101
|
},
|
|
87
102
|
links: ctxRef.current?.span ? [{ _tag: 'SpanLink', span: ctxRef.current.span, attributes: {} }] : undefined,
|
|
88
103
|
}));
|
|
89
|
-
const pushPartial = ({
|
|
104
|
+
const pushPartial = ({ event: { name, args }, clientId, sessionId }) => Effect.gen(function* () {
|
|
90
105
|
const syncState = yield* syncStateSref;
|
|
91
106
|
if (syncState === undefined)
|
|
92
107
|
return shouldNeverHappen('Not initialized');
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
108
|
+
const eventDef = getEventDef(schema, name);
|
|
109
|
+
const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
|
|
110
|
+
name,
|
|
96
111
|
args,
|
|
97
112
|
clientId,
|
|
98
113
|
sessionId,
|
|
99
|
-
...EventId.nextPair(syncState.localHead,
|
|
114
|
+
...EventId.nextPair(syncState.localHead, eventDef.eventDef.options.clientOnly),
|
|
100
115
|
});
|
|
101
|
-
yield* push([
|
|
116
|
+
yield* push([eventEncoded]);
|
|
102
117
|
}).pipe(Effect.catchTag('LeaderAheadError', Effect.orDie));
|
|
103
118
|
// Starts various background loops
|
|
104
119
|
const boot = Effect.gen(function* () {
|
|
@@ -112,33 +127,31 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
112
127
|
devtoolsLatch: devtools.enabled ? devtools.syncBackendLatch : undefined,
|
|
113
128
|
runtime,
|
|
114
129
|
};
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
: Mutationlog.getBackendHeadFromDb(dbMutationLog);
|
|
118
|
-
const initialLocalHead = dbMutationLogMissing ? EventId.ROOT : Mutationlog.getClientHeadFromDb(dbMutationLog);
|
|
130
|
+
const initialLocalHead = dbEventlogMissing ? EventId.ROOT : Eventlog.getClientHeadFromDb(dbEventlog);
|
|
131
|
+
const initialBackendHead = dbEventlogMissing ? EventId.ROOT.global : Eventlog.getBackendHeadFromDb(dbEventlog);
|
|
119
132
|
if (initialBackendHead > initialLocalHead.global) {
|
|
120
133
|
return shouldNeverHappen(`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`);
|
|
121
134
|
}
|
|
122
|
-
const
|
|
135
|
+
const pendingEvents = dbEventlogMissing
|
|
123
136
|
? []
|
|
124
|
-
: yield*
|
|
137
|
+
: yield* Eventlog.getEventsSince({ global: initialBackendHead, client: EventId.clientDefault });
|
|
125
138
|
const initialSyncState = new SyncState.SyncState({
|
|
126
|
-
pending:
|
|
139
|
+
pending: pendingEvents,
|
|
127
140
|
upstreamHead: { global: initialBackendHead, client: EventId.clientDefault },
|
|
128
141
|
localHead: initialLocalHead,
|
|
129
142
|
});
|
|
130
143
|
/** State transitions need to happen atomically, so we use a Ref to track the state */
|
|
131
144
|
yield* SubscriptionRef.set(syncStateSref, initialSyncState);
|
|
132
145
|
// Rehydrate sync queue
|
|
133
|
-
if (
|
|
134
|
-
const
|
|
135
|
-
// Don't sync clientOnly
|
|
136
|
-
.filter((
|
|
137
|
-
const
|
|
138
|
-
return
|
|
146
|
+
if (pendingEvents.length > 0) {
|
|
147
|
+
const globalPendingEvents = pendingEvents
|
|
148
|
+
// Don't sync clientOnly events
|
|
149
|
+
.filter((eventEncoded) => {
|
|
150
|
+
const eventDef = getEventDef(schema, eventEncoded.name);
|
|
151
|
+
return eventDef.eventDef.options.clientOnly === false;
|
|
139
152
|
});
|
|
140
|
-
if (
|
|
141
|
-
yield* BucketQueue.offerAll(syncBackendPushQueue,
|
|
153
|
+
if (globalPendingEvents.length > 0) {
|
|
154
|
+
yield* BucketQueue.offerAll(syncBackendPushQueue, globalPendingEvents);
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
const shutdownOnError = (cause) => Effect.gen(function* () {
|
|
@@ -160,13 +173,19 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
160
173
|
connectedClientSessionPullQueues,
|
|
161
174
|
mergeCounterRef,
|
|
162
175
|
mergePayloads,
|
|
176
|
+
localPushBatchSize,
|
|
177
|
+
testing: {
|
|
178
|
+
delay: testing?.delays?.localPushProcessing,
|
|
179
|
+
},
|
|
163
180
|
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped);
|
|
164
181
|
const backendPushingFiberHandle = yield* FiberHandle.make();
|
|
165
|
-
|
|
182
|
+
const backendPushingEffect = backgroundBackendPushing({
|
|
166
183
|
syncBackendPushQueue,
|
|
167
184
|
otelSpan,
|
|
168
185
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
169
|
-
|
|
186
|
+
backendPushBatchSize,
|
|
187
|
+
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError));
|
|
188
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect);
|
|
170
189
|
yield* backgroundBackendPulling({
|
|
171
190
|
initialBackendHead,
|
|
172
191
|
isClientEvent,
|
|
@@ -177,11 +196,7 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
177
196
|
yield* BucketQueue.clear(syncBackendPushQueue);
|
|
178
197
|
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredRebasedPending);
|
|
179
198
|
// Restart pushing fiber
|
|
180
|
-
yield* FiberHandle.run(backendPushingFiberHandle,
|
|
181
|
-
syncBackendPushQueue,
|
|
182
|
-
otelSpan,
|
|
183
|
-
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
184
|
-
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError)));
|
|
199
|
+
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect);
|
|
185
200
|
}),
|
|
186
201
|
syncStateSref,
|
|
187
202
|
localPushesLatch,
|
|
@@ -192,23 +207,36 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
192
207
|
connectedClientSessionPullQueues,
|
|
193
208
|
mergeCounterRef,
|
|
194
209
|
mergePayloads,
|
|
210
|
+
advancePushHead,
|
|
195
211
|
}).pipe(Effect.tapCauseLogPretty, Effect.catchAllCause(shutdownOnError), Effect.forkScoped);
|
|
196
212
|
return { initialLeaderHead: initialLocalHead };
|
|
197
213
|
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'));
|
|
198
|
-
const pull = ({ cursor }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}).pipe(Stream.unwrapScoped);
|
|
203
|
-
};
|
|
214
|
+
const pull = ({ cursor }) => Effect.gen(function* () {
|
|
215
|
+
const queue = yield* pullQueue({ cursor });
|
|
216
|
+
return Stream.fromQueue(queue);
|
|
217
|
+
}).pipe(Stream.unwrapScoped);
|
|
204
218
|
const pullQueue = ({ cursor }) => {
|
|
205
219
|
const runtime = ctxRef.current?.runtime ?? shouldNeverHappen('Not initialized');
|
|
206
220
|
return Effect.gen(function* () {
|
|
207
|
-
const queue = yield* connectedClientSessionPullQueues.makeQueue
|
|
221
|
+
const queue = yield* connectedClientSessionPullQueues.makeQueue;
|
|
208
222
|
const payloadsSinceCursor = Array.from(mergePayloads.entries())
|
|
209
223
|
.map(([mergeCounter, payload]) => ({ payload, mergeCounter }))
|
|
210
|
-
.filter(({ mergeCounter }) => mergeCounter > cursor)
|
|
211
|
-
.toSorted((a, b) => a.mergeCounter - b.mergeCounter)
|
|
224
|
+
.filter(({ mergeCounter }) => mergeCounter > cursor.mergeCounter)
|
|
225
|
+
.toSorted((a, b) => a.mergeCounter - b.mergeCounter)
|
|
226
|
+
.map(({ payload, mergeCounter }) => {
|
|
227
|
+
if (payload._tag === 'upstream-advance') {
|
|
228
|
+
return {
|
|
229
|
+
payload: {
|
|
230
|
+
_tag: 'upstream-advance',
|
|
231
|
+
newEvents: ReadonlyArray.dropWhile(payload.newEvents, (eventEncoded) => EventId.isGreaterThanOrEqual(cursor.eventId, eventEncoded.id)),
|
|
232
|
+
},
|
|
233
|
+
mergeCounter,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
return { payload, mergeCounter };
|
|
238
|
+
}
|
|
239
|
+
});
|
|
212
240
|
yield* queue.offerAll(payloadsSinceCursor);
|
|
213
241
|
return queue;
|
|
214
242
|
}).pipe(Effect.provide(runtime));
|
|
@@ -232,10 +260,12 @@ export const makeLeaderSyncProcessor = ({ schema, dbMutationLogMissing, dbMutati
|
|
|
232
260
|
getMergeCounter: () => mergeCounterRef.current,
|
|
233
261
|
};
|
|
234
262
|
});
|
|
235
|
-
const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLatch, syncStateSref, syncBackendPushQueue, schema, isClientEvent, otelSpan, currentLocalPushGenerationRef, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, }) => Effect.gen(function* () {
|
|
263
|
+
const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLatch, syncStateSref, syncBackendPushQueue, schema, isClientEvent, otelSpan, currentLocalPushGenerationRef, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, localPushBatchSize, testing, }) => Effect.gen(function* () {
|
|
236
264
|
while (true) {
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
if (testing.delay !== undefined) {
|
|
266
|
+
yield* testing.delay.pipe(Effect.withSpan('localPushProcessingDelay'));
|
|
267
|
+
}
|
|
268
|
+
const batchItems = yield* BucketQueue.takeBetween(localPushesQueue, 1, localPushBatchSize);
|
|
239
269
|
// Wait for the backend pulling to finish
|
|
240
270
|
yield* localPushesLatch.await;
|
|
241
271
|
// Prevent backend pull processing until this local push is finished
|
|
@@ -244,7 +274,7 @@ const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLa
|
|
|
244
274
|
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
|
245
275
|
const filteredBatchItems = batchItems
|
|
246
276
|
.filter(([_1, _2, generation]) => generation === currentLocalPushGenerationRef.current)
|
|
247
|
-
.map(([
|
|
277
|
+
.map(([eventEncoded, deferred]) => [eventEncoded, deferred]);
|
|
248
278
|
if (filteredBatchItems.length === 0) {
|
|
249
279
|
// console.log('dropping old-gen batch', currentLocalPushGenerationRef.current)
|
|
250
280
|
// Allow the backend pulling to start
|
|
@@ -259,12 +289,12 @@ const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLa
|
|
|
259
289
|
syncState,
|
|
260
290
|
payload: { _tag: 'local-push', newEvents },
|
|
261
291
|
isClientEvent,
|
|
262
|
-
isEqualEvent:
|
|
292
|
+
isEqualEvent: LiveStoreEvent.isEqualEncoded,
|
|
263
293
|
});
|
|
264
294
|
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef);
|
|
265
295
|
switch (mergeResult._tag) {
|
|
266
296
|
case 'unexpected-error': {
|
|
267
|
-
otelSpan?.addEvent(`
|
|
297
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:unexpected-error`, {
|
|
268
298
|
batchSize: newEvents.length,
|
|
269
299
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
270
300
|
});
|
|
@@ -274,7 +304,7 @@ const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLa
|
|
|
274
304
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push');
|
|
275
305
|
}
|
|
276
306
|
case 'reject': {
|
|
277
|
-
otelSpan?.addEvent(`
|
|
307
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:reject`, {
|
|
278
308
|
batchSize: newEvents.length,
|
|
279
309
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
280
310
|
});
|
|
@@ -319,48 +349,48 @@ const backgroundApplyLocalPushes = ({ localPushesLatch, localPushesQueue, pullLa
|
|
|
319
349
|
mergeCounter,
|
|
320
350
|
});
|
|
321
351
|
mergePayloads.set(mergeCounter, SyncState.PayloadUpstreamAdvance.make({ newEvents: mergeResult.newEvents }));
|
|
322
|
-
otelSpan?.addEvent(`
|
|
352
|
+
otelSpan?.addEvent(`[${mergeCounter}]:push:advance`, {
|
|
323
353
|
batchSize: newEvents.length,
|
|
324
354
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
325
355
|
});
|
|
326
|
-
// Don't sync clientOnly
|
|
327
|
-
const filteredBatch = mergeResult.newEvents.filter((
|
|
328
|
-
const
|
|
329
|
-
return
|
|
356
|
+
// Don't sync clientOnly events
|
|
357
|
+
const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
|
|
358
|
+
const eventDef = getEventDef(schema, eventEncoded.name);
|
|
359
|
+
return eventDef.eventDef.options.clientOnly === false;
|
|
330
360
|
});
|
|
331
361
|
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch);
|
|
332
|
-
yield*
|
|
362
|
+
yield* applyEventsBatch({ batchItems: mergeResult.newEvents, deferreds });
|
|
333
363
|
// Allow the backend pulling to start
|
|
334
364
|
yield* pullLatch.open;
|
|
335
365
|
}
|
|
336
366
|
});
|
|
337
367
|
// TODO how to handle errors gracefully
|
|
338
|
-
const
|
|
339
|
-
const { dbReadModel: db,
|
|
340
|
-
// NOTE We always start a transaction to ensure consistency between db and
|
|
368
|
+
const applyEventsBatch = ({ batchItems, deferreds }) => Effect.gen(function* () {
|
|
369
|
+
const { dbReadModel: db, dbEventlog, applyEvent } = yield* LeaderThreadCtx;
|
|
370
|
+
// NOTE We always start a transaction to ensure consistency between db and eventlog (even for single-item batches)
|
|
341
371
|
db.execute('BEGIN TRANSACTION', undefined); // Start the transaction
|
|
342
|
-
|
|
372
|
+
dbEventlog.execute('BEGIN TRANSACTION', undefined); // Start the transaction
|
|
343
373
|
yield* Effect.addFinalizer((exit) => Effect.gen(function* () {
|
|
344
374
|
if (Exit.isSuccess(exit))
|
|
345
375
|
return;
|
|
346
376
|
// Rollback in case of an error
|
|
347
377
|
db.execute('ROLLBACK', undefined);
|
|
348
|
-
|
|
378
|
+
dbEventlog.execute('ROLLBACK', undefined);
|
|
349
379
|
}));
|
|
350
380
|
for (let i = 0; i < batchItems.length; i++) {
|
|
351
|
-
const { sessionChangeset } = yield*
|
|
381
|
+
const { sessionChangeset } = yield* applyEvent(batchItems[i]);
|
|
352
382
|
batchItems[i].meta.sessionChangeset = sessionChangeset;
|
|
353
383
|
if (deferreds?.[i] !== undefined) {
|
|
354
384
|
yield* Deferred.succeed(deferreds[i], void 0);
|
|
355
385
|
}
|
|
356
386
|
}
|
|
357
387
|
db.execute('COMMIT', undefined); // Commit the transaction
|
|
358
|
-
|
|
359
|
-
}).pipe(Effect.uninterruptible, Effect.scoped, Effect.withSpan('@livestore/common:LeaderSyncProcessor:
|
|
388
|
+
dbEventlog.execute('COMMIT', undefined); // Commit the transaction
|
|
389
|
+
}).pipe(Effect.uninterruptible, Effect.scoped, Effect.withSpan('@livestore/common:LeaderSyncProcessor:applyEventItems', {
|
|
360
390
|
attributes: { batchSize: batchItems.length },
|
|
361
391
|
}), Effect.tapCauseLogPretty, UnexpectedError.mapToUnexpectedError);
|
|
362
|
-
const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBackendPushing, otelSpan, syncStateSref, localPushesLatch, pullLatch, devtoolsLatch, initialBlockingSyncContext, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, }) => Effect.gen(function* () {
|
|
363
|
-
const { syncBackend, dbReadModel: db,
|
|
392
|
+
const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBackendPushing, otelSpan, syncStateSref, localPushesLatch, pullLatch, devtoolsLatch, initialBlockingSyncContext, connectedClientSessionPullQueues, mergeCounterRef, mergePayloads, advancePushHead, }) => Effect.gen(function* () {
|
|
393
|
+
const { syncBackend, dbReadModel: db, dbEventlog, schema } = yield* LeaderThreadCtx;
|
|
364
394
|
if (syncBackend === undefined)
|
|
365
395
|
return;
|
|
366
396
|
const onNewPullChunk = (newEvents, remaining) => Effect.gen(function* () {
|
|
@@ -380,7 +410,7 @@ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBa
|
|
|
380
410
|
syncState,
|
|
381
411
|
payload: SyncState.PayloadUpstreamAdvance.make({ newEvents }),
|
|
382
412
|
isClientEvent,
|
|
383
|
-
isEqualEvent:
|
|
413
|
+
isEqualEvent: LiveStoreEvent.isEqualEncoded,
|
|
384
414
|
ignoreClientEvents: true,
|
|
385
415
|
});
|
|
386
416
|
const mergeCounter = yield* incrementMergeCounter(mergeCounterRef);
|
|
@@ -388,28 +418,28 @@ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBa
|
|
|
388
418
|
return shouldNeverHappen('The leader thread should never reject upstream advances');
|
|
389
419
|
}
|
|
390
420
|
else if (mergeResult._tag === 'unexpected-error') {
|
|
391
|
-
otelSpan?.addEvent(`
|
|
421
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:unexpected-error`, {
|
|
392
422
|
newEventsCount: newEvents.length,
|
|
393
423
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
394
424
|
});
|
|
395
425
|
return yield* Effect.fail(mergeResult.cause);
|
|
396
426
|
}
|
|
397
427
|
const newBackendHead = newEvents.at(-1).id;
|
|
398
|
-
|
|
428
|
+
Eventlog.updateBackendHead(dbEventlog, newBackendHead);
|
|
399
429
|
if (mergeResult._tag === 'rebase') {
|
|
400
|
-
otelSpan?.addEvent(`
|
|
430
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:rebase`, {
|
|
401
431
|
newEventsCount: newEvents.length,
|
|
402
432
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
403
433
|
rollbackCount: mergeResult.rollbackEvents.length,
|
|
404
434
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
405
435
|
});
|
|
406
|
-
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((
|
|
407
|
-
const
|
|
408
|
-
return
|
|
436
|
+
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
437
|
+
const eventDef = getEventDef(schema, event.name);
|
|
438
|
+
return eventDef.eventDef.options.clientOnly === false;
|
|
409
439
|
});
|
|
410
440
|
yield* restartBackendPushing(globalRebasedPendingEvents);
|
|
411
441
|
if (mergeResult.rollbackEvents.length > 0) {
|
|
412
|
-
yield* rollback({ db,
|
|
442
|
+
yield* rollback({ db, dbEventlog, eventIdsToRollback: mergeResult.rollbackEvents.map((_) => _.id) });
|
|
413
443
|
}
|
|
414
444
|
yield* connectedClientSessionPullQueues.offer({
|
|
415
445
|
payload: SyncState.PayloadUpstreamRebase.make({
|
|
@@ -424,7 +454,7 @@ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBa
|
|
|
424
454
|
}));
|
|
425
455
|
}
|
|
426
456
|
else {
|
|
427
|
-
otelSpan?.addEvent(`
|
|
457
|
+
otelSpan?.addEvent(`[${mergeCounter}]:pull:advance`, {
|
|
428
458
|
newEventsCount: newEvents.length,
|
|
429
459
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
430
460
|
});
|
|
@@ -436,20 +466,21 @@ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBa
|
|
|
436
466
|
if (mergeResult.confirmedEvents.length > 0) {
|
|
437
467
|
// `mergeResult.confirmedEvents` don't contain the correct sync metadata, so we need to use
|
|
438
468
|
// `newEvents` instead which we filter via `mergeResult.confirmedEvents`
|
|
439
|
-
const confirmedNewEvents = newEvents.filter((
|
|
440
|
-
yield*
|
|
469
|
+
const confirmedNewEvents = newEvents.filter((event) => mergeResult.confirmedEvents.some((confirmedEvent) => EventId.isEqual(event.id, confirmedEvent.id)));
|
|
470
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents);
|
|
441
471
|
}
|
|
442
472
|
}
|
|
443
473
|
// Removes the changeset rows which are no longer needed as we'll never have to rollback beyond this point
|
|
444
474
|
trimChangesetRows(db, newBackendHead);
|
|
445
|
-
|
|
475
|
+
advancePushHead(mergeResult.newSyncState.localHead);
|
|
476
|
+
yield* applyEventsBatch({ batchItems: mergeResult.newEvents, deferreds: undefined });
|
|
446
477
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState);
|
|
447
478
|
// Allow local pushes to be processed again
|
|
448
479
|
if (remaining === 0) {
|
|
449
480
|
yield* localPushesLatch.open;
|
|
450
481
|
}
|
|
451
482
|
});
|
|
452
|
-
const cursorInfo = yield*
|
|
483
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo(initialBackendHead);
|
|
453
484
|
yield* syncBackend.pull(cursorInfo).pipe(
|
|
454
485
|
// TODO only take from queue while connected
|
|
455
486
|
Stream.tap(({ batch, remaining }) => Effect.gen(function* () {
|
|
@@ -459,22 +490,21 @@ const backgroundBackendPulling = ({ initialBackendHead, isClientEvent, restartBa
|
|
|
459
490
|
// batch: TRACE_VERBOSE ? batch : undefined,
|
|
460
491
|
// },
|
|
461
492
|
// })
|
|
462
|
-
// NOTE we only want to take process
|
|
493
|
+
// NOTE we only want to take process events when the sync backend is connected
|
|
463
494
|
// (e.g. needed for simulating being offline)
|
|
464
495
|
// TODO remove when there's a better way to handle this in stream above
|
|
465
496
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
|
|
466
|
-
yield* onNewPullChunk(batch.map((_) =>
|
|
497
|
+
yield* onNewPullChunk(batch.map((_) => LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, _.metadata)), remaining);
|
|
467
498
|
yield* initialBlockingSyncContext.update({ processed: batch.length, remaining });
|
|
468
499
|
})), Stream.runDrain, Effect.interruptible);
|
|
469
500
|
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'));
|
|
470
|
-
const backgroundBackendPushing = ({ syncBackendPushQueue, otelSpan, devtoolsLatch, }) => Effect.gen(function* () {
|
|
501
|
+
const backgroundBackendPushing = ({ syncBackendPushQueue, otelSpan, devtoolsLatch, backendPushBatchSize, }) => Effect.gen(function* () {
|
|
471
502
|
const { syncBackend } = yield* LeaderThreadCtx;
|
|
472
503
|
if (syncBackend === undefined)
|
|
473
504
|
return;
|
|
474
505
|
while (true) {
|
|
475
506
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
|
|
476
|
-
|
|
477
|
-
const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, BACKEND_PUSH_BATCH_SIZE);
|
|
507
|
+
const queueItems = yield* BucketQueue.takeBetween(syncBackendPushQueue, 1, backendPushBatchSize);
|
|
478
508
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true);
|
|
479
509
|
if (devtoolsLatch !== undefined) {
|
|
480
510
|
yield* devtoolsLatch.await;
|
|
@@ -508,7 +538,7 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
508
538
|
}
|
|
509
539
|
set.clear();
|
|
510
540
|
}));
|
|
511
|
-
const makeQueue =
|
|
541
|
+
const makeQueue = Effect.gen(function* () {
|
|
512
542
|
const queue = yield* Queue.unbounded().pipe(Effect.acquireRelease(Queue.shutdown));
|
|
513
543
|
yield* Effect.addFinalizer(() => Effect.sync(() => set.delete(queue)));
|
|
514
544
|
set.add(queue);
|
|
@@ -538,4 +568,22 @@ const getMergeCounterFromDb = (dbReadModel) => Effect.gen(function* () {
|
|
|
538
568
|
const result = dbReadModel.select(sql `SELECT mergeCounter FROM ${LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`);
|
|
539
569
|
return result[0]?.mergeCounter ?? 0;
|
|
540
570
|
});
|
|
571
|
+
const validatePushBatch = (batch, pushHead) => Effect.gen(function* () {
|
|
572
|
+
if (batch.length === 0) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Make sure batch is monotonically increasing
|
|
576
|
+
for (let i = 1; i < batch.length; i++) {
|
|
577
|
+
if (EventId.isGreaterThanOrEqual(batch[i - 1].id, batch[i].id)) {
|
|
578
|
+
shouldNeverHappen(`Events must be ordered in monotonically ascending order by eventId. Received: [${batch.map((e) => EventId.toString(e.id)).join(', ')}]`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Make sure smallest event id is > pushHead
|
|
582
|
+
if (EventId.isGreaterThanOrEqual(pushHead, batch[0].id)) {
|
|
583
|
+
return yield* LeaderAheadError.make({
|
|
584
|
+
minimumExpectedId: pushHead,
|
|
585
|
+
providedId: batch[0].id,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
});
|
|
541
589
|
//# sourceMappingURL=LeaderSyncProcessor.js.map
|