@livestore/common 0.3.1 → 0.3.2-dev.0
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 +35 -0
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
- package/dist/ClientSessionLeaderThreadProxy.js +6 -0
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
- package/dist/adapter-types.d.ts +10 -161
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -49
- package/dist/adapter-types.js.map +1 -1
- package/dist/defs.d.ts +20 -0
- package/dist/defs.d.ts.map +1 -0
- package/dist/defs.js +12 -0
- package/dist/defs.js.map +1 -0
- package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +26 -24
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +112 -122
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +17 -6
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +32 -17
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +1 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +37 -7
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +7 -1
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -0
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +13 -6
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +5 -7
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/make-client-session.d.ts +1 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +1 -1
- package/dist/make-client-session.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 +10 -2
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +2 -2
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +2 -2
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +20 -2
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +71 -19
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +88 -3
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +25 -11
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +12 -4
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
- package/dist/schema/state/sqlite/db-schema/hash.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/query-builder/api.d.ts +35 -8
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.js.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 +16 -11
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -81
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +67 -62
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +8 -17
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts +3 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +2 -0
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sqlite-types.d.ts +72 -0
- package/dist/sqlite-types.d.ts.map +1 -0
- package/dist/sqlite-types.js +5 -0
- package/dist/sqlite-types.js.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts +6 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +16 -13
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/graphology.d.ts.map +1 -1
- package/dist/sync/next/graphology.js +0 -6
- package/dist/sync/next/graphology.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +1 -0
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +1 -1
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +12 -3
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -0
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +3 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +13 -4
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +23 -10
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +17 -17
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -166
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +141 -180
- package/src/leader-thread/eventlog.ts +78 -56
- package/src/leader-thread/leader-worker-devtools.ts +1 -2
- package/src/leader-thread/make-leader-thread-layer.ts +52 -8
- package/src/leader-thread/materialize-event.ts +8 -1
- package/src/leader-thread/mod.ts +1 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +6 -11
- package/src/make-client-session.ts +2 -2
- package/src/rematerialize-from-eventlog.ts +10 -2
- package/src/schema/EventDef.ts +5 -3
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +20 -4
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
- package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
- package/src/schema/state/sqlite/mod.ts +1 -1
- package/src/schema/state/sqlite/query-builder/api.ts +38 -8
- package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
- package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
- package/src/schema/state/sqlite/system-tables.ts +9 -22
- package/src/schema/state/sqlite/table-def.ts +1 -1
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +17 -20
- package/src/sync/next/graphology.ts +0 -6
- package/src/sync/next/rebase-events.ts +1 -0
- package/src/sync/next/test/compact-events.test.ts +1 -1
- package/src/sync/next/test/event-fixtures.ts +12 -3
- package/src/sync/sync.ts +3 -0
- package/src/sync/syncstate.test.ts +17 -17
- package/src/sync/syncstate.ts +31 -10
- package/src/version.ts +1 -1
@@ -14,7 +14,7 @@ import {
|
|
14
14
|
import { migrateTable } from '../schema-management/migrations.js'
|
15
15
|
import { insertRow, updateRows } from '../sql-queries/sql-queries.js'
|
16
16
|
import type { PreparedBindValues } from '../util.js'
|
17
|
-
import {
|
17
|
+
import { sql } from '../util.js'
|
18
18
|
import { execSql } from './connection.js'
|
19
19
|
import type { InitialSyncInfo } from './types.js'
|
20
20
|
import { LeaderThreadCtx } from './types.js'
|
@@ -40,68 +40,78 @@ export const initEventlogDb = (dbEventlog: SqliteDb) =>
|
|
40
40
|
)
|
41
41
|
})
|
42
42
|
|
43
|
-
/**
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
.
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
43
|
+
/**
|
44
|
+
* Exclusive of the "since event"
|
45
|
+
* Also queries the state db in order to get the SQLite session changeset data.
|
46
|
+
*/
|
47
|
+
export const getEventsSince = ({
|
48
|
+
dbEventlog,
|
49
|
+
dbState,
|
50
|
+
since,
|
51
|
+
}: {
|
52
|
+
dbEventlog: SqliteDb
|
53
|
+
dbState: SqliteDb
|
54
|
+
since: EventSequenceNumber.EventSequenceNumber
|
55
|
+
}): ReadonlyArray<LiveStoreEvent.EncodedWithMeta> => {
|
56
|
+
const pendingEvents = dbEventlog.select(eventlogMetaTable.where('seqNumGlobal', '>=', since.global))
|
57
|
+
|
58
|
+
const sessionChangesetRowsDecoded = dbState.select(
|
59
|
+
sessionChangesetMetaTable.where('seqNumGlobal', '>=', since.global),
|
60
|
+
)
|
61
|
+
|
62
|
+
return pendingEvents
|
63
|
+
.map((eventlogEvent) => {
|
64
|
+
const sessionChangeset = sessionChangesetRowsDecoded.find(
|
65
|
+
(readModelEvent) =>
|
66
|
+
readModelEvent.seqNumGlobal === eventlogEvent.seqNumGlobal &&
|
67
|
+
readModelEvent.seqNumClient === eventlogEvent.seqNumClient,
|
68
|
+
)
|
69
|
+
return LiveStoreEvent.EncodedWithMeta.make({
|
70
|
+
name: eventlogEvent.name,
|
71
|
+
args: eventlogEvent.argsJson,
|
72
|
+
seqNum: {
|
73
|
+
global: eventlogEvent.seqNumGlobal,
|
74
|
+
client: eventlogEvent.seqNumClient,
|
75
|
+
rebaseGeneration: eventlogEvent.seqNumRebaseGeneration,
|
76
|
+
},
|
77
|
+
parentSeqNum: {
|
78
|
+
global: eventlogEvent.parentSeqNumGlobal,
|
79
|
+
client: eventlogEvent.parentSeqNumClient,
|
80
|
+
rebaseGeneration: eventlogEvent.parentSeqNumRebaseGeneration,
|
81
|
+
},
|
82
|
+
clientId: eventlogEvent.clientId,
|
83
|
+
sessionId: eventlogEvent.sessionId,
|
84
|
+
meta: {
|
85
|
+
sessionChangeset:
|
86
|
+
sessionChangeset && sessionChangeset.changeset !== null
|
87
|
+
? {
|
88
|
+
_tag: 'sessionChangeset' as const,
|
89
|
+
data: sessionChangeset.changeset,
|
90
|
+
debug: sessionChangeset.debug,
|
91
|
+
}
|
92
|
+
: { _tag: 'unset' as const },
|
93
|
+
syncMetadata: eventlogEvent.syncMetadataJson,
|
94
|
+
materializerHashLeader: Option.none(),
|
95
|
+
materializerHashSession: Option.none(),
|
96
|
+
},
|
91
97
|
})
|
92
|
-
|
93
|
-
|
94
|
-
|
98
|
+
})
|
99
|
+
.filter((_) => EventSequenceNumber.compare(_.seqNum, since) > 0)
|
100
|
+
.sort((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
|
101
|
+
}
|
95
102
|
|
96
103
|
export const getClientHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.EventSequenceNumber => {
|
97
104
|
const res = dbEventlog.select<{
|
98
105
|
seqNumGlobal: EventSequenceNumber.GlobalEventSequenceNumber
|
99
106
|
seqNumClient: EventSequenceNumber.ClientEventSequenceNumber
|
107
|
+
seqNumRebaseGeneration: number
|
100
108
|
}>(
|
101
|
-
sql`select seqNumGlobal, seqNumClient from ${EVENTLOG_META_TABLE} order by seqNumGlobal DESC, seqNumClient DESC limit 1`,
|
109
|
+
sql`select seqNumGlobal, seqNumClient, seqNumRebaseGeneration from ${EVENTLOG_META_TABLE} order by seqNumGlobal DESC, seqNumClient DESC limit 1`,
|
102
110
|
)[0]
|
103
111
|
|
104
|
-
return res
|
112
|
+
return res
|
113
|
+
? { global: res.seqNumGlobal, client: res.seqNumClient, rebaseGeneration: res.seqNumRebaseGeneration }
|
114
|
+
: EventSequenceNumber.ROOT
|
105
115
|
}
|
106
116
|
|
107
117
|
export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.GlobalEventSequenceNumber =>
|
@@ -145,8 +155,10 @@ export const insertIntoEventlog = (
|
|
145
155
|
values: {
|
146
156
|
seqNumGlobal: eventEncoded.seqNum.global,
|
147
157
|
seqNumClient: eventEncoded.seqNum.client,
|
158
|
+
seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
148
159
|
parentSeqNumGlobal: eventEncoded.parentSeqNum.global,
|
149
160
|
parentSeqNumClient: eventEncoded.parentSeqNum.client,
|
161
|
+
parentSeqNumRebaseGeneration: eventEncoded.parentSeqNum.rebaseGeneration,
|
150
162
|
name: eventEncoded.name,
|
151
163
|
argsJson: eventEncoded.args ?? {},
|
152
164
|
clientId,
|
@@ -156,6 +168,8 @@ export const insertIntoEventlog = (
|
|
156
168
|
},
|
157
169
|
}),
|
158
170
|
)
|
171
|
+
|
172
|
+
dbEventlog.debug.head = eventEncoded.seqNum
|
159
173
|
})
|
160
174
|
|
161
175
|
export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>) =>
|
@@ -178,7 +192,11 @@ export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWi
|
|
178
192
|
}
|
179
193
|
})
|
180
194
|
|
181
|
-
export const getSyncBackendCursorInfo = (
|
195
|
+
export const getSyncBackendCursorInfo = ({
|
196
|
+
remoteHead,
|
197
|
+
}: {
|
198
|
+
remoteHead: EventSequenceNumber.GlobalEventSequenceNumber
|
199
|
+
}) =>
|
182
200
|
Effect.gen(function* () {
|
183
201
|
const { dbEventlog } = yield* LeaderThreadCtx
|
184
202
|
|
@@ -195,7 +213,11 @@ export const getSyncBackendCursorInfo = (remoteHead: EventSequenceNumber.GlobalE
|
|
195
213
|
).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
196
214
|
|
197
215
|
return Option.some({
|
198
|
-
cursor: {
|
216
|
+
cursor: {
|
217
|
+
global: remoteHead,
|
218
|
+
client: EventSequenceNumber.clientDefault,
|
219
|
+
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
220
|
+
},
|
199
221
|
metadata: syncMetadataOption,
|
200
222
|
}) satisfies InitialSyncInfo
|
201
223
|
}).pipe(Effect.withSpan('@livestore/common:eventlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
|
@@ -48,9 +48,8 @@ export const bootDevtools = (options: DevtoolsOptions) =>
|
|
48
48
|
)
|
49
49
|
|
50
50
|
const syncState = yield* syncProcessor.syncState
|
51
|
-
const mergeCounter = syncProcessor.getMergeCounter()
|
52
51
|
|
53
|
-
yield* syncProcessor.pull({ cursor:
|
52
|
+
yield* syncProcessor.pull({ cursor: syncState.localHead }).pipe(
|
54
53
|
Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
|
55
54
|
Stream.runDrain,
|
56
55
|
Effect.forkScoped,
|
@@ -1,16 +1,18 @@
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
1
2
|
import type { HttpClient, Schema, Scope } from '@livestore/utils/effect'
|
2
3
|
import { Deferred, Effect, Layer, Queue, SubscriptionRef } from '@livestore/utils/effect'
|
3
4
|
|
4
|
-
import type { BootStatus, MakeSqliteDb, SqliteError } from '../adapter-types.js'
|
5
|
+
import type { BootStatus, MakeSqliteDb, SqliteDb, SqliteError } from '../adapter-types.js'
|
5
6
|
import { UnexpectedError } from '../adapter-types.js'
|
6
7
|
import type * as Devtools from '../devtools/mod.js'
|
7
8
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
8
|
-
import { LiveStoreEvent } from '../schema/mod.js'
|
9
|
+
import { EventSequenceNumber, LiveStoreEvent } from '../schema/mod.js'
|
9
10
|
import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
|
11
|
+
import { SyncState } from '../sync/syncstate.js'
|
10
12
|
import { sql } from '../util.js'
|
11
13
|
import * as Eventlog from './eventlog.js'
|
12
|
-
import { bootDevtools } from './leader-worker-devtools.js'
|
13
14
|
import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
|
15
|
+
import { bootDevtools } from './leader-worker-devtools.js'
|
14
16
|
import { makeMaterializeEvent } from './materialize-event.js'
|
15
17
|
import { recreateDb } from './recreate-db.js'
|
16
18
|
import type { ShutdownChannel } from './shutdown-channel.js'
|
@@ -89,10 +91,8 @@ export const makeLeaderThreadLayer = ({
|
|
89
91
|
|
90
92
|
const syncProcessor = yield* makeLeaderSyncProcessor({
|
91
93
|
schema,
|
92
|
-
dbEventlogMissing,
|
93
|
-
dbEventlog,
|
94
94
|
dbState,
|
95
|
-
|
95
|
+
initialSyncState: getInitialSyncState({ dbEventlog, dbState, dbEventlogMissing }),
|
96
96
|
initialBlockingSyncContext,
|
97
97
|
onError: syncOptions?.onSyncError ?? 'ignore',
|
98
98
|
params: {
|
@@ -158,6 +158,48 @@ export const makeLeaderThreadLayer = ({
|
|
158
158
|
Layer.unwrapScoped,
|
159
159
|
)
|
160
160
|
|
161
|
+
const getInitialSyncState = ({
|
162
|
+
dbEventlog,
|
163
|
+
dbState,
|
164
|
+
dbEventlogMissing,
|
165
|
+
}: {
|
166
|
+
dbEventlog: SqliteDb
|
167
|
+
dbState: SqliteDb
|
168
|
+
dbEventlogMissing: boolean
|
169
|
+
}) => {
|
170
|
+
const initialBackendHead = dbEventlogMissing
|
171
|
+
? EventSequenceNumber.ROOT.global
|
172
|
+
: Eventlog.getBackendHeadFromDb(dbEventlog)
|
173
|
+
|
174
|
+
const initialLocalHead = dbEventlogMissing ? EventSequenceNumber.ROOT : Eventlog.getClientHeadFromDb(dbEventlog)
|
175
|
+
|
176
|
+
if (initialBackendHead > initialLocalHead.global) {
|
177
|
+
return shouldNeverHappen(
|
178
|
+
`During boot the backend head (${initialBackendHead}) should never be greater than the local head (${initialLocalHead.global})`,
|
179
|
+
)
|
180
|
+
}
|
181
|
+
|
182
|
+
return SyncState.make({
|
183
|
+
localHead: initialLocalHead,
|
184
|
+
upstreamHead: {
|
185
|
+
global: initialBackendHead,
|
186
|
+
client: EventSequenceNumber.clientDefault,
|
187
|
+
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
188
|
+
},
|
189
|
+
pending: dbEventlogMissing
|
190
|
+
? []
|
191
|
+
: Eventlog.getEventsSince({
|
192
|
+
dbEventlog,
|
193
|
+
dbState,
|
194
|
+
since: {
|
195
|
+
global: initialBackendHead,
|
196
|
+
client: EventSequenceNumber.clientDefault,
|
197
|
+
rebaseGeneration: initialLocalHead.rebaseGeneration,
|
198
|
+
},
|
199
|
+
}),
|
200
|
+
})
|
201
|
+
}
|
202
|
+
|
161
203
|
const makeInitialBlockingSyncContext = ({
|
162
204
|
initialSyncOptions,
|
163
205
|
bootStatusQueue,
|
@@ -223,11 +265,13 @@ const bootLeaderThread = ({
|
|
223
265
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
224
266
|
> =>
|
225
267
|
Effect.gen(function* () {
|
226
|
-
const { dbEventlog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
268
|
+
const { dbEventlog, bootStatusQueue, syncProcessor, schema, materializeEvent, dbState } = yield* LeaderThreadCtx
|
227
269
|
|
228
270
|
yield* Eventlog.initEventlogDb(dbEventlog)
|
229
271
|
|
230
|
-
const { migrationsReport } = dbStateMissing
|
272
|
+
const { migrationsReport } = dbStateMissing
|
273
|
+
? yield* recreateDb({ dbState, dbEventlog, schema, bootStatusQueue, materializeEvent })
|
274
|
+
: { migrationsReport: { migrations: [] } }
|
231
275
|
|
232
276
|
// NOTE the sync processor depends on the dbs being initialized properly
|
233
277
|
const { initialLeaderHead } = yield* syncProcessor.boot
|
@@ -74,6 +74,8 @@ export const makeMaterializeEvent = ({
|
|
74
74
|
yield* execSqlPrepared(dbState, statementSql, bindValues)
|
75
75
|
}
|
76
76
|
|
77
|
+
dbState.debug.head = eventEncoded.seqNum
|
78
|
+
|
77
79
|
const changeset = session.changeset()
|
78
80
|
session.finish()
|
79
81
|
|
@@ -86,6 +88,7 @@ export const makeMaterializeEvent = ({
|
|
86
88
|
values: {
|
87
89
|
seqNumGlobal: eventEncoded.seqNum.global,
|
88
90
|
seqNumClient: eventEncoded.seqNum.client,
|
91
|
+
seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
89
92
|
// NOTE the changeset will be empty (i.e. null) for no-op events
|
90
93
|
changeset: changeset ?? null,
|
91
94
|
debug: LS_DEV ? execArgsArr : null,
|
@@ -149,7 +152,11 @@ export const rollback = ({
|
|
149
152
|
sql`SELECT * FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE (seqNumGlobal, seqNumClient) IN (${eventNumsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
|
150
153
|
)
|
151
154
|
.map((_) => ({
|
152
|
-
seqNum: {
|
155
|
+
seqNum: {
|
156
|
+
global: _.seqNumGlobal,
|
157
|
+
client: _.seqNumClient,
|
158
|
+
rebaseGeneration: -1, // unused in this code path
|
159
|
+
},
|
153
160
|
changeset: _.changeset,
|
154
161
|
debug: _.debug,
|
155
162
|
}))
|
package/src/leader-thread/mod.ts
CHANGED
@@ -1,108 +1,116 @@
|
|
1
1
|
import { casesHandled } from '@livestore/utils'
|
2
|
-
import type { HttpClient } from '@livestore/utils/effect'
|
3
2
|
import { Effect, Queue } from '@livestore/utils/effect'
|
4
3
|
|
5
|
-
import type {
|
4
|
+
import type { MigrationsReport } from '../defs.js'
|
5
|
+
import type { BootStatus, MigrationHooks, SqliteDb, SqliteError } from '../index.js'
|
6
6
|
import { migrateDb, rematerializeFromEventlog, UnexpectedError } from '../index.js'
|
7
|
+
import type { LiveStoreSchema } from '../schema/mod.js'
|
7
8
|
import { configureConnection } from './connection.js'
|
8
|
-
import {
|
9
|
-
|
10
|
-
export const recreateDb
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
9
|
+
import type { MaterializeEvent } from './types.js'
|
10
|
+
|
11
|
+
export const recreateDb = ({
|
12
|
+
dbState,
|
13
|
+
dbEventlog,
|
14
|
+
schema,
|
15
|
+
bootStatusQueue,
|
16
|
+
materializeEvent,
|
17
|
+
}: {
|
18
|
+
dbState: SqliteDb
|
19
|
+
dbEventlog: SqliteDb
|
20
|
+
schema: LiveStoreSchema
|
21
|
+
bootStatusQueue: Queue.Queue<BootStatus>
|
22
|
+
materializeEvent: MaterializeEvent
|
23
|
+
}): Effect.Effect<{ migrationsReport: MigrationsReport }, UnexpectedError | SqliteError> =>
|
24
|
+
Effect.gen(function* () {
|
25
|
+
const migrationOptions = schema.state.sqlite.migrations
|
26
|
+
let migrationsReport: MigrationsReport
|
27
|
+
|
28
|
+
yield* Effect.addFinalizer(
|
29
|
+
Effect.fn('recreateDb:finalizer')(function* (ex) {
|
30
|
+
if (ex._tag === 'Failure') dbState.destroy()
|
31
|
+
}),
|
32
|
+
)
|
33
|
+
|
34
|
+
// NOTE to speed up the operations below, we're creating a temporary in-memory database
|
35
|
+
// and later we'll overwrite the persisted database with the new data
|
36
|
+
// TODO bring back this optimization
|
37
|
+
// const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
|
38
|
+
const tmpDb = dbState
|
39
|
+
yield* configureConnection(tmpDb, { foreignKeys: true })
|
40
|
+
|
41
|
+
const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
|
42
|
+
Effect.gen(function* () {
|
43
|
+
yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
44
|
+
|
45
|
+
const migrationsReport = yield* migrateDb({
|
46
|
+
db: tmpDb,
|
47
|
+
schema,
|
48
|
+
onProgress: ({ done, total }) =>
|
49
|
+
Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
|
50
|
+
})
|
51
|
+
|
52
|
+
yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
53
|
+
|
54
|
+
return { migrationsReport, tmpDb }
|
42
55
|
})
|
43
56
|
|
44
|
-
|
57
|
+
switch (migrationOptions.strategy) {
|
58
|
+
case 'auto': {
|
59
|
+
const hooks = migrationOptions.hooks
|
60
|
+
const initResult = yield* initDb(hooks)
|
45
61
|
|
46
|
-
|
47
|
-
})
|
62
|
+
migrationsReport = initResult.migrationsReport
|
48
63
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
yield* rematerializeFromEventlog({
|
65
|
+
// db: initResult.tmpDb,
|
66
|
+
dbEventlog,
|
67
|
+
schema,
|
68
|
+
materializeEvent,
|
69
|
+
onProgress: ({ done, total }) =>
|
70
|
+
Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
|
71
|
+
})
|
53
72
|
|
54
|
-
|
73
|
+
yield* Effect.tryAll(() => hooks?.post?.(initResult.tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
|
55
74
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
materializeEvent,
|
61
|
-
onProgress: ({ done, total }) =>
|
62
|
-
Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
|
63
|
-
})
|
75
|
+
break
|
76
|
+
}
|
77
|
+
case 'manual': {
|
78
|
+
const oldDbData = dbState.export()
|
64
79
|
|
65
|
-
|
66
|
-
|
67
|
-
break
|
68
|
-
}
|
69
|
-
case 'manual': {
|
70
|
-
const oldDbData = dbState.export()
|
80
|
+
migrationsReport = { migrations: [] }
|
71
81
|
|
72
|
-
|
82
|
+
const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
|
83
|
+
UnexpectedError.mapToUnexpectedError,
|
84
|
+
)
|
73
85
|
|
74
|
-
|
75
|
-
UnexpectedError.mapToUnexpectedError,
|
76
|
-
)
|
86
|
+
tmpDb.import(newDbData)
|
77
87
|
|
78
|
-
|
88
|
+
// TODO validate schema
|
79
89
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
casesHandled(migrationOptions)
|
90
|
+
break
|
91
|
+
}
|
92
|
+
default: {
|
93
|
+
casesHandled(migrationOptions)
|
94
|
+
}
|
86
95
|
}
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
)
|
96
|
+
|
97
|
+
// TODO bring back
|
98
|
+
// Import the temporary in-memory database into the persistent database
|
99
|
+
// yield* Effect.sync(() => db.import(tmpDb)).pipe(
|
100
|
+
// Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
|
101
|
+
// )
|
102
|
+
|
103
|
+
// TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
|
104
|
+
// We've disabled this for now as it made the code too complex, as we often run syncing right after
|
105
|
+
// so the snapshot is no longer up to date
|
106
|
+
// const snapshotFromTmpDb = tmpDb.export()
|
107
|
+
|
108
|
+
// TODO bring back
|
109
|
+
// tmpDb.close()
|
110
|
+
|
111
|
+
return { migrationsReport }
|
112
|
+
}).pipe(
|
113
|
+
Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
|
114
|
+
Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
|
115
|
+
Effect.withPerformanceMeasure('@livestore/common:leader-thread:recreateDb'),
|
116
|
+
)
|
@@ -12,13 +12,13 @@ import type {
|
|
12
12
|
import { Context, Schema } from '@livestore/utils/effect'
|
13
13
|
import type { MeshNode } from '@livestore/webmesh'
|
14
14
|
|
15
|
-
import type {
|
15
|
+
import type { MigrationsReport } from '../defs.js'
|
16
|
+
import type { SqliteError } from '../errors.js'
|
16
17
|
import type {
|
17
18
|
BootStatus,
|
18
19
|
Devtools,
|
19
20
|
LeaderAheadError,
|
20
21
|
MakeSqliteDb,
|
21
|
-
MigrationsReport,
|
22
22
|
PersistenceInfo,
|
23
23
|
SqliteDb,
|
24
24
|
SyncBackend,
|
@@ -136,16 +136,12 @@ export type InitialBlockingSyncContext = {
|
|
136
136
|
export interface LeaderSyncProcessor {
|
137
137
|
/** Used by client sessions to subscribe to upstream sync state changes */
|
138
138
|
pull: (args: {
|
139
|
-
cursor:
|
140
|
-
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type
|
139
|
+
cursor: EventSequenceNumber.EventSequenceNumber
|
140
|
+
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type }, UnexpectedError>
|
141
141
|
/** The `pullQueue` API can be used instead of `pull` when more convenient */
|
142
142
|
pullQueue: (args: {
|
143
|
-
cursor:
|
144
|
-
}) => Effect.Effect<
|
145
|
-
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
146
|
-
UnexpectedError,
|
147
|
-
Scope.Scope
|
148
|
-
>
|
143
|
+
cursor: EventSequenceNumber.EventSequenceNumber
|
144
|
+
}) => Effect.Effect<Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type }>, UnexpectedError, Scope.Scope>
|
149
145
|
|
150
146
|
/** Used by client sessions to push events to the leader thread */
|
151
147
|
push: (
|
@@ -173,5 +169,4 @@ export interface LeaderSyncProcessor {
|
|
173
169
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
174
170
|
>
|
175
171
|
syncState: Subscribable.Subscribable<SyncState.SyncState>
|
176
|
-
getMergeCounter: () => number
|
177
172
|
}
|
@@ -39,7 +39,7 @@ export const makeClientSession = <R>({
|
|
39
39
|
sessionId: string
|
40
40
|
isLeader: boolean
|
41
41
|
lockStatus: SubscriptionRef.SubscriptionRef<LockStatus>
|
42
|
-
leaderThread: ClientSessionLeaderThreadProxy
|
42
|
+
leaderThread: ClientSessionLeaderThreadProxy.ClientSessionLeaderThreadProxy
|
43
43
|
sqliteDb: SqliteDb
|
44
44
|
connectWebmeshNode: (args: {
|
45
45
|
webmeshNode: Webmesh.MeshNode
|
@@ -133,4 +133,4 @@ export const makeClientSession = <R>({
|
|
133
133
|
shutdown,
|
134
134
|
debugInstanceId,
|
135
135
|
} satisfies ClientSession
|
136
|
-
})
|
136
|
+
}).pipe(Effect.withSpan('@livestore/common:make-client-session'))
|
@@ -56,8 +56,16 @@ This likely means the schema has changed in an incompatible way.
|
|
56
56
|
)
|
57
57
|
|
58
58
|
const eventEncoded = LiveStoreEvent.EncodedWithMeta.make({
|
59
|
-
seqNum: {
|
60
|
-
|
59
|
+
seqNum: {
|
60
|
+
global: row.seqNumGlobal,
|
61
|
+
client: row.seqNumClient,
|
62
|
+
rebaseGeneration: row.seqNumRebaseGeneration,
|
63
|
+
},
|
64
|
+
parentSeqNum: {
|
65
|
+
global: row.parentSeqNumGlobal,
|
66
|
+
client: row.parentSeqNumClient,
|
67
|
+
rebaseGeneration: row.parentSeqNumRebaseGeneration,
|
68
|
+
},
|
61
69
|
name: row.name,
|
62
70
|
args,
|
63
71
|
clientId: row.clientId,
|
package/src/schema/EventDef.ts
CHANGED
@@ -28,7 +28,9 @@ export type EventDef<TName extends string, TType, TEncoded = TType, TDerived ext
|
|
28
28
|
}
|
29
29
|
|
30
30
|
/** Helper function to construct a partial event */
|
31
|
-
(
|
31
|
+
(
|
32
|
+
args: TType,
|
33
|
+
): {
|
32
34
|
name: TName
|
33
35
|
args: TType
|
34
36
|
}
|
@@ -194,14 +196,14 @@ export type Materializer<TEventDef extends EventDef.AnyWithoutFn = EventDef.AnyW
|
|
194
196
|
) => SingleOrReadonlyArray<MaterializerResult>
|
195
197
|
|
196
198
|
export const defineMaterializer = <TEventDef extends EventDef.AnyWithoutFn>(
|
197
|
-
|
199
|
+
_eventDef: TEventDef,
|
198
200
|
materializer: Materializer<TEventDef>,
|
199
201
|
): Materializer<TEventDef> => {
|
200
202
|
return materializer
|
201
203
|
}
|
202
204
|
|
203
205
|
export const materializers = <TInputRecord extends Record<string, EventDef.AnyWithoutFn>>(
|
204
|
-
|
206
|
+
_eventDefRecord: TInputRecord,
|
205
207
|
handlers: {
|
206
208
|
[TEventName in TInputRecord[keyof TInputRecord]['name'] as Extract<
|
207
209
|
TInputRecord[keyof TInputRecord],
|