@livestore/common 0.3.1-dev.0 → 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 -156
- 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-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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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 +122 -123
- 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 +34 -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 +3 -3
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +27 -10
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +2 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +2 -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 +6 -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/materializer-helper.d.ts +13 -2
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +25 -11
- 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 +12 -4
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +8 -3
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +5 -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 +56 -8
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +34 -8
- 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 +36 -9
- 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 -86
- 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 +380 -432
- 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 +2 -2
- 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-db-helper.d.ts +7 -0
- package/dist/sqlite-db-helper.d.ts.map +1 -0
- package/dist/sqlite-db-helper.js +29 -0
- package/dist/sqlite-db-helper.js.map +1 -0
- 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 +12 -3
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +37 -19
- 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.js +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -161
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/index.ts +1 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
- package/src/leader-thread/eventlog.ts +78 -54
- 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 +33 -12
- package/src/leader-thread/mod.ts +2 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +10 -12
- package/src/make-client-session.ts +2 -2
- package/src/materializer-helper.ts +45 -19
- package/src/rematerialize-from-eventlog.ts +12 -4
- package/src/schema/EventDef.ts +16 -4
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +49 -8
- 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 +39 -9
- 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 +2 -2
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-db-helper.ts +41 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +51 -28
- 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,66 +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
|
-
|
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
|
+
},
|
89
97
|
})
|
90
|
-
|
91
|
-
|
92
|
-
|
98
|
+
})
|
99
|
+
.filter((_) => EventSequenceNumber.compare(_.seqNum, since) > 0)
|
100
|
+
.sort((a, b) => EventSequenceNumber.compare(a.seqNum, b.seqNum))
|
101
|
+
}
|
93
102
|
|
94
103
|
export const getClientHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.EventSequenceNumber => {
|
95
104
|
const res = dbEventlog.select<{
|
96
105
|
seqNumGlobal: EventSequenceNumber.GlobalEventSequenceNumber
|
97
106
|
seqNumClient: EventSequenceNumber.ClientEventSequenceNumber
|
107
|
+
seqNumRebaseGeneration: number
|
98
108
|
}>(
|
99
|
-
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`,
|
100
110
|
)[0]
|
101
111
|
|
102
|
-
return res
|
112
|
+
return res
|
113
|
+
? { global: res.seqNumGlobal, client: res.seqNumClient, rebaseGeneration: res.seqNumRebaseGeneration }
|
114
|
+
: EventSequenceNumber.ROOT
|
103
115
|
}
|
104
116
|
|
105
117
|
export const getBackendHeadFromDb = (dbEventlog: SqliteDb): EventSequenceNumber.GlobalEventSequenceNumber =>
|
@@ -143,8 +155,10 @@ export const insertIntoEventlog = (
|
|
143
155
|
values: {
|
144
156
|
seqNumGlobal: eventEncoded.seqNum.global,
|
145
157
|
seqNumClient: eventEncoded.seqNum.client,
|
158
|
+
seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
146
159
|
parentSeqNumGlobal: eventEncoded.parentSeqNum.global,
|
147
160
|
parentSeqNumClient: eventEncoded.parentSeqNum.client,
|
161
|
+
parentSeqNumRebaseGeneration: eventEncoded.parentSeqNum.rebaseGeneration,
|
148
162
|
name: eventEncoded.name,
|
149
163
|
argsJson: eventEncoded.args ?? {},
|
150
164
|
clientId,
|
@@ -154,6 +168,8 @@ export const insertIntoEventlog = (
|
|
154
168
|
},
|
155
169
|
}),
|
156
170
|
)
|
171
|
+
|
172
|
+
dbEventlog.debug.head = eventEncoded.seqNum
|
157
173
|
})
|
158
174
|
|
159
175
|
export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWithMeta>) =>
|
@@ -176,7 +192,11 @@ export const updateSyncMetadata = (items: ReadonlyArray<LiveStoreEvent.EncodedWi
|
|
176
192
|
}
|
177
193
|
})
|
178
194
|
|
179
|
-
export const getSyncBackendCursorInfo = (
|
195
|
+
export const getSyncBackendCursorInfo = ({
|
196
|
+
remoteHead,
|
197
|
+
}: {
|
198
|
+
remoteHead: EventSequenceNumber.GlobalEventSequenceNumber
|
199
|
+
}) =>
|
180
200
|
Effect.gen(function* () {
|
181
201
|
const { dbEventlog } = yield* LeaderThreadCtx
|
182
202
|
|
@@ -193,7 +213,11 @@ export const getSyncBackendCursorInfo = (remoteHead: EventSequenceNumber.GlobalE
|
|
193
213
|
).pipe(Effect.andThen(Schema.decode(EventlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
194
214
|
|
195
215
|
return Option.some({
|
196
|
-
cursor: {
|
216
|
+
cursor: {
|
217
|
+
global: remoteHead,
|
218
|
+
client: EventSequenceNumber.clientDefault,
|
219
|
+
rebaseGeneration: EventSequenceNumber.rebaseGenerationDefault,
|
220
|
+
},
|
197
221
|
metadata: syncMetadataOption,
|
198
222
|
}) satisfies InitialSyncInfo
|
199
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
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
2
|
-
import { Effect, ReadonlyArray, Schema } from '@livestore/utils/effect'
|
1
|
+
import { isDevEnv, LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
2
|
+
import { Effect, Option, ReadonlyArray, Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import type
|
5
|
-
import {
|
4
|
+
import { type SqliteDb, UnexpectedError } from '../adapter-types.js'
|
5
|
+
import { getExecStatementsFromMaterializer, hashMaterializerResults } from '../materializer-helper.js'
|
6
6
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
7
7
|
import { EventSequenceNumber, getEventDef, SystemTables } from '../schema/mod.js'
|
8
8
|
import { insertRow } from '../sql-queries/index.js'
|
@@ -13,13 +13,13 @@ import type { MaterializeEvent } from './types.js'
|
|
13
13
|
|
14
14
|
export const makeMaterializeEvent = ({
|
15
15
|
schema,
|
16
|
-
dbState
|
16
|
+
dbState,
|
17
17
|
dbEventlog,
|
18
18
|
}: {
|
19
19
|
schema: LiveStoreSchema
|
20
20
|
dbState: SqliteDb
|
21
21
|
dbEventlog: SqliteDb
|
22
|
-
}): Effect.Effect<MaterializeEvent,
|
22
|
+
}): Effect.Effect<MaterializeEvent, UnexpectedError> =>
|
23
23
|
Effect.gen(function* () {
|
24
24
|
const eventDefSchemaHashMap = new Map(
|
25
25
|
// TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
|
@@ -35,13 +35,26 @@ export const makeMaterializeEvent = ({
|
|
35
35
|
const eventName = eventEncoded.name
|
36
36
|
const { eventDef, materializer } = getEventDef(schema, eventName)
|
37
37
|
|
38
|
-
const execArgsArr =
|
38
|
+
const execArgsArr = getExecStatementsFromMaterializer({
|
39
39
|
eventDef,
|
40
40
|
materializer,
|
41
|
-
|
41
|
+
dbState,
|
42
42
|
event: { decoded: undefined, encoded: eventEncoded },
|
43
43
|
})
|
44
44
|
|
45
|
+
const materializerHash = isDevEnv() ? Option.some(hashMaterializerResults(execArgsArr)) : Option.none()
|
46
|
+
|
47
|
+
if (
|
48
|
+
materializerHash._tag === 'Some' &&
|
49
|
+
eventEncoded.meta.materializerHashSession._tag === 'Some' &&
|
50
|
+
eventEncoded.meta.materializerHashSession.value !== materializerHash.value
|
51
|
+
) {
|
52
|
+
yield* UnexpectedError.make({
|
53
|
+
cause: `Materializer hash mismatch detected for event "${eventEncoded.name}".`,
|
54
|
+
note: `Please make sure your event materializer is a pure function without side effects.`,
|
55
|
+
})
|
56
|
+
}
|
57
|
+
|
45
58
|
// NOTE we might want to bring this back if we want to debug no-op events
|
46
59
|
// const makeExecuteOptions = (statementSql: string, bindValues: any) => ({
|
47
60
|
// onRowsChanged: (rowsChanged: number) => {
|
@@ -53,26 +66,29 @@ export const makeMaterializeEvent = ({
|
|
53
66
|
|
54
67
|
// console.group('[@livestore/common:leader-thread:materializeEvent]', { eventName })
|
55
68
|
|
56
|
-
const session =
|
69
|
+
const session = dbState.session()
|
57
70
|
|
58
71
|
for (const { statementSql, bindValues } of execArgsArr) {
|
59
72
|
// console.debug(eventName, statementSql, bindValues)
|
60
73
|
// TODO use cached prepared statements instead of exec
|
61
|
-
yield* execSqlPrepared(
|
74
|
+
yield* execSqlPrepared(dbState, statementSql, bindValues)
|
62
75
|
}
|
63
76
|
|
77
|
+
dbState.debug.head = eventEncoded.seqNum
|
78
|
+
|
64
79
|
const changeset = session.changeset()
|
65
80
|
session.finish()
|
66
81
|
|
67
82
|
// TODO use prepared statements
|
68
83
|
yield* execSql(
|
69
|
-
|
84
|
+
dbState,
|
70
85
|
...insertRow({
|
71
86
|
tableName: SystemTables.SESSION_CHANGESET_META_TABLE,
|
72
87
|
columns: SystemTables.sessionChangesetMetaTable.sqliteDef.columns,
|
73
88
|
values: {
|
74
89
|
seqNumGlobal: eventEncoded.seqNum.global,
|
75
90
|
seqNumClient: eventEncoded.seqNum.client,
|
91
|
+
seqNumRebaseGeneration: eventEncoded.seqNum.rebaseGeneration,
|
76
92
|
// NOTE the changeset will be empty (i.e. null) for no-op events
|
77
93
|
changeset: changeset ?? null,
|
78
94
|
debug: LS_DEV ? execArgsArr : null,
|
@@ -107,6 +123,7 @@ export const makeMaterializeEvent = ({
|
|
107
123
|
debug: LS_DEV ? execArgsArr : null,
|
108
124
|
}
|
109
125
|
: { _tag: 'no-op' as const },
|
126
|
+
hash: materializerHash,
|
110
127
|
}
|
111
128
|
}).pipe(
|
112
129
|
Effect.withSpan(`@livestore/common:leader-thread:materializeEvent`, {
|
@@ -135,7 +152,11 @@ export const rollback = ({
|
|
135
152
|
sql`SELECT * FROM ${SystemTables.SESSION_CHANGESET_META_TABLE} WHERE (seqNumGlobal, seqNumClient) IN (${eventNumsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
|
136
153
|
)
|
137
154
|
.map((_) => ({
|
138
|
-
seqNum: {
|
155
|
+
seqNum: {
|
156
|
+
global: _.seqNumGlobal,
|
157
|
+
client: _.seqNumClient,
|
158
|
+
rebaseGeneration: -1, // unused in this code path
|
159
|
+
},
|
139
160
|
changeset: _.changeset,
|
140
161
|
debug: _.debug,
|
141
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
|
+
)
|