@livestore/common 0.4.0-dev.1 → 0.4.0-dev.11
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 +8 -3
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
- package/dist/adapter-types.d.ts +9 -3
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +7 -14
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +1 -6
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +27 -25
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +47 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +22 -3
- package/dist/errors.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +135 -52
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +4 -10
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +4 -6
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +6 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +68 -19
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
- package/dist/leader-thread/materialize-event.d.ts +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +23 -9
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +2 -3
- package/dist/leader-thread/recreate-db.d.ts.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/types.d.ts +7 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/materializer-helper.d.ts +1 -1
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +20 -4
- package/dist/materializer-helper.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 +25 -16
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +3 -0
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +4 -1
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +4 -1
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +2 -2
- package/dist/schema/LiveStoreEvent.d.ts +1 -1
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +1 -2
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/mod.d.ts +2 -0
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +1 -0
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +15 -0
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +26 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +95 -4
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
- 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 +14 -6
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts +6 -2
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +128 -185
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +116 -73
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +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 +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +1 -1
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +2 -0
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +4 -4
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +2 -2
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +87 -2
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts +47 -0
- package/dist/schema/unknown-events.d.ts.map +1 -0
- package/dist/schema/unknown-events.js +69 -0
- package/dist/schema/unknown-events.js.map +1 -0
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +2 -1
- package/dist/sql-queries/sql-query-builder.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +36 -33
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +61 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +36 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +3 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mock-sync-backend.d.ts +23 -0
- package/dist/sync/mock-sync-backend.d.ts.map +1 -0
- package/dist/sync/mock-sync-backend.js +114 -0
- package/dist/sync/mock-sync-backend.js.map +1 -0
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +4 -5
- 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 +1 -2
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +50 -11
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +193 -4
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/sync-backend-kv.d.ts +7 -0
- package/dist/sync/sync-backend-kv.d.ts.map +1 -0
- package/dist/sync/sync-backend-kv.js +18 -0
- package/dist/sync/sync-backend-kv.js.map +1 -0
- package/dist/sync/sync-backend.d.ts +105 -0
- package/dist/sync/sync-backend.d.ts.map +1 -0
- package/dist/sync/sync-backend.js +61 -0
- package/dist/sync/sync-backend.js.map +1 -0
- package/dist/sync/sync.d.ts +6 -84
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +2 -27
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.test.js +16 -15
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/transport-chunking.d.ts +36 -0
- package/dist/sync/transport-chunking.d.ts.map +1 -0
- package/dist/sync/transport-chunking.js +56 -0
- package/dist/sync/transport-chunking.js.map +1 -0
- package/dist/sync/validate-push-payload.d.ts +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +6 -6
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/testing/event-factory.d.ts +68 -0
- package/dist/testing/event-factory.d.ts.map +1 -0
- package/dist/testing/event-factory.js +80 -0
- package/dist/testing/event-factory.js.map +1 -0
- package/dist/testing/mod.d.ts +2 -0
- package/dist/testing/mod.d.ts.map +1 -0
- package/dist/testing/mod.js +2 -0
- package/dist/testing/mod.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +7 -8
- package/src/ClientSessionLeaderThreadProxy.ts +8 -3
- package/src/adapter-types.ts +13 -3
- package/src/devtools/devtools-messages-common.ts +1 -8
- package/src/errors.ts +33 -4
- package/src/leader-thread/LeaderSyncProcessor.ts +204 -63
- package/src/leader-thread/eventlog.ts +10 -6
- package/src/leader-thread/leader-worker-devtools.ts +6 -2
- package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
- package/src/leader-thread/make-leader-thread-layer.ts +137 -26
- package/src/leader-thread/materialize-event.ts +34 -9
- package/src/leader-thread/recreate-db.ts +11 -3
- package/src/leader-thread/shutdown-channel.ts +16 -2
- package/src/leader-thread/types.ts +7 -5
- package/src/materializer-helper.ts +22 -5
- package/src/rematerialize-from-eventlog.ts +33 -23
- package/src/schema/EventDef.ts +3 -0
- package/src/schema/EventSequenceNumber.test.ts +2 -2
- package/src/schema/EventSequenceNumber.ts +5 -2
- package/src/schema/LiveStoreEvent.ts +1 -2
- package/src/schema/mod.ts +2 -0
- package/src/schema/schema.ts +37 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
- package/src/schema/state/sqlite/client-document-def.ts +117 -5
- package/src/schema/state/sqlite/column-annotations.ts +16 -6
- package/src/schema/state/sqlite/column-def.test.ts +150 -93
- package/src/schema/state/sqlite/column-def.ts +136 -203
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
- package/src/schema/state/sqlite/mod.ts +1 -0
- package/src/schema/state/sqlite/query-builder/api.ts +7 -2
- package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
- package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
- package/src/schema/state/sqlite/system-tables.ts +2 -0
- package/src/schema/state/sqlite/table-def.test.ts +112 -2
- package/src/schema/state/sqlite/table-def.ts +9 -8
- package/src/schema/unknown-events.ts +131 -0
- package/src/sql-queries/sql-query-builder.ts +2 -1
- package/src/sync/ClientSessionSyncProcessor.ts +56 -49
- package/src/sync/errors.ts +38 -0
- package/src/sync/index.ts +3 -0
- package/src/sync/mock-sync-backend.ts +184 -0
- package/src/sync/next/compact-events.ts +4 -5
- package/src/sync/next/facts.ts +1 -3
- package/src/sync/next/history-dag-common.ts +272 -21
- package/src/sync/next/history-dag.ts +3 -1
- package/src/sync/sync-backend-kv.ts +22 -0
- package/src/sync/sync-backend.ts +185 -0
- package/src/sync/sync.ts +6 -89
- package/src/sync/syncstate.test.ts +17 -15
- package/src/sync/transport-chunking.ts +90 -0
- package/src/sync/validate-push-payload.ts +6 -7
- package/src/testing/event-factory.ts +133 -0
- package/src/testing/mod.ts +1 -0
- package/src/version.ts +2 -2
- package/dist/schema-management/migrations.test.d.ts +0 -2
- package/dist/schema-management/migrations.test.d.ts.map +0 -1
- package/dist/schema-management/migrations.test.js +0 -52
- package/dist/schema-management/migrations.test.js.map +0 -1
- package/dist/sync/next/graphology.d.ts +0 -8
- package/dist/sync/next/graphology.d.ts.map +0 -1
- package/dist/sync/next/graphology.js +0 -30
- package/dist/sync/next/graphology.js.map +0 -1
- package/dist/sync/next/graphology_.d.ts +0 -3
- package/dist/sync/next/graphology_.d.ts.map +0 -1
- package/dist/sync/next/graphology_.js +0 -3
- package/dist/sync/next/graphology_.js.map +0 -1
- package/src/sync/next/ambient.d.ts +0 -3
- package/src/sync/next/graphology.ts +0 -41
- package/src/sync/next/graphology_.ts +0 -2
|
@@ -2,14 +2,7 @@ import { Schema } from '@livestore/utils/effect'
|
|
|
2
2
|
|
|
3
3
|
import { liveStoreVersion as pkgVersion } from '../version.ts'
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
isConnected: Schema.Boolean,
|
|
7
|
-
timestampMs: Schema.Number,
|
|
8
|
-
/** Whether the network status devtools latch is closed. Used to simulate network disconnection. */
|
|
9
|
-
latchClosed: Schema.Boolean,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
export type NetworkStatus = typeof NetworkStatus.Type
|
|
5
|
+
export { NetworkStatus } from '../sync/sync-backend.ts'
|
|
13
6
|
|
|
14
7
|
export const requestId = Schema.String
|
|
15
8
|
export const clientId = Schema.String
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Effect, Schema, Stream } from '@livestore/utils/effect'
|
|
1
|
+
import { Cause, Effect, Layer, Schema, Stream } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
import * as LiveStoreEvent from './schema/LiveStoreEvent.ts'
|
|
2
4
|
|
|
3
5
|
export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('LiveStore.UnexpectedError', {
|
|
4
6
|
cause: Schema.Defect,
|
|
@@ -11,15 +13,30 @@ export class UnexpectedError extends Schema.TaggedError<UnexpectedError>()('Live
|
|
|
11
13
|
Effect.catchAllDefect((cause) => new UnexpectedError({ cause })),
|
|
12
14
|
)
|
|
13
15
|
|
|
16
|
+
static mapToUnexpectedErrorLayer = <A, E, R>(layer: Layer.Layer<A, E, R>) =>
|
|
17
|
+
layer.pipe(
|
|
18
|
+
Layer.catchAllCause((cause) =>
|
|
19
|
+
Cause.isFailType(cause) && Schema.is(UnexpectedError)(cause.error)
|
|
20
|
+
? Layer.fail(cause.error)
|
|
21
|
+
: Layer.fail(new UnexpectedError({ cause: cause })),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
14
25
|
static mapToUnexpectedErrorStream = <A, E, R>(stream: Stream.Stream<A, E, R>) =>
|
|
15
26
|
stream.pipe(
|
|
16
27
|
Stream.mapError((cause) => (Schema.is(UnexpectedError)(cause) ? cause : new UnexpectedError({ cause }))),
|
|
17
28
|
)
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
export class
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
export class MaterializerHashMismatchError extends Schema.TaggedError<MaterializerHashMismatchError>()(
|
|
32
|
+
'LiveStore.MaterializerHashMismatchError',
|
|
33
|
+
{
|
|
34
|
+
eventName: Schema.String,
|
|
35
|
+
note: Schema.optionalWith(Schema.String, {
|
|
36
|
+
default: () => 'Please make sure your event materializer is a pure function without side effects.',
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
) {}
|
|
23
40
|
|
|
24
41
|
export class IntentionalShutdownCause extends Schema.TaggedError<IntentionalShutdownCause>()(
|
|
25
42
|
'LiveStore.IntentionalShutdownCause',
|
|
@@ -47,3 +64,15 @@ export class SqliteError extends Schema.TaggedError<SqliteError>()('LiveStore.Sq
|
|
|
47
64
|
cause: Schema.Defect,
|
|
48
65
|
note: Schema.optional(Schema.String),
|
|
49
66
|
}) {}
|
|
67
|
+
|
|
68
|
+
export class UnknownEventError extends Schema.TaggedError<UnknownEventError>()('LiveStore.UnknownEventError', {
|
|
69
|
+
event: LiveStoreEvent.AnyEncoded.pipe(Schema.pick('name', 'args', 'seqNum', 'clientId', 'sessionId')),
|
|
70
|
+
reason: Schema.Literal('event-definition-missing', 'materializer-missing'),
|
|
71
|
+
operation: Schema.String,
|
|
72
|
+
note: Schema.optional(Schema.String),
|
|
73
|
+
}) {}
|
|
74
|
+
|
|
75
|
+
export class MaterializeError extends Schema.TaggedError<MaterializeError>()('LiveStore.MaterializeError', {
|
|
76
|
+
cause: Schema.Union(MaterializerHashMismatchError, SqliteError, UnknownEventError),
|
|
77
|
+
note: Schema.optional(Schema.String),
|
|
78
|
+
}) {}
|
|
@@ -2,27 +2,39 @@ import { casesHandled, isNotUndefined, LS_DEV, shouldNeverHappen, TRACE_VERBOSE
|
|
|
2
2
|
import type { HttpClient, Runtime, Scope, Tracer } from '@livestore/utils/effect'
|
|
3
3
|
import {
|
|
4
4
|
BucketQueue,
|
|
5
|
+
Cause,
|
|
5
6
|
Deferred,
|
|
7
|
+
Duration,
|
|
6
8
|
Effect,
|
|
7
9
|
Exit,
|
|
8
10
|
FiberHandle,
|
|
11
|
+
Layer,
|
|
9
12
|
Option,
|
|
10
13
|
OtelTracer,
|
|
11
|
-
pipe,
|
|
12
14
|
Queue,
|
|
13
15
|
ReadonlyArray,
|
|
16
|
+
Schedule,
|
|
14
17
|
Stream,
|
|
15
18
|
Subscribable,
|
|
16
19
|
SubscriptionRef,
|
|
17
20
|
} from '@livestore/utils/effect'
|
|
18
21
|
import type * as otel from '@opentelemetry/api'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
import {
|
|
23
|
+
type IntentionalShutdownCause,
|
|
24
|
+
type MaterializeError,
|
|
25
|
+
type SqliteDb,
|
|
26
|
+
UnexpectedError,
|
|
27
|
+
} from '../adapter-types.ts'
|
|
22
28
|
import { makeMaterializerHash } from '../materializer-helper.ts'
|
|
23
29
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
|
24
|
-
import { EventSequenceNumber,
|
|
25
|
-
import {
|
|
30
|
+
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
|
|
31
|
+
import {
|
|
32
|
+
type InvalidPullError,
|
|
33
|
+
type InvalidPushError,
|
|
34
|
+
type IsOfflineError,
|
|
35
|
+
LeaderAheadError,
|
|
36
|
+
type SyncBackend,
|
|
37
|
+
} from '../sync/sync.ts'
|
|
26
38
|
import * as SyncState from '../sync/syncstate.ts'
|
|
27
39
|
import { sql } from '../util.ts'
|
|
28
40
|
import * as Eventlog from './eventlog.ts'
|
|
@@ -71,6 +83,7 @@ export const makeLeaderSyncProcessor = ({
|
|
|
71
83
|
initialBlockingSyncContext,
|
|
72
84
|
initialSyncState,
|
|
73
85
|
onError,
|
|
86
|
+
livePull,
|
|
74
87
|
params,
|
|
75
88
|
testing,
|
|
76
89
|
}: {
|
|
@@ -90,6 +103,11 @@ export const makeLeaderSyncProcessor = ({
|
|
|
90
103
|
*/
|
|
91
104
|
backendPushBatchSize?: number
|
|
92
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether the sync backend should reactively pull new events from the sync backend
|
|
108
|
+
* When `false`, the sync processor will only do an initial pull
|
|
109
|
+
*/
|
|
110
|
+
livePull: boolean
|
|
93
111
|
testing: {
|
|
94
112
|
delays?: {
|
|
95
113
|
localPushProcessing?: Effect.Effect<void>
|
|
@@ -103,10 +121,8 @@ export const makeLeaderSyncProcessor = ({
|
|
|
103
121
|
|
|
104
122
|
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
|
105
123
|
|
|
106
|
-
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
|
|
107
|
-
|
|
108
|
-
return eventDef.options.clientOnly
|
|
109
|
-
}
|
|
124
|
+
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
|
|
125
|
+
schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
|
|
110
126
|
|
|
111
127
|
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
|
112
128
|
|
|
@@ -180,14 +196,32 @@ export const makeLeaderSyncProcessor = ({
|
|
|
180
196
|
const syncState = yield* syncStateSref
|
|
181
197
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
182
198
|
|
|
183
|
-
const
|
|
199
|
+
const resolution = yield* resolveEventDef(schema, {
|
|
200
|
+
operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
|
|
201
|
+
event: {
|
|
202
|
+
name,
|
|
203
|
+
args,
|
|
204
|
+
clientId,
|
|
205
|
+
sessionId,
|
|
206
|
+
seqNum: syncState.localHead,
|
|
207
|
+
},
|
|
208
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
|
209
|
+
|
|
210
|
+
if (resolution._tag === 'unknown') {
|
|
211
|
+
// Ignore partial pushes for unrecognised events – they are still
|
|
212
|
+
// persisted server-side once a schema update ships.
|
|
213
|
+
return
|
|
214
|
+
}
|
|
184
215
|
|
|
185
216
|
const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
|
|
186
217
|
name,
|
|
187
218
|
args,
|
|
188
219
|
clientId,
|
|
189
220
|
sessionId,
|
|
190
|
-
...EventSequenceNumber.nextPair({
|
|
221
|
+
...EventSequenceNumber.nextPair({
|
|
222
|
+
seqNum: syncState.localHead,
|
|
223
|
+
isClient: resolution.eventDef.options.clientOnly,
|
|
224
|
+
}),
|
|
191
225
|
})
|
|
192
226
|
|
|
193
227
|
yield* push([eventEncoded])
|
|
@@ -215,8 +249,8 @@ export const makeLeaderSyncProcessor = ({
|
|
|
215
249
|
const globalPendingEvents = initialSyncState.pending
|
|
216
250
|
// Don't sync clientOnly events
|
|
217
251
|
.filter((eventEncoded) => {
|
|
218
|
-
const
|
|
219
|
-
return eventDef.options.clientOnly === false
|
|
252
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
253
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
220
254
|
})
|
|
221
255
|
|
|
222
256
|
if (globalPendingEvents.length > 0) {
|
|
@@ -224,12 +258,31 @@ export const makeLeaderSyncProcessor = ({
|
|
|
224
258
|
}
|
|
225
259
|
}
|
|
226
260
|
|
|
227
|
-
const
|
|
261
|
+
const maybeShutdownOnError = (
|
|
262
|
+
cause: Cause.Cause<
|
|
263
|
+
| UnexpectedError
|
|
264
|
+
| IntentionalShutdownCause
|
|
265
|
+
| IsOfflineError
|
|
266
|
+
| InvalidPushError
|
|
267
|
+
| InvalidPullError
|
|
268
|
+
| MaterializeError
|
|
269
|
+
>,
|
|
270
|
+
) =>
|
|
228
271
|
Effect.gen(function* () {
|
|
229
|
-
if (onError === '
|
|
230
|
-
|
|
231
|
-
|
|
272
|
+
if (onError === 'ignore') {
|
|
273
|
+
if (LS_DEV) {
|
|
274
|
+
yield* Effect.logDebug(
|
|
275
|
+
`Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
|
|
276
|
+
Cause.pretty(cause),
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
return
|
|
232
280
|
}
|
|
281
|
+
|
|
282
|
+
const errorToSend = Cause.isFailType(cause) ? cause.error : UnexpectedError.make({ cause })
|
|
283
|
+
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
|
284
|
+
|
|
285
|
+
return yield* Effect.die(cause)
|
|
233
286
|
})
|
|
234
287
|
|
|
235
288
|
yield* backgroundApplyLocalPushes({
|
|
@@ -246,20 +299,19 @@ export const makeLeaderSyncProcessor = ({
|
|
|
246
299
|
testing: {
|
|
247
300
|
delay: testing?.delays?.localPushProcessing,
|
|
248
301
|
},
|
|
249
|
-
}).pipe(Effect.
|
|
302
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError), Effect.forkScoped)
|
|
250
303
|
|
|
251
|
-
const backendPushingFiberHandle = yield* FiberHandle.make()
|
|
304
|
+
const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
|
|
252
305
|
const backendPushingEffect = backgroundBackendPushing({
|
|
253
306
|
syncBackendPushQueue,
|
|
254
307
|
otelSpan,
|
|
255
308
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
256
309
|
backendPushBatchSize,
|
|
257
|
-
}).pipe(Effect.
|
|
310
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError))
|
|
258
311
|
|
|
259
312
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
|
260
313
|
|
|
261
314
|
yield* backgroundBackendPulling({
|
|
262
|
-
initialBackendHead: initialSyncState.upstreamHead.global,
|
|
263
315
|
isClientEvent,
|
|
264
316
|
restartBackendPushing: (filteredRebasedPending) =>
|
|
265
317
|
Effect.gen(function* () {
|
|
@@ -276,13 +328,24 @@ export const makeLeaderSyncProcessor = ({
|
|
|
276
328
|
syncStateSref,
|
|
277
329
|
localPushesLatch,
|
|
278
330
|
pullLatch,
|
|
331
|
+
livePull,
|
|
279
332
|
dbState,
|
|
280
333
|
otelSpan,
|
|
281
334
|
initialBlockingSyncContext,
|
|
282
335
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
|
283
336
|
connectedClientSessionPullQueues,
|
|
284
337
|
advancePushHead,
|
|
285
|
-
}).pipe(
|
|
338
|
+
}).pipe(
|
|
339
|
+
Effect.retry({
|
|
340
|
+
// We want to retry pulling if we've lost connection to the sync backend
|
|
341
|
+
while: (cause) => cause._tag === 'IsOfflineError',
|
|
342
|
+
}),
|
|
343
|
+
Effect.catchAllCause(maybeShutdownOnError),
|
|
344
|
+
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
|
345
|
+
// This might be a bug in Effect. Only seems to happen in the browser.
|
|
346
|
+
Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
|
|
347
|
+
Effect.forkScoped,
|
|
348
|
+
)
|
|
286
349
|
|
|
287
350
|
return { initialLeaderHead: initialSyncState.localHead }
|
|
288
351
|
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
|
@@ -379,19 +442,44 @@ const backgroundApplyLocalPushes = ({
|
|
|
379
442
|
|
|
380
443
|
// Since the rebase generation might have changed since enqueuing, we need to filter out items with older generation
|
|
381
444
|
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
|
382
|
-
const [
|
|
445
|
+
const [droppedItems, filteredItems] = ReadonlyArray.partition(
|
|
383
446
|
batchItems,
|
|
384
|
-
|
|
385
|
-
ReadonlyArray.unzip,
|
|
447
|
+
([eventEncoded]) => eventEncoded.seqNum.rebaseGeneration >= currentRebaseGeneration,
|
|
386
448
|
)
|
|
387
449
|
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
450
|
+
if (droppedItems.length > 0) {
|
|
451
|
+
otelSpan?.addEvent(`push:drop-old-generation`, {
|
|
452
|
+
droppedCount: droppedItems.length,
|
|
453
|
+
currentRebaseGeneration,
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Dropped pushes may still have a deferred awaiting completion.
|
|
458
|
+
* Fail it so the caller learns the leader advanced and resubmits with the updated generation.
|
|
459
|
+
*/
|
|
460
|
+
yield* Effect.forEach(
|
|
461
|
+
droppedItems.filter(
|
|
462
|
+
(item): item is [LiveStoreEvent.EncodedWithMeta, Deferred.Deferred<void, LeaderAheadError>] =>
|
|
463
|
+
item[1] !== undefined,
|
|
464
|
+
),
|
|
465
|
+
([eventEncoded, deferred]) =>
|
|
466
|
+
Deferred.fail(
|
|
467
|
+
deferred,
|
|
468
|
+
LeaderAheadError.make({
|
|
469
|
+
minimumExpectedNum: syncState.localHead,
|
|
470
|
+
providedNum: eventEncoded.seqNum,
|
|
471
|
+
}),
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (filteredItems.length === 0) {
|
|
391
477
|
yield* pullLatch.open
|
|
392
478
|
continue
|
|
393
479
|
}
|
|
394
480
|
|
|
481
|
+
const [newEvents, deferreds] = ReadonlyArray.unzip(filteredItems)
|
|
482
|
+
|
|
395
483
|
const mergeResult = SyncState.merge({
|
|
396
484
|
syncState,
|
|
397
485
|
payload: { _tag: 'local-push', newEvents },
|
|
@@ -405,7 +493,7 @@ const backgroundApplyLocalPushes = ({
|
|
|
405
493
|
batchSize: newEvents.length,
|
|
406
494
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
407
495
|
})
|
|
408
|
-
return yield* new
|
|
496
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
|
409
497
|
}
|
|
410
498
|
case 'rebase': {
|
|
411
499
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
|
@@ -476,8 +564,8 @@ const backgroundApplyLocalPushes = ({
|
|
|
476
564
|
|
|
477
565
|
// Don't sync clientOnly events
|
|
478
566
|
const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
|
|
479
|
-
const
|
|
480
|
-
return eventDef.options.clientOnly === false
|
|
567
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
|
568
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
481
569
|
})
|
|
482
570
|
|
|
483
571
|
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
|
|
@@ -496,7 +584,7 @@ type MaterializeEventsBatch = (_: {
|
|
|
496
584
|
* Indexes are aligned with `batchItems`
|
|
497
585
|
*/
|
|
498
586
|
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
|
499
|
-
}) => Effect.Effect<void,
|
|
587
|
+
}) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
|
|
500
588
|
|
|
501
589
|
// TODO how to handle errors gracefully
|
|
502
590
|
const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
|
|
@@ -536,24 +624,22 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
|
|
|
536
624
|
attributes: { batchSize: batchItems.length },
|
|
537
625
|
}),
|
|
538
626
|
Effect.tapCauseLogPretty,
|
|
539
|
-
UnexpectedError.mapToUnexpectedError,
|
|
540
627
|
)
|
|
541
628
|
|
|
542
629
|
const backgroundBackendPulling = ({
|
|
543
|
-
initialBackendHead,
|
|
544
630
|
isClientEvent,
|
|
545
631
|
restartBackendPushing,
|
|
546
632
|
otelSpan,
|
|
547
633
|
dbState,
|
|
548
634
|
syncStateSref,
|
|
549
635
|
localPushesLatch,
|
|
636
|
+
livePull,
|
|
550
637
|
pullLatch,
|
|
551
638
|
devtoolsLatch,
|
|
552
639
|
initialBlockingSyncContext,
|
|
553
640
|
connectedClientSessionPullQueues,
|
|
554
641
|
advancePushHead,
|
|
555
642
|
}: {
|
|
556
|
-
initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
|
|
557
643
|
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
|
558
644
|
restartBackendPushing: (
|
|
559
645
|
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
|
@@ -563,6 +649,7 @@ const backgroundBackendPulling = ({
|
|
|
563
649
|
dbState: SqliteDb
|
|
564
650
|
localPushesLatch: Effect.Latch
|
|
565
651
|
pullLatch: Effect.Latch
|
|
652
|
+
livePull: boolean
|
|
566
653
|
devtoolsLatch: Effect.Latch | undefined
|
|
567
654
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
568
655
|
connectedClientSessionPullQueues: PullQueueSet
|
|
@@ -573,7 +660,7 @@ const backgroundBackendPulling = ({
|
|
|
573
660
|
|
|
574
661
|
if (syncBackend === undefined) return
|
|
575
662
|
|
|
576
|
-
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[],
|
|
663
|
+
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
|
|
577
664
|
Effect.gen(function* () {
|
|
578
665
|
if (newEvents.length === 0) return
|
|
579
666
|
|
|
@@ -605,7 +692,7 @@ const backgroundBackendPulling = ({
|
|
|
605
692
|
newEventsCount: newEvents.length,
|
|
606
693
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
|
607
694
|
})
|
|
608
|
-
return yield* new
|
|
695
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
|
609
696
|
}
|
|
610
697
|
|
|
611
698
|
const newBackendHead = newEvents.at(-1)!.seqNum
|
|
@@ -621,8 +708,8 @@ const backgroundBackendPulling = ({
|
|
|
621
708
|
})
|
|
622
709
|
|
|
623
710
|
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
624
|
-
const
|
|
625
|
-
return eventDef.options.clientOnly === false
|
|
711
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
|
712
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
626
713
|
})
|
|
627
714
|
yield* restartBackendPushing(globalRebasedPendingEvents)
|
|
628
715
|
|
|
@@ -644,6 +731,13 @@ const backgroundBackendPulling = ({
|
|
|
644
731
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
645
732
|
})
|
|
646
733
|
|
|
734
|
+
// Ensure push fiber is active after advance by restarting with current pending (non-client) events
|
|
735
|
+
const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
|
736
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
|
737
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
|
738
|
+
})
|
|
739
|
+
yield* restartBackendPushing(globalPendingEvents)
|
|
740
|
+
|
|
647
741
|
yield* connectedClientSessionPullQueues.offer({
|
|
648
742
|
payload: SyncState.payloadFromMergeResult(mergeResult),
|
|
649
743
|
leaderHead: mergeResult.newSyncState.localHead,
|
|
@@ -657,7 +751,7 @@ const backgroundBackendPulling = ({
|
|
|
657
751
|
EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
|
|
658
752
|
),
|
|
659
753
|
)
|
|
660
|
-
yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
|
|
754
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(UnexpectedError.mapToUnexpectedError)
|
|
661
755
|
}
|
|
662
756
|
}
|
|
663
757
|
|
|
@@ -671,18 +765,20 @@ const backgroundBackendPulling = ({
|
|
|
671
765
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
|
672
766
|
|
|
673
767
|
// Allow local pushes to be processed again
|
|
674
|
-
if (
|
|
768
|
+
if (pageInfo._tag === 'NoMore') {
|
|
675
769
|
yield* localPushesLatch.open
|
|
676
770
|
}
|
|
677
771
|
})
|
|
678
772
|
|
|
679
|
-
const
|
|
773
|
+
const syncState = yield* syncStateSref
|
|
774
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
|
775
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
|
|
680
776
|
|
|
681
777
|
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
|
682
778
|
|
|
683
|
-
yield* syncBackend.pull(cursorInfo).pipe(
|
|
779
|
+
yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
|
|
684
780
|
// TODO only take from queue while connected
|
|
685
|
-
Stream.tap(({ batch,
|
|
781
|
+
Stream.tap(({ batch, pageInfo }) =>
|
|
686
782
|
Effect.gen(function* () {
|
|
687
783
|
// yield* Effect.spanEvent('batch', {
|
|
688
784
|
// attributes: {
|
|
@@ -690,12 +786,10 @@ const backgroundBackendPulling = ({
|
|
|
690
786
|
// batch: TRACE_VERBOSE ? batch : undefined,
|
|
691
787
|
// },
|
|
692
788
|
// })
|
|
693
|
-
|
|
694
789
|
// NOTE we only want to take process events when the sync backend is connected
|
|
695
790
|
// (e.g. needed for simulating being offline)
|
|
696
791
|
// TODO remove when there's a better way to handle this in stream above
|
|
697
792
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
|
698
|
-
|
|
699
793
|
yield* onNewPullChunk(
|
|
700
794
|
batch.map((_) =>
|
|
701
795
|
LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
|
@@ -706,15 +800,17 @@ const backgroundBackendPulling = ({
|
|
|
706
800
|
materializerHashSession: Option.none(),
|
|
707
801
|
}),
|
|
708
802
|
),
|
|
709
|
-
|
|
803
|
+
pageInfo,
|
|
710
804
|
)
|
|
711
|
-
|
|
712
|
-
yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
|
|
805
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
|
713
806
|
}),
|
|
714
807
|
),
|
|
715
808
|
Stream.runDrain,
|
|
716
809
|
Effect.interruptible,
|
|
717
810
|
)
|
|
811
|
+
|
|
812
|
+
// Should only ever happen when livePull is false
|
|
813
|
+
yield* Effect.logDebug('backend-pulling finished', { livePull })
|
|
718
814
|
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
|
|
719
815
|
|
|
720
816
|
const backgroundBackendPushing = ({
|
|
@@ -748,17 +844,53 @@ const backgroundBackendPushing = ({
|
|
|
748
844
|
batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
|
|
749
845
|
})
|
|
750
846
|
|
|
751
|
-
//
|
|
752
|
-
|
|
847
|
+
// Push with declarative retry/backoff using Effect schedules
|
|
848
|
+
// - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
|
|
849
|
+
// - Delay clamped at 30s (continues retrying at 30s)
|
|
850
|
+
// - Resets automatically after successful push
|
|
851
|
+
// TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
|
|
852
|
+
|
|
853
|
+
// Only retry for transient UnexpectedError cases
|
|
854
|
+
const isRetryable = (err: InvalidPushError | IsOfflineError) =>
|
|
855
|
+
err._tag === 'InvalidPushError' && err.cause._tag === 'LiveStore.UnexpectedError'
|
|
856
|
+
|
|
857
|
+
// Input: InvalidPushError | IsOfflineError, Output: Duration
|
|
858
|
+
const retrySchedule: Schedule.Schedule<Duration.DurationInput, InvalidPushError | IsOfflineError> =
|
|
859
|
+
Schedule.exponential(Duration.seconds(1)).pipe(
|
|
860
|
+
Schedule.andThenEither(Schedule.spaced(Duration.seconds(30))), // clamp at 30 second intervals
|
|
861
|
+
Schedule.compose(Schedule.elapsed),
|
|
862
|
+
Schedule.whileInput(isRetryable),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
yield* Effect.gen(function* () {
|
|
866
|
+
const iteration = yield* Schedule.CurrentIterationMetadata
|
|
753
867
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
868
|
+
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
|
|
869
|
+
|
|
870
|
+
const retries = iteration.recurrence
|
|
871
|
+
if (retries > 0 && pushResult._tag === 'Right') {
|
|
872
|
+
otelSpan?.addEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
|
|
757
873
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
874
|
+
|
|
875
|
+
if (pushResult._tag === 'Left') {
|
|
876
|
+
otelSpan?.addEvent('backend-push-error', {
|
|
877
|
+
error: pushResult.left.toString(),
|
|
878
|
+
retries,
|
|
879
|
+
batchSize: queueItems.length,
|
|
880
|
+
})
|
|
881
|
+
const error = pushResult.left
|
|
882
|
+
if (
|
|
883
|
+
error._tag === 'IsOfflineError' ||
|
|
884
|
+
(error._tag === 'InvalidPushError' && error.cause._tag === 'ServerAheadError')
|
|
885
|
+
) {
|
|
886
|
+
// It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
|
|
887
|
+
yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
|
|
888
|
+
return yield* Effect.never
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return yield* error
|
|
892
|
+
}
|
|
893
|
+
}).pipe(Effect.retry(retrySchedule))
|
|
762
894
|
}
|
|
763
895
|
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
|
|
764
896
|
|
|
@@ -890,6 +1022,11 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
|
890
1022
|
}
|
|
891
1023
|
})
|
|
892
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Validate a client-provided batch before it is admitted to the leader queue.
|
|
1027
|
+
* Ensures the numbers form a strictly increasing chain and that the first
|
|
1028
|
+
* event sits ahead of the current push head.
|
|
1029
|
+
*/
|
|
893
1030
|
const validatePushBatch = (
|
|
894
1031
|
batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
|
895
1032
|
pushHead: EventSequenceNumber.EventSequenceNumber,
|
|
@@ -899,12 +1036,16 @@ const validatePushBatch = (
|
|
|
899
1036
|
return
|
|
900
1037
|
}
|
|
901
1038
|
|
|
902
|
-
//
|
|
1039
|
+
// Example: session A already enqueued e1…e6 while session B (same client, different
|
|
1040
|
+
// session) still believes the head is e1 and submits [e2, e7, e8]. The numbers look
|
|
1041
|
+
// monotonic from B’s perspective, but we must reject and force B to rebase locally
|
|
1042
|
+
// so the leader never regresses.
|
|
903
1043
|
for (let i = 1; i < batch.length; i++) {
|
|
904
1044
|
if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1045
|
+
return yield* LeaderAheadError.make({
|
|
1046
|
+
minimumExpectedNum: batch[i - 1]!.seqNum,
|
|
1047
|
+
providedNum: batch[i]!.seqNum,
|
|
1048
|
+
})
|
|
908
1049
|
}
|
|
909
1050
|
}
|
|
910
1051
|
|
|
@@ -123,6 +123,14 @@ export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.
|
|
|
123
123
|
export const updateBackendHead = (dbEventlog: SqliteDb, head: EventSequenceNumber.EventSequenceNumber) =>
|
|
124
124
|
dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
|
|
125
125
|
|
|
126
|
+
export const getBackendIdFromDb = (dbEventlog: SqliteDb): Option.Option<string> =>
|
|
127
|
+
Option.fromNullable(
|
|
128
|
+
dbEventlog.select<{ backendId: string | null }>(sql`select backendId from ${SYNC_STATUS_TABLE}`)[0]?.backendId,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
export const updateBackendId = (dbEventlog: SqliteDb, backendId: string) =>
|
|
132
|
+
dbEventlog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET backendId = '${backendId}'`)
|
|
133
|
+
|
|
126
134
|
export const insertIntoEventlog = (
|
|
127
135
|
eventEncoded: LiveStoreEvent.EncodedWithMeta,
|
|
128
136
|
dbEventlog: SqliteDb,
|
|
@@ -141,7 +149,7 @@ export const insertIntoEventlog = (
|
|
|
141
149
|
|
|
142
150
|
if (parentEventExists === false) {
|
|
143
151
|
shouldNeverHappen(
|
|
144
|
-
`Parent
|
|
152
|
+
`Parent event ${eventEncoded.parentSeqNum.global},${eventEncoded.parentSeqNum.client} does not exist in eventlog`,
|
|
145
153
|
)
|
|
146
154
|
}
|
|
147
155
|
}
|
|
@@ -213,11 +221,7 @@ export const getSyncBackendCursorInfo = ({
|
|
|
213
221
|
).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
|
214
222
|
|
|
215
223
|
return Option.some({
|
|
216
|
-
|
|
217
|
-
global: remoteHead,
|
|
218
|
-
client: EventSequenceNumber.clientDefault,
|
|
219
|
-
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
|
220
|
-
},
|
|
224
|
+
eventSequenceNumber: remoteHead,
|
|
221
225
|
metadata: syncMetadataOption,
|
|
222
226
|
}) satisfies InitialSyncInfo
|
|
223
227
|
}).pipe(Effect.withSpan('@livestore/common:eventlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
|
|
@@ -262,7 +262,7 @@ const listenToDevtools = ({
|
|
|
262
262
|
|
|
263
263
|
if (syncBackend !== undefined) {
|
|
264
264
|
// TODO consider piggybacking on the existing leader-thread sync-pulling
|
|
265
|
-
yield* syncBackend.pull(Option.none()).pipe(
|
|
265
|
+
yield* syncBackend.pull(Option.none(), { live: true }).pipe(
|
|
266
266
|
Stream.map((_) => _.batch),
|
|
267
267
|
Stream.flattenIterables,
|
|
268
268
|
Stream.tap(({ eventEncoded, metadata }) =>
|
|
@@ -319,7 +319,11 @@ const listenToDevtools = ({
|
|
|
319
319
|
Stream.tap(([isConnected, { latchClosed }]) =>
|
|
320
320
|
sendMessage(
|
|
321
321
|
Devtools.Leader.NetworkStatusRes.make({
|
|
322
|
-
networkStatus: {
|
|
322
|
+
networkStatus: {
|
|
323
|
+
isConnected,
|
|
324
|
+
timestampMs: Date.now(),
|
|
325
|
+
devtools: { latchClosed },
|
|
326
|
+
},
|
|
323
327
|
subscriptionId,
|
|
324
328
|
...reqPayload,
|
|
325
329
|
requestId: nanoid(10),
|