@livestore/common 0.4.0-dev.1 → 0.4.0-dev.10
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 +7 -2
- 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 +122 -49
- 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/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 +122 -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 +51 -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 +35 -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/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 +7 -2
- 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 +179 -57
- 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/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 +128 -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 +64 -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 +55 -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/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,27 +2,40 @@ 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
14
|
pipe,
|
12
15
|
Queue,
|
13
16
|
ReadonlyArray,
|
17
|
+
Schedule,
|
14
18
|
Stream,
|
15
19
|
Subscribable,
|
16
20
|
SubscriptionRef,
|
17
21
|
} from '@livestore/utils/effect'
|
18
22
|
import type * as otel from '@opentelemetry/api'
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
import {
|
24
|
+
type IntentionalShutdownCause,
|
25
|
+
type MaterializeError,
|
26
|
+
type SqliteDb,
|
27
|
+
UnexpectedError,
|
28
|
+
} from '../adapter-types.ts'
|
22
29
|
import { makeMaterializerHash } from '../materializer-helper.ts'
|
23
30
|
import type { LiveStoreSchema } from '../schema/mod.ts'
|
24
|
-
import { EventSequenceNumber,
|
25
|
-
import {
|
31
|
+
import { EventSequenceNumber, LiveStoreEvent, resolveEventDef, SystemTables } from '../schema/mod.ts'
|
32
|
+
import {
|
33
|
+
type InvalidPullError,
|
34
|
+
type InvalidPushError,
|
35
|
+
type IsOfflineError,
|
36
|
+
LeaderAheadError,
|
37
|
+
type SyncBackend,
|
38
|
+
} from '../sync/sync.ts'
|
26
39
|
import * as SyncState from '../sync/syncstate.ts'
|
27
40
|
import { sql } from '../util.ts'
|
28
41
|
import * as Eventlog from './eventlog.ts'
|
@@ -71,6 +84,7 @@ export const makeLeaderSyncProcessor = ({
|
|
71
84
|
initialBlockingSyncContext,
|
72
85
|
initialSyncState,
|
73
86
|
onError,
|
87
|
+
livePull,
|
74
88
|
params,
|
75
89
|
testing,
|
76
90
|
}: {
|
@@ -90,6 +104,11 @@ export const makeLeaderSyncProcessor = ({
|
|
90
104
|
*/
|
91
105
|
backendPushBatchSize?: number
|
92
106
|
}
|
107
|
+
/**
|
108
|
+
* Whether the sync backend should reactively pull new events from the sync backend
|
109
|
+
* When `false`, the sync processor will only do an initial pull
|
110
|
+
*/
|
111
|
+
livePull: boolean
|
93
112
|
testing: {
|
94
113
|
delays?: {
|
95
114
|
localPushProcessing?: Effect.Effect<void>
|
@@ -103,10 +122,8 @@ export const makeLeaderSyncProcessor = ({
|
|
103
122
|
|
104
123
|
const syncStateSref = yield* SubscriptionRef.make<SyncState.SyncState | undefined>(undefined)
|
105
124
|
|
106
|
-
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
|
107
|
-
|
108
|
-
return eventDef.options.clientOnly
|
109
|
-
}
|
125
|
+
const isClientEvent = (eventEncoded: LiveStoreEvent.EncodedWithMeta) =>
|
126
|
+
schema.eventsDefsMap.get(eventEncoded.name)?.options.clientOnly ?? false
|
110
127
|
|
111
128
|
const connectedClientSessionPullQueues = yield* makePullQueueSet
|
112
129
|
|
@@ -180,14 +197,32 @@ export const makeLeaderSyncProcessor = ({
|
|
180
197
|
const syncState = yield* syncStateSref
|
181
198
|
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
182
199
|
|
183
|
-
const
|
200
|
+
const resolution = yield* resolveEventDef(schema, {
|
201
|
+
operation: '@livestore/common:LeaderSyncProcessor:pushPartial',
|
202
|
+
event: {
|
203
|
+
name,
|
204
|
+
args,
|
205
|
+
clientId,
|
206
|
+
sessionId,
|
207
|
+
seqNum: syncState.localHead,
|
208
|
+
},
|
209
|
+
}).pipe(UnexpectedError.mapToUnexpectedError)
|
210
|
+
|
211
|
+
if (resolution._tag === 'unknown') {
|
212
|
+
// Ignore partial pushes for unrecognised events – they are still
|
213
|
+
// persisted server-side once a schema update ships.
|
214
|
+
return
|
215
|
+
}
|
184
216
|
|
185
217
|
const eventEncoded = new LiveStoreEvent.EncodedWithMeta({
|
186
218
|
name,
|
187
219
|
args,
|
188
220
|
clientId,
|
189
221
|
sessionId,
|
190
|
-
...EventSequenceNumber.nextPair({
|
222
|
+
...EventSequenceNumber.nextPair({
|
223
|
+
seqNum: syncState.localHead,
|
224
|
+
isClient: resolution.eventDef.options.clientOnly,
|
225
|
+
}),
|
191
226
|
})
|
192
227
|
|
193
228
|
yield* push([eventEncoded])
|
@@ -215,8 +250,8 @@ export const makeLeaderSyncProcessor = ({
|
|
215
250
|
const globalPendingEvents = initialSyncState.pending
|
216
251
|
// Don't sync clientOnly events
|
217
252
|
.filter((eventEncoded) => {
|
218
|
-
const
|
219
|
-
return eventDef.options.clientOnly === false
|
253
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
254
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
220
255
|
})
|
221
256
|
|
222
257
|
if (globalPendingEvents.length > 0) {
|
@@ -224,12 +259,31 @@ export const makeLeaderSyncProcessor = ({
|
|
224
259
|
}
|
225
260
|
}
|
226
261
|
|
227
|
-
const
|
262
|
+
const maybeShutdownOnError = (
|
263
|
+
cause: Cause.Cause<
|
264
|
+
| UnexpectedError
|
265
|
+
| IntentionalShutdownCause
|
266
|
+
| IsOfflineError
|
267
|
+
| InvalidPushError
|
268
|
+
| InvalidPullError
|
269
|
+
| MaterializeError
|
270
|
+
>,
|
271
|
+
) =>
|
228
272
|
Effect.gen(function* () {
|
229
|
-
if (onError === '
|
230
|
-
|
231
|
-
|
273
|
+
if (onError === 'ignore') {
|
274
|
+
if (LS_DEV) {
|
275
|
+
yield* Effect.logDebug(
|
276
|
+
`Ignoring sync error (${cause._tag === 'Fail' ? cause.error._tag : cause._tag})`,
|
277
|
+
Cause.pretty(cause),
|
278
|
+
)
|
279
|
+
}
|
280
|
+
return
|
232
281
|
}
|
282
|
+
|
283
|
+
const errorToSend = Cause.isFailType(cause) ? cause.error : UnexpectedError.make({ cause })
|
284
|
+
yield* shutdownChannel.send(errorToSend).pipe(Effect.orDie)
|
285
|
+
|
286
|
+
return yield* Effect.die(cause)
|
233
287
|
})
|
234
288
|
|
235
289
|
yield* backgroundApplyLocalPushes({
|
@@ -246,20 +300,19 @@ export const makeLeaderSyncProcessor = ({
|
|
246
300
|
testing: {
|
247
301
|
delay: testing?.delays?.localPushProcessing,
|
248
302
|
},
|
249
|
-
}).pipe(Effect.
|
303
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError), Effect.forkScoped)
|
250
304
|
|
251
|
-
const backendPushingFiberHandle = yield* FiberHandle.make()
|
305
|
+
const backendPushingFiberHandle = yield* FiberHandle.make<void, never>()
|
252
306
|
const backendPushingEffect = backgroundBackendPushing({
|
253
307
|
syncBackendPushQueue,
|
254
308
|
otelSpan,
|
255
309
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
256
310
|
backendPushBatchSize,
|
257
|
-
}).pipe(Effect.
|
311
|
+
}).pipe(Effect.catchAllCause(maybeShutdownOnError))
|
258
312
|
|
259
313
|
yield* FiberHandle.run(backendPushingFiberHandle, backendPushingEffect)
|
260
314
|
|
261
315
|
yield* backgroundBackendPulling({
|
262
|
-
initialBackendHead: initialSyncState.upstreamHead.global,
|
263
316
|
isClientEvent,
|
264
317
|
restartBackendPushing: (filteredRebasedPending) =>
|
265
318
|
Effect.gen(function* () {
|
@@ -276,13 +329,24 @@ export const makeLeaderSyncProcessor = ({
|
|
276
329
|
syncStateSref,
|
277
330
|
localPushesLatch,
|
278
331
|
pullLatch,
|
332
|
+
livePull,
|
279
333
|
dbState,
|
280
334
|
otelSpan,
|
281
335
|
initialBlockingSyncContext,
|
282
336
|
devtoolsLatch: ctxRef.current?.devtoolsLatch,
|
283
337
|
connectedClientSessionPullQueues,
|
284
338
|
advancePushHead,
|
285
|
-
}).pipe(
|
339
|
+
}).pipe(
|
340
|
+
Effect.retry({
|
341
|
+
// We want to retry pulling if we've lost connection to the sync backend
|
342
|
+
while: (cause) => cause._tag === 'IsOfflineError',
|
343
|
+
}),
|
344
|
+
Effect.catchAllCause(maybeShutdownOnError),
|
345
|
+
// Needed to avoid `Fiber terminated with an unhandled error` logs which seem to happen because of the `Effect.retry` above.
|
346
|
+
// This might be a bug in Effect. Only seems to happen in the browser.
|
347
|
+
Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
|
348
|
+
Effect.forkScoped,
|
349
|
+
)
|
286
350
|
|
287
351
|
return { initialLeaderHead: initialSyncState.localHead }
|
288
352
|
}).pipe(Effect.withSpanScoped('@livestore/common:LeaderSyncProcessor:boot'))
|
@@ -381,7 +445,12 @@ const backgroundApplyLocalPushes = ({
|
|
381
445
|
// It's important that we filter after we got localPushesLatch, otherwise we might filter with the old generation
|
382
446
|
const [newEvents, deferreds] = pipe(
|
383
447
|
batchItems,
|
384
|
-
ReadonlyArray.filter(
|
448
|
+
ReadonlyArray.filter(
|
449
|
+
([eventEncoded]) =>
|
450
|
+
// Keep events that match the current generation or newer. Older generations will
|
451
|
+
// be rejected below when their sequence numbers no longer advance the local head.
|
452
|
+
eventEncoded.seqNum.rebaseGeneration >= currentRebaseGeneration,
|
453
|
+
),
|
385
454
|
ReadonlyArray.unzip,
|
386
455
|
)
|
387
456
|
|
@@ -405,7 +474,7 @@ const backgroundApplyLocalPushes = ({
|
|
405
474
|
batchSize: newEvents.length,
|
406
475
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
407
476
|
})
|
408
|
-
return yield* new
|
477
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
409
478
|
}
|
410
479
|
case 'rebase': {
|
411
480
|
return shouldNeverHappen('The leader thread should never have to rebase due to a local push')
|
@@ -476,8 +545,8 @@ const backgroundApplyLocalPushes = ({
|
|
476
545
|
|
477
546
|
// Don't sync clientOnly events
|
478
547
|
const filteredBatch = mergeResult.newEvents.filter((eventEncoded) => {
|
479
|
-
const
|
480
|
-
return eventDef.options.clientOnly === false
|
548
|
+
const eventDef = schema.eventsDefsMap.get(eventEncoded.name)
|
549
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
481
550
|
})
|
482
551
|
|
483
552
|
yield* BucketQueue.offerAll(syncBackendPushQueue, filteredBatch)
|
@@ -496,7 +565,7 @@ type MaterializeEventsBatch = (_: {
|
|
496
565
|
* Indexes are aligned with `batchItems`
|
497
566
|
*/
|
498
567
|
deferreds: ReadonlyArray<Deferred.Deferred<void, LeaderAheadError> | undefined> | undefined
|
499
|
-
}) => Effect.Effect<void,
|
568
|
+
}) => Effect.Effect<void, MaterializeError, LeaderThreadCtx>
|
500
569
|
|
501
570
|
// TODO how to handle errors gracefully
|
502
571
|
const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds }) =>
|
@@ -536,24 +605,22 @@ const materializeEventsBatch: MaterializeEventsBatch = ({ batchItems, deferreds
|
|
536
605
|
attributes: { batchSize: batchItems.length },
|
537
606
|
}),
|
538
607
|
Effect.tapCauseLogPretty,
|
539
|
-
UnexpectedError.mapToUnexpectedError,
|
540
608
|
)
|
541
609
|
|
542
610
|
const backgroundBackendPulling = ({
|
543
|
-
initialBackendHead,
|
544
611
|
isClientEvent,
|
545
612
|
restartBackendPushing,
|
546
613
|
otelSpan,
|
547
614
|
dbState,
|
548
615
|
syncStateSref,
|
549
616
|
localPushesLatch,
|
617
|
+
livePull,
|
550
618
|
pullLatch,
|
551
619
|
devtoolsLatch,
|
552
620
|
initialBlockingSyncContext,
|
553
621
|
connectedClientSessionPullQueues,
|
554
622
|
advancePushHead,
|
555
623
|
}: {
|
556
|
-
initialBackendHead: EventSequenceNumber.GlobalEventSequenceNumber
|
557
624
|
isClientEvent: (eventEncoded: LiveStoreEvent.EncodedWithMeta) => boolean
|
558
625
|
restartBackendPushing: (
|
559
626
|
filteredRebasedPending: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
@@ -563,6 +630,7 @@ const backgroundBackendPulling = ({
|
|
563
630
|
dbState: SqliteDb
|
564
631
|
localPushesLatch: Effect.Latch
|
565
632
|
pullLatch: Effect.Latch
|
633
|
+
livePull: boolean
|
566
634
|
devtoolsLatch: Effect.Latch | undefined
|
567
635
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
568
636
|
connectedClientSessionPullQueues: PullQueueSet
|
@@ -573,7 +641,7 @@ const backgroundBackendPulling = ({
|
|
573
641
|
|
574
642
|
if (syncBackend === undefined) return
|
575
643
|
|
576
|
-
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[],
|
644
|
+
const onNewPullChunk = (newEvents: LiveStoreEvent.EncodedWithMeta[], pageInfo: SyncBackend.PullResPageInfo) =>
|
577
645
|
Effect.gen(function* () {
|
578
646
|
if (newEvents.length === 0) return
|
579
647
|
|
@@ -605,7 +673,7 @@ const backgroundBackendPulling = ({
|
|
605
673
|
newEventsCount: newEvents.length,
|
606
674
|
newEvents: TRACE_VERBOSE ? JSON.stringify(newEvents) : undefined,
|
607
675
|
})
|
608
|
-
return yield* new
|
676
|
+
return yield* new UnexpectedError({ cause: mergeResult.message })
|
609
677
|
}
|
610
678
|
|
611
679
|
const newBackendHead = newEvents.at(-1)!.seqNum
|
@@ -621,8 +689,8 @@ const backgroundBackendPulling = ({
|
|
621
689
|
})
|
622
690
|
|
623
691
|
const globalRebasedPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
624
|
-
const
|
625
|
-
return eventDef.options.clientOnly === false
|
692
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
693
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
626
694
|
})
|
627
695
|
yield* restartBackendPushing(globalRebasedPendingEvents)
|
628
696
|
|
@@ -644,6 +712,13 @@ const backgroundBackendPulling = ({
|
|
644
712
|
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
645
713
|
})
|
646
714
|
|
715
|
+
// Ensure push fiber is active after advance by restarting with current pending (non-client) events
|
716
|
+
const globalPendingEvents = mergeResult.newSyncState.pending.filter((event) => {
|
717
|
+
const eventDef = schema.eventsDefsMap.get(event.name)
|
718
|
+
return eventDef === undefined ? true : eventDef.options.clientOnly === false
|
719
|
+
})
|
720
|
+
yield* restartBackendPushing(globalPendingEvents)
|
721
|
+
|
647
722
|
yield* connectedClientSessionPullQueues.offer({
|
648
723
|
payload: SyncState.payloadFromMergeResult(mergeResult),
|
649
724
|
leaderHead: mergeResult.newSyncState.localHead,
|
@@ -657,7 +732,7 @@ const backgroundBackendPulling = ({
|
|
657
732
|
EventSequenceNumber.isEqual(event.seqNum, confirmedEvent.seqNum),
|
658
733
|
),
|
659
734
|
)
|
660
|
-
yield* Eventlog.updateSyncMetadata(confirmedNewEvents)
|
735
|
+
yield* Eventlog.updateSyncMetadata(confirmedNewEvents).pipe(UnexpectedError.mapToUnexpectedError)
|
661
736
|
}
|
662
737
|
}
|
663
738
|
|
@@ -671,18 +746,20 @@ const backgroundBackendPulling = ({
|
|
671
746
|
yield* SubscriptionRef.set(syncStateSref, mergeResult.newSyncState)
|
672
747
|
|
673
748
|
// Allow local pushes to be processed again
|
674
|
-
if (
|
749
|
+
if (pageInfo._tag === 'NoMore') {
|
675
750
|
yield* localPushesLatch.open
|
676
751
|
}
|
677
752
|
})
|
678
753
|
|
679
|
-
const
|
754
|
+
const syncState = yield* syncStateSref
|
755
|
+
if (syncState === undefined) return shouldNeverHappen('Not initialized')
|
756
|
+
const cursorInfo = yield* Eventlog.getSyncBackendCursorInfo({ remoteHead: syncState.upstreamHead.global })
|
680
757
|
|
681
758
|
const hashMaterializerResult = makeMaterializerHash({ schema, dbState })
|
682
759
|
|
683
|
-
yield* syncBackend.pull(cursorInfo).pipe(
|
760
|
+
yield* syncBackend.pull(cursorInfo, { live: livePull }).pipe(
|
684
761
|
// TODO only take from queue while connected
|
685
|
-
Stream.tap(({ batch,
|
762
|
+
Stream.tap(({ batch, pageInfo }) =>
|
686
763
|
Effect.gen(function* () {
|
687
764
|
// yield* Effect.spanEvent('batch', {
|
688
765
|
// attributes: {
|
@@ -690,12 +767,10 @@ const backgroundBackendPulling = ({
|
|
690
767
|
// batch: TRACE_VERBOSE ? batch : undefined,
|
691
768
|
// },
|
692
769
|
// })
|
693
|
-
|
694
770
|
// NOTE we only want to take process events when the sync backend is connected
|
695
771
|
// (e.g. needed for simulating being offline)
|
696
772
|
// TODO remove when there's a better way to handle this in stream above
|
697
773
|
yield* SubscriptionRef.waitUntil(syncBackend.isConnected, (isConnected) => isConnected === true)
|
698
|
-
|
699
774
|
yield* onNewPullChunk(
|
700
775
|
batch.map((_) =>
|
701
776
|
LiveStoreEvent.EncodedWithMeta.fromGlobal(_.eventEncoded, {
|
@@ -706,15 +781,17 @@ const backgroundBackendPulling = ({
|
|
706
781
|
materializerHashSession: Option.none(),
|
707
782
|
}),
|
708
783
|
),
|
709
|
-
|
784
|
+
pageInfo,
|
710
785
|
)
|
711
|
-
|
712
|
-
yield* initialBlockingSyncContext.update({ processed: batch.length, remaining })
|
786
|
+
yield* initialBlockingSyncContext.update({ processed: batch.length, pageInfo })
|
713
787
|
}),
|
714
788
|
),
|
715
789
|
Stream.runDrain,
|
716
790
|
Effect.interruptible,
|
717
791
|
)
|
792
|
+
|
793
|
+
// Should only ever happen when livePull is false
|
794
|
+
yield* Effect.logDebug('backend-pulling finished', { livePull })
|
718
795
|
}).pipe(Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pulling'))
|
719
796
|
|
720
797
|
const backgroundBackendPushing = ({
|
@@ -748,17 +825,53 @@ const backgroundBackendPushing = ({
|
|
748
825
|
batch: TRACE_VERBOSE ? JSON.stringify(queueItems) : undefined,
|
749
826
|
})
|
750
827
|
|
751
|
-
//
|
752
|
-
|
828
|
+
// Push with declarative retry/backoff using Effect schedules
|
829
|
+
// - Exponential backoff starting at 1s and doubling (1s, 2s, 4s, 8s, 16s, 30s ...)
|
830
|
+
// - Delay clamped at 30s (continues retrying at 30s)
|
831
|
+
// - Resets automatically after successful push
|
832
|
+
// TODO(metrics): expose counters/gauges for retry attempts and queue health via devtools/metrics
|
833
|
+
|
834
|
+
// Only retry for transient UnexpectedError cases
|
835
|
+
const isRetryable = (err: InvalidPushError | IsOfflineError) =>
|
836
|
+
err._tag === 'InvalidPushError' && err.cause._tag === 'LiveStore.UnexpectedError'
|
837
|
+
|
838
|
+
// Input: InvalidPushError | IsOfflineError, Output: Duration
|
839
|
+
const retrySchedule: Schedule.Schedule<Duration.DurationInput, InvalidPushError | IsOfflineError> =
|
840
|
+
Schedule.exponential(Duration.seconds(1)).pipe(
|
841
|
+
Schedule.andThenEither(Schedule.spaced(Duration.seconds(30))), // clamp at 30 second intervals
|
842
|
+
Schedule.compose(Schedule.elapsed),
|
843
|
+
Schedule.whileInput(isRetryable),
|
844
|
+
)
|
753
845
|
|
754
|
-
|
755
|
-
|
756
|
-
|
846
|
+
yield* Effect.gen(function* () {
|
847
|
+
const iteration = yield* Schedule.CurrentIterationMetadata
|
848
|
+
|
849
|
+
const pushResult = yield* syncBackend.push(queueItems.map((_) => _.toGlobal())).pipe(Effect.either)
|
850
|
+
|
851
|
+
const retries = iteration.recurrence
|
852
|
+
if (retries > 0 && pushResult._tag === 'Right') {
|
853
|
+
otelSpan?.addEvent('backend-push-retry-success', { retries, batchSize: queueItems.length })
|
757
854
|
}
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
855
|
+
|
856
|
+
if (pushResult._tag === 'Left') {
|
857
|
+
otelSpan?.addEvent('backend-push-error', {
|
858
|
+
error: pushResult.left.toString(),
|
859
|
+
retries,
|
860
|
+
batchSize: queueItems.length,
|
861
|
+
})
|
862
|
+
const error = pushResult.left
|
863
|
+
if (
|
864
|
+
error._tag === 'IsOfflineError' ||
|
865
|
+
(error._tag === 'InvalidPushError' && error.cause._tag === 'ServerAheadError')
|
866
|
+
) {
|
867
|
+
// It's a core part of the sync protocol that the sync backend will emit a new pull chunk alongside the ServerAheadError
|
868
|
+
yield* Effect.logDebug('handled backend-push-error (waiting for interupt caused by pull)', { error })
|
869
|
+
return yield* Effect.never
|
870
|
+
}
|
871
|
+
|
872
|
+
return yield* error
|
873
|
+
}
|
874
|
+
}).pipe(Effect.retry(retrySchedule))
|
762
875
|
}
|
763
876
|
}).pipe(Effect.interruptible, Effect.withSpan('@livestore/common:LeaderSyncProcessor:backend-pushing'))
|
764
877
|
|
@@ -890,6 +1003,11 @@ const makePullQueueSet = Effect.gen(function* () {
|
|
890
1003
|
}
|
891
1004
|
})
|
892
1005
|
|
1006
|
+
/**
|
1007
|
+
* Validate a client-provided batch before it is admitted to the leader queue.
|
1008
|
+
* Ensures the numbers form a strictly increasing chain and that the first
|
1009
|
+
* event sits ahead of the current push head.
|
1010
|
+
*/
|
893
1011
|
const validatePushBatch = (
|
894
1012
|
batch: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>,
|
895
1013
|
pushHead: EventSequenceNumber.EventSequenceNumber,
|
@@ -899,12 +1017,16 @@ const validatePushBatch = (
|
|
899
1017
|
return
|
900
1018
|
}
|
901
1019
|
|
902
|
-
//
|
1020
|
+
// Example: session A already enqueued e1…e6 while session B (same client, different
|
1021
|
+
// session) still believes the head is e1 and submits [e2, e7, e8]. The numbers look
|
1022
|
+
// monotonic from B’s perspective, but we must reject and force B to rebase locally
|
1023
|
+
// so the leader never regresses.
|
903
1024
|
for (let i = 1; i < batch.length; i++) {
|
904
1025
|
if (EventSequenceNumber.isGreaterThanOrEqual(batch[i - 1]!.seqNum, batch[i]!.seqNum)) {
|
905
|
-
|
906
|
-
|
907
|
-
|
1026
|
+
return yield* LeaderAheadError.make({
|
1027
|
+
minimumExpectedNum: batch[i - 1]!.seqNum,
|
1028
|
+
providedNum: batch[i]!.seqNum,
|
1029
|
+
})
|
908
1030
|
}
|
909
1031
|
}
|
910
1032
|
|
@@ -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),
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { Effect, Stream, SubscriptionRef } from '@livestore/utils/effect'
|
2
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
3
|
+
|
4
|
+
import { makeMockSyncBackend } from '../sync/mock-sync-backend.ts'
|
5
|
+
import type { SyncBackend } from '../sync/sync.ts'
|
6
|
+
import { makeNetworkStatusSubscribable } from './make-leader-thread-layer.ts'
|
7
|
+
import type { DevtoolsContext } from './types.ts'
|
8
|
+
|
9
|
+
Vitest.describe('makeNetworkStatusSubscribable', () => {
|
10
|
+
Vitest.scopedLive('tracks sync backend connectivity and devtools latch state', () =>
|
11
|
+
Effect.gen(function* () {
|
12
|
+
const mockBackend = yield* makeMockSyncBackend({ startConnected: false })
|
13
|
+
const syncBackend = yield* mockBackend.makeSyncBackend
|
14
|
+
const latchStateRef = yield* SubscriptionRef.make<{ latchClosed: boolean }>({ latchClosed: false })
|
15
|
+
|
16
|
+
const devtoolsContext: DevtoolsContext = {
|
17
|
+
enabled: true,
|
18
|
+
syncBackendLatch: yield* Effect.makeLatch(true),
|
19
|
+
syncBackendLatchState: latchStateRef,
|
20
|
+
}
|
21
|
+
|
22
|
+
const networkStatus = yield* makeNetworkStatusSubscribable({ syncBackend, devtoolsContext })
|
23
|
+
|
24
|
+
const initial = yield* networkStatus
|
25
|
+
Vitest.expect(initial.isConnected).toBe(false)
|
26
|
+
Vitest.expect(initial.devtools.latchClosed).toBe(false)
|
27
|
+
|
28
|
+
const waitFor = (predicate: (status: SyncBackend.NetworkStatus) => boolean) =>
|
29
|
+
networkStatus.changes.pipe(Stream.filter(predicate), Stream.runHead, Effect.flatten)
|
30
|
+
|
31
|
+
const onlineFiber = yield* waitFor((status) => status.isConnected).pipe(Effect.forkScoped)
|
32
|
+
yield* mockBackend.connect
|
33
|
+
const online = yield* onlineFiber
|
34
|
+
Vitest.expect(online.isConnected).toBe(true)
|
35
|
+
Vitest.expect(online.timestampMs).toBeGreaterThan(initial.timestampMs)
|
36
|
+
|
37
|
+
const latchedFiber = yield* waitFor((status) => status.devtools.latchClosed).pipe(Effect.forkScoped)
|
38
|
+
yield* SubscriptionRef.set(latchStateRef, { latchClosed: true })
|
39
|
+
const latched = yield* latchedFiber
|
40
|
+
Vitest.expect(latched.devtools.latchClosed).toBe(true)
|
41
|
+
Vitest.expect(latched.timestampMs).toBeGreaterThanOrEqual(online.timestampMs)
|
42
|
+
}),
|
43
|
+
)
|
44
|
+
})
|