@livestore/common 0.3.0-dev.25 → 0.3.0-dev.27
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/adapter-types.d.ts +13 -12
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -6
- 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 +13 -6
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +6 -0
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +25 -25
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +1 -2
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +15 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +229 -207
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/apply-mutation.d.ts +14 -9
- package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
- package/dist/leader-thread/apply-mutation.js +43 -36
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +4 -5
- 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 +21 -33
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -1
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -1
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/mutationlog.d.ts +19 -3
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/mutationlog.js +105 -12
- package/dist/leader-thread/mutationlog.js.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/pull-queue-set.js +6 -16
- package/dist/leader-thread/pull-queue-set.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +4 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +34 -19
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts +5 -4
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +7 -9
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/EventId.d.ts +9 -0
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +17 -2
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts +78 -25
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +25 -12
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/system-tables.d.ts +67 -0
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +12 -1
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +49 -43
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/sync.d.ts +6 -5
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +60 -84
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +127 -136
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +205 -276
- 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 +2 -2
- package/src/adapter-types.ts +11 -13
- package/src/devtools/devtools-messages-common.ts +9 -0
- package/src/devtools/devtools-messages-leader.ts +1 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +399 -364
- package/src/leader-thread/apply-mutation.ts +81 -71
- package/src/leader-thread/leader-worker-devtools.ts +5 -7
- package/src/leader-thread/make-leader-thread-layer.ts +26 -41
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/mutationlog.ts +166 -13
- package/src/leader-thread/recreate-db.ts +4 -3
- package/src/leader-thread/types.ts +33 -23
- package/src/rehydrate-from-mutationlog.ts +12 -12
- package/src/schema/EventId.ts +20 -2
- package/src/schema/MutationEvent.ts +32 -16
- package/src/schema/schema.ts +1 -1
- package/src/schema/system-tables.ts +20 -1
- package/src/sync/ClientSessionSyncProcessor.ts +59 -47
- package/src/sync/sync.ts +6 -9
- package/src/sync/syncstate.test.ts +239 -315
- package/src/sync/syncstate.ts +191 -188
- package/src/version.ts +1 -1
- package/tmp/pack.tgz +0 -0
- package/src/leader-thread/pull-queue-set.ts +0 -67
@@ -1,46 +1,43 @@
|
|
1
1
|
import { LS_DEV, memoizeByRef, shouldNeverHappen } from '@livestore/utils'
|
2
|
-
import
|
3
|
-
import { Effect, Option, Schema } from '@livestore/utils/effect'
|
2
|
+
import { Effect, ReadonlyArray, Schema } from '@livestore/utils/effect'
|
4
3
|
|
5
|
-
import type {
|
4
|
+
import type { SqliteDb } from '../adapter-types.js'
|
6
5
|
import { getExecArgsFromMutation } from '../mutation.js'
|
6
|
+
import type { LiveStoreSchema, MutationEvent, SessionChangesetMetaRow } from '../schema/mod.js'
|
7
7
|
import {
|
8
8
|
EventId,
|
9
9
|
getMutationDef,
|
10
|
-
type LiveStoreSchema,
|
11
10
|
MUTATION_LOG_META_TABLE,
|
12
|
-
type MutationEvent,
|
13
|
-
mutationLogMetaTable,
|
14
11
|
SESSION_CHANGESET_META_TABLE,
|
15
12
|
sessionChangesetMetaTable,
|
16
13
|
} from '../schema/mod.js'
|
17
14
|
import { insertRow } from '../sql-queries/index.js'
|
15
|
+
import { sql } from '../util.js'
|
18
16
|
import { execSql, execSqlPrepared } from './connection.js'
|
19
|
-
import
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
const shouldExcludeMutationFromLog = makeShouldExcludeMutationFromLog(
|
17
|
+
import * as Mutationlog from './mutationlog.js'
|
18
|
+
import type { ApplyMutation } from './types.js'
|
19
|
+
|
20
|
+
export const makeApplyMutation = ({
|
21
|
+
schema,
|
22
|
+
dbReadModel: db,
|
23
|
+
dbMutationLog,
|
24
|
+
}: {
|
25
|
+
schema: LiveStoreSchema
|
26
|
+
dbReadModel: SqliteDb
|
27
|
+
dbMutationLog: SqliteDb
|
28
|
+
}): Effect.Effect<ApplyMutation, never> =>
|
29
|
+
Effect.gen(function* () {
|
30
|
+
const shouldExcludeMutationFromLog = makeShouldExcludeMutationFromLog(schema)
|
33
31
|
|
34
32
|
const mutationDefSchemaHashMap = new Map(
|
35
33
|
// TODO Running `Schema.hash` can be a bottleneck for larger schemas. There is an opportunity to run this
|
36
34
|
// at build time and lookup the pre-computed hash at runtime.
|
37
35
|
// Also see https://github.com/Effect-TS/effect/issues/2719
|
38
|
-
[...
|
36
|
+
[...schema.mutations.map.entries()].map(([k, v]) => [k, Schema.hash(v.schema)] as const),
|
39
37
|
)
|
40
38
|
|
41
39
|
return (mutationEventEncoded, options) =>
|
42
40
|
Effect.gen(function* () {
|
43
|
-
const { schema, dbReadModel: db, dbMutationLog } = leaderThreadCtx
|
44
41
|
const skipMutationLog = options?.skipMutationLog ?? false
|
45
42
|
|
46
43
|
const mutationName = mutationEventEncoded.mutation
|
@@ -84,7 +81,7 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
|
|
84
81
|
idClient: mutationEventEncoded.id.client,
|
85
82
|
// NOTE the changeset will be empty (i.e. null) for no-op mutations
|
86
83
|
changeset: changeset ?? null,
|
87
|
-
debug: execArgsArr,
|
84
|
+
debug: LS_DEV ? execArgsArr : null,
|
88
85
|
},
|
89
86
|
}),
|
90
87
|
)
|
@@ -94,76 +91,89 @@ export const makeApplyMutation: Effect.Effect<ApplyMutation, never, Scope.Scope
|
|
94
91
|
// write to mutation_log
|
95
92
|
const excludeFromMutationLog = shouldExcludeMutationFromLog(mutationName, mutationEventEncoded)
|
96
93
|
if (skipMutationLog === false && excludeFromMutationLog === false) {
|
97
|
-
|
94
|
+
const mutationName = mutationEventEncoded.mutation
|
95
|
+
const mutationDefSchemaHash =
|
96
|
+
mutationDefSchemaHashMap.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
|
97
|
+
|
98
|
+
yield* Mutationlog.insertIntoMutationLog(
|
98
99
|
mutationEventEncoded,
|
99
100
|
dbMutationLog,
|
100
|
-
|
101
|
+
mutationDefSchemaHash,
|
101
102
|
mutationEventEncoded.clientId,
|
102
103
|
mutationEventEncoded.sessionId,
|
103
104
|
)
|
104
105
|
} else {
|
105
106
|
// console.debug('[@livestore/common:leader-thread] skipping mutation log write', mutation, statementSql, bindValues)
|
106
107
|
}
|
108
|
+
|
109
|
+
return {
|
110
|
+
sessionChangeset: changeset
|
111
|
+
? {
|
112
|
+
_tag: 'sessionChangeset' as const,
|
113
|
+
data: changeset,
|
114
|
+
debug: LS_DEV ? execArgsArr : null,
|
115
|
+
}
|
116
|
+
: { _tag: 'no-op' as const },
|
117
|
+
}
|
107
118
|
}).pipe(
|
108
119
|
Effect.withSpan(`@livestore/common:leader-thread:applyMutation`, {
|
109
120
|
attributes: {
|
110
121
|
mutationName: mutationEventEncoded.mutation,
|
111
122
|
mutationId: mutationEventEncoded.id,
|
112
|
-
'span.label':
|
123
|
+
'span.label': `${EventId.toString(mutationEventEncoded.id)} ${mutationEventEncoded.mutation}`,
|
113
124
|
},
|
114
125
|
}),
|
115
126
|
// Effect.logDuration('@livestore/common:leader-thread:applyMutation'),
|
116
127
|
)
|
117
|
-
}
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
128
|
+
})
|
129
|
+
|
130
|
+
export const rollback = ({
|
131
|
+
db,
|
132
|
+
dbMutationLog,
|
133
|
+
eventIdsToRollback,
|
134
|
+
}: {
|
135
|
+
db: SqliteDb
|
136
|
+
dbMutationLog: SqliteDb
|
137
|
+
eventIdsToRollback: EventId.EventId[]
|
138
|
+
}) =>
|
127
139
|
Effect.gen(function* () {
|
128
|
-
const
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
shouldNeverHappen(
|
141
|
-
`Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
|
142
|
-
)
|
140
|
+
const rollbackEvents = db
|
141
|
+
.select<SessionChangesetMetaRow>(
|
142
|
+
sql`SELECT * FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`).join(', ')})`,
|
143
|
+
)
|
144
|
+
.map((_) => ({ id: { global: _.idGlobal, client: _.idClient }, changeset: _.changeset, debug: _.debug }))
|
145
|
+
.toSorted((a, b) => EventId.compare(a.id, b.id))
|
146
|
+
|
147
|
+
// Apply changesets in reverse order
|
148
|
+
for (let i = rollbackEvents.length - 1; i >= 0; i--) {
|
149
|
+
const { changeset } = rollbackEvents[i]!
|
150
|
+
if (changeset !== null) {
|
151
|
+
db.makeChangeset(changeset).invert().apply()
|
143
152
|
}
|
144
153
|
}
|
145
154
|
|
146
|
-
|
147
|
-
|
148
|
-
dbMutationLog,
|
149
|
-
...insertRow({
|
150
|
-
tableName: MUTATION_LOG_META_TABLE,
|
151
|
-
columns: mutationLogMetaTable.sqliteDef.columns,
|
152
|
-
values: {
|
153
|
-
idGlobal: mutationEventEncoded.id.global,
|
154
|
-
idClient: mutationEventEncoded.id.client,
|
155
|
-
parentIdGlobal: mutationEventEncoded.parentId.global,
|
156
|
-
parentIdClient: mutationEventEncoded.parentId.client,
|
157
|
-
mutation: mutationEventEncoded.mutation,
|
158
|
-
argsJson: mutationEventEncoded.args ?? {},
|
159
|
-
clientId,
|
160
|
-
sessionId,
|
161
|
-
schemaHash: mutationDefSchemaHash,
|
162
|
-
syncMetadataJson: Option.none(),
|
163
|
-
},
|
164
|
-
}),
|
155
|
+
const eventIdPairChunks = ReadonlyArray.chunksOf(100)(
|
156
|
+
eventIdsToRollback.map((id) => `(${id.global}, ${id.client})`),
|
165
157
|
)
|
166
|
-
|
158
|
+
|
159
|
+
// Delete the changeset rows
|
160
|
+
for (const eventIdPairChunk of eventIdPairChunks) {
|
161
|
+
db.execute(
|
162
|
+
sql`DELETE FROM ${SESSION_CHANGESET_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
163
|
+
)
|
164
|
+
}
|
165
|
+
|
166
|
+
// Delete the mutation log rows
|
167
|
+
for (const eventIdPairChunk of eventIdPairChunks) {
|
168
|
+
dbMutationLog.execute(
|
169
|
+
sql`DELETE FROM ${MUTATION_LOG_META_TABLE} WHERE (idGlobal, idClient) IN (${eventIdPairChunk.join(', ')})`,
|
170
|
+
)
|
171
|
+
}
|
172
|
+
}).pipe(
|
173
|
+
Effect.withSpan('@livestore/common:LeaderSyncProcessor:rollback', {
|
174
|
+
attributes: { count: eventIdsToRollback.length },
|
175
|
+
}),
|
176
|
+
)
|
167
177
|
|
168
178
|
// TODO let's consider removing this "should exclude" mechanism in favour of log compaction etc
|
169
179
|
const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema) => {
|
@@ -15,7 +15,7 @@ export const bootDevtools = (options: DevtoolsOptions) =>
|
|
15
15
|
return
|
16
16
|
}
|
17
17
|
|
18
|
-
const {
|
18
|
+
const { syncProcessor, extraIncomingMessagesQueue } = yield* LeaderThreadCtx
|
19
19
|
|
20
20
|
yield* listenToDevtools({
|
21
21
|
incomingMessages: Stream.fromQueue(extraIncomingMessagesQueue),
|
@@ -33,13 +33,11 @@ export const bootDevtools = (options: DevtoolsOptions) =>
|
|
33
33
|
Effect.ignoreLogged,
|
34
34
|
)
|
35
35
|
|
36
|
-
const
|
36
|
+
const syncState = yield* syncProcessor.syncState
|
37
|
+
const mergeCounter = syncProcessor.getMergeCounter()
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
yield* Stream.fromQueue(pullQueue).pipe(
|
42
|
-
Stream.tap((msg) => sendMessage(Devtools.Leader.SyncPull.make({ payload: msg.payload, liveStoreVersion }))),
|
39
|
+
yield* syncProcessor.pull({ cursor: { mergeCounter, eventId: syncState.localHead } }).pipe(
|
40
|
+
Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
|
43
41
|
Stream.runDrain,
|
44
42
|
Effect.forkScoped,
|
45
43
|
)
|
@@ -5,14 +5,13 @@ import type { BootStatus, MakeSqliteDb, MigrationsReport, SqliteError } from '..
|
|
5
5
|
import { UnexpectedError } from '../adapter-types.js'
|
6
6
|
import type * as Devtools from '../devtools/mod.js'
|
7
7
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
8
|
-
import {
|
9
|
-
import { migrateTable } from '../schema-management/migrations.js'
|
8
|
+
import { MutationEvent } from '../schema/mod.js'
|
10
9
|
import type { InvalidPullError, IsOfflineError, SyncOptions } from '../sync/sync.js'
|
11
10
|
import { sql } from '../util.js'
|
12
|
-
import {
|
11
|
+
import { makeApplyMutation } from './apply-mutation.js'
|
13
12
|
import { bootDevtools } from './leader-worker-devtools.js'
|
14
13
|
import { makeLeaderSyncProcessor } from './LeaderSyncProcessor.js'
|
15
|
-
import
|
14
|
+
import * as Mutationlog from './mutationlog.js'
|
16
15
|
import { recreateDb } from './recreate-db.js'
|
17
16
|
import type { ShutdownChannel } from './shutdown-channel.js'
|
18
17
|
import type {
|
@@ -52,7 +51,10 @@ export const makeLeaderThreadLayer = ({
|
|
52
51
|
|
53
52
|
// TODO do more validation here than just checking the count of tables
|
54
53
|
// Either happens on initial boot or if schema changes
|
55
|
-
const
|
54
|
+
const dbMutationLogMissing =
|
55
|
+
dbMutationLog.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
|
56
|
+
|
57
|
+
const dbReadModelMissing =
|
56
58
|
dbReadModel.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
|
57
59
|
|
58
60
|
const syncBackend =
|
@@ -60,6 +62,11 @@ export const makeLeaderThreadLayer = ({
|
|
60
62
|
? undefined
|
61
63
|
: yield* syncOptions.backend({ storeId, clientId, payload: syncPayload })
|
62
64
|
|
65
|
+
if (syncBackend !== undefined) {
|
66
|
+
// We're already connecting to the sync backend concurrently
|
67
|
+
yield* syncBackend.connect.pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
68
|
+
}
|
69
|
+
|
63
70
|
const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
|
64
71
|
initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
|
65
72
|
bootStatusQueue,
|
@@ -67,10 +74,11 @@ export const makeLeaderThreadLayer = ({
|
|
67
74
|
|
68
75
|
const syncProcessor = yield* makeLeaderSyncProcessor({
|
69
76
|
schema,
|
70
|
-
|
77
|
+
dbMutationLogMissing,
|
71
78
|
dbMutationLog,
|
79
|
+
dbReadModel,
|
80
|
+
dbReadModelMissing,
|
72
81
|
initialBlockingSyncContext,
|
73
|
-
clientId,
|
74
82
|
onError: syncOptions?.onSyncError ?? 'ignore',
|
75
83
|
})
|
76
84
|
|
@@ -86,6 +94,8 @@ export const makeLeaderThreadLayer = ({
|
|
86
94
|
}
|
87
95
|
: { enabled: false as const }
|
88
96
|
|
97
|
+
const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
|
98
|
+
|
89
99
|
const ctx = {
|
90
100
|
schema,
|
91
101
|
bootStatusQueue,
|
@@ -99,7 +109,7 @@ export const makeLeaderThreadLayer = ({
|
|
99
109
|
shutdownChannel,
|
100
110
|
syncBackend,
|
101
111
|
syncProcessor,
|
102
|
-
|
112
|
+
applyMutation,
|
103
113
|
extraIncomingMessagesQueue,
|
104
114
|
devtools: devtoolsContext,
|
105
115
|
// State will be set during `bootLeaderThread`
|
@@ -112,7 +122,7 @@ export const makeLeaderThreadLayer = ({
|
|
112
122
|
const layer = Layer.succeed(LeaderThreadCtx, ctx)
|
113
123
|
|
114
124
|
ctx.initialState = yield* bootLeaderThread({
|
115
|
-
|
125
|
+
dbReadModelMissing,
|
116
126
|
initialBlockingSyncContext,
|
117
127
|
devtoolsOptions,
|
118
128
|
}).pipe(Effect.provide(layer))
|
@@ -122,6 +132,7 @@ export const makeLeaderThreadLayer = ({
|
|
122
132
|
Effect.withSpan('@livestore/common:leader-thread:boot'),
|
123
133
|
Effect.withSpanScoped('@livestore/common:leader-thread'),
|
124
134
|
UnexpectedError.mapToUnexpectedError,
|
135
|
+
Effect.tapCauseLogPretty,
|
125
136
|
Layer.unwrapScoped,
|
126
137
|
)
|
127
138
|
|
@@ -177,11 +188,11 @@ const makeInitialBlockingSyncContext = ({
|
|
177
188
|
* It also starts various background processes (e.g. syncing)
|
178
189
|
*/
|
179
190
|
const bootLeaderThread = ({
|
180
|
-
|
191
|
+
dbReadModelMissing,
|
181
192
|
initialBlockingSyncContext,
|
182
193
|
devtoolsOptions,
|
183
194
|
}: {
|
184
|
-
|
195
|
+
dbReadModelMissing: boolean
|
185
196
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
186
197
|
devtoolsOptions: DevtoolsOptions
|
187
198
|
}): Effect.Effect<
|
@@ -192,44 +203,18 @@ const bootLeaderThread = ({
|
|
192
203
|
Effect.gen(function* () {
|
193
204
|
const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
194
205
|
|
195
|
-
yield*
|
196
|
-
db: dbMutationLog,
|
197
|
-
behaviour: 'create-if-not-exists',
|
198
|
-
tableAst: mutationLogMetaTable.sqliteDef.ast,
|
199
|
-
skipMetaTable: true,
|
200
|
-
})
|
201
|
-
|
202
|
-
yield* migrateTable({
|
203
|
-
db: dbMutationLog,
|
204
|
-
behaviour: 'create-if-not-exists',
|
205
|
-
tableAst: syncStatusTable.sqliteDef.ast,
|
206
|
-
skipMetaTable: true,
|
207
|
-
})
|
208
|
-
|
209
|
-
// Create sync status row if it doesn't exist
|
210
|
-
yield* execSql(
|
211
|
-
dbMutationLog,
|
212
|
-
sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
|
213
|
-
SELECT ${EventId.ROOT.global}
|
214
|
-
WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
|
215
|
-
{},
|
216
|
-
)
|
217
|
-
|
218
|
-
const dbReady = yield* Deferred.make<void>()
|
219
|
-
|
220
|
-
// We're already starting pulling from the sync backend concurrently but wait until the db is ready before
|
221
|
-
// processing any incoming mutations
|
222
|
-
const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
|
206
|
+
yield* Mutationlog.initMutationLogDb(dbMutationLog)
|
223
207
|
|
224
208
|
let migrationsReport: MigrationsReport
|
225
|
-
if (
|
209
|
+
if (dbReadModelMissing) {
|
226
210
|
const recreateResult = yield* recreateDb
|
227
211
|
migrationsReport = recreateResult.migrationsReport
|
228
212
|
} else {
|
229
213
|
migrationsReport = { migrations: [] }
|
230
214
|
}
|
231
215
|
|
232
|
-
|
216
|
+
// NOTE the sync processor depends on the dbs being initialized properly
|
217
|
+
const { initialLeaderHead } = yield* syncProcessor.boot
|
233
218
|
|
234
219
|
if (initialBlockingSyncContext.blockingDeferred !== undefined) {
|
235
220
|
// Provides a syncing status right away before the first pull response comes in
|
package/src/leader-thread/mod.ts
CHANGED
@@ -1,18 +1,56 @@
|
|
1
|
-
import {
|
1
|
+
import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
2
|
+
import { Effect, Option, Schema } from '@livestore/utils/effect'
|
2
3
|
|
3
4
|
import type { SqliteDb } from '../adapter-types.js'
|
4
5
|
import * as EventId from '../schema/EventId.js'
|
5
|
-
import
|
6
|
-
import {
|
6
|
+
import * as MutationEvent from '../schema/MutationEvent.js'
|
7
|
+
import {
|
8
|
+
MUTATION_LOG_META_TABLE,
|
9
|
+
mutationLogMetaTable,
|
10
|
+
sessionChangesetMetaTable,
|
11
|
+
SYNC_STATUS_TABLE,
|
12
|
+
syncStatusTable,
|
13
|
+
} from '../schema/system-tables.js'
|
14
|
+
import { migrateTable } from '../schema-management/migrations.js'
|
15
|
+
import { insertRow, updateRows } from '../sql-queries/sql-queries.js'
|
16
|
+
import type { PreparedBindValues } from '../util.js'
|
7
17
|
import { prepareBindValues, sql } from '../util.js'
|
18
|
+
import { execSql } from './connection.js'
|
19
|
+
import type { InitialSyncInfo } from './types.js'
|
8
20
|
import { LeaderThreadCtx } from './types.js'
|
9
21
|
|
22
|
+
export const initMutationLogDb = (dbMutationLog: SqliteDb) =>
|
23
|
+
Effect.gen(function* () {
|
24
|
+
yield* migrateTable({
|
25
|
+
db: dbMutationLog,
|
26
|
+
behaviour: 'create-if-not-exists',
|
27
|
+
tableAst: mutationLogMetaTable.sqliteDef.ast,
|
28
|
+
skipMetaTable: true,
|
29
|
+
})
|
30
|
+
|
31
|
+
yield* migrateTable({
|
32
|
+
db: dbMutationLog,
|
33
|
+
behaviour: 'create-if-not-exists',
|
34
|
+
tableAst: syncStatusTable.sqliteDef.ast,
|
35
|
+
skipMetaTable: true,
|
36
|
+
})
|
37
|
+
|
38
|
+
// Create sync status row if it doesn't exist
|
39
|
+
yield* execSql(
|
40
|
+
dbMutationLog,
|
41
|
+
sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
|
42
|
+
SELECT ${EventId.ROOT.global}
|
43
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
|
44
|
+
{},
|
45
|
+
)
|
46
|
+
})
|
47
|
+
|
10
48
|
/** Exclusive of the "since event" */
|
11
49
|
export const getMutationEventsSince = (
|
12
50
|
since: EventId.EventId,
|
13
|
-
): Effect.Effect<ReadonlyArray<MutationEvent.
|
51
|
+
): Effect.Effect<ReadonlyArray<MutationEvent.EncodedWithMeta>, never, LeaderThreadCtx> =>
|
14
52
|
Effect.gen(function* () {
|
15
|
-
const { dbMutationLog } = yield* LeaderThreadCtx
|
53
|
+
const { dbMutationLog, dbReadModel } = yield* LeaderThreadCtx
|
16
54
|
|
17
55
|
const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
|
18
56
|
const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
|
@@ -20,16 +58,44 @@ export const getMutationEventsSince = (
|
|
20
58
|
pendingMutationEventsRaw,
|
21
59
|
)
|
22
60
|
|
61
|
+
const sessionChangesetRows = sessionChangesetMetaTable.query.where('idGlobal', '>=', since.global).asSql()
|
62
|
+
const sessionChangesetRowsRaw = dbReadModel.select(
|
63
|
+
sessionChangesetRows.query,
|
64
|
+
prepareBindValues(sessionChangesetRows.bindValues, sessionChangesetRows.query),
|
65
|
+
)
|
66
|
+
const sessionChangesetRowsDecoded = Schema.decodeUnknownSync(sessionChangesetMetaTable.schema.pipe(Schema.Array))(
|
67
|
+
sessionChangesetRowsRaw,
|
68
|
+
)
|
69
|
+
|
23
70
|
return pendingMutationEvents
|
24
|
-
.map((
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
71
|
+
.map((mutationLogEvent) => {
|
72
|
+
const sessionChangeset = sessionChangesetRowsDecoded.find(
|
73
|
+
(readModelEvent) =>
|
74
|
+
readModelEvent.idGlobal === mutationLogEvent.idGlobal &&
|
75
|
+
readModelEvent.idClient === mutationLogEvent.idClient,
|
76
|
+
)
|
77
|
+
return MutationEvent.EncodedWithMeta.make({
|
78
|
+
mutation: mutationLogEvent.mutation,
|
79
|
+
args: mutationLogEvent.argsJson,
|
80
|
+
id: { global: mutationLogEvent.idGlobal, client: mutationLogEvent.idClient },
|
81
|
+
parentId: { global: mutationLogEvent.parentIdGlobal, client: mutationLogEvent.parentIdClient },
|
82
|
+
clientId: mutationLogEvent.clientId,
|
83
|
+
sessionId: mutationLogEvent.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: mutationLogEvent.syncMetadataJson,
|
94
|
+
},
|
95
|
+
})
|
96
|
+
})
|
32
97
|
.filter((_) => EventId.compare(_.id, since) > 0)
|
98
|
+
.sort((a, b) => EventId.compare(a.id, b.id))
|
33
99
|
})
|
34
100
|
|
35
101
|
export const getClientHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
|
@@ -47,3 +113,90 @@ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEve
|
|
47
113
|
// TODO use prepared statements
|
48
114
|
export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
|
49
115
|
dbMutationLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
|
116
|
+
|
117
|
+
export const insertIntoMutationLog = (
|
118
|
+
mutationEventEncoded: MutationEvent.EncodedWithMeta,
|
119
|
+
dbMutationLog: SqliteDb,
|
120
|
+
mutationDefSchemaHash: number,
|
121
|
+
clientId: string,
|
122
|
+
sessionId: string,
|
123
|
+
) =>
|
124
|
+
Effect.gen(function* () {
|
125
|
+
// Check history consistency during LS_DEV
|
126
|
+
if (LS_DEV && mutationEventEncoded.parentId.global !== EventId.ROOT.global) {
|
127
|
+
const parentMutationExists =
|
128
|
+
dbMutationLog.select<{ count: number }>(
|
129
|
+
`SELECT COUNT(*) as count FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ? AND idClient = ?`,
|
130
|
+
[mutationEventEncoded.parentId.global, mutationEventEncoded.parentId.client] as any as PreparedBindValues,
|
131
|
+
)[0]!.count === 1
|
132
|
+
|
133
|
+
if (parentMutationExists === false) {
|
134
|
+
shouldNeverHappen(
|
135
|
+
`Parent mutation ${mutationEventEncoded.parentId.global},${mutationEventEncoded.parentId.client} does not exist`,
|
136
|
+
)
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
// TODO use prepared statements
|
141
|
+
yield* execSql(
|
142
|
+
dbMutationLog,
|
143
|
+
...insertRow({
|
144
|
+
tableName: MUTATION_LOG_META_TABLE,
|
145
|
+
columns: mutationLogMetaTable.sqliteDef.columns,
|
146
|
+
values: {
|
147
|
+
idGlobal: mutationEventEncoded.id.global,
|
148
|
+
idClient: mutationEventEncoded.id.client,
|
149
|
+
parentIdGlobal: mutationEventEncoded.parentId.global,
|
150
|
+
parentIdClient: mutationEventEncoded.parentId.client,
|
151
|
+
mutation: mutationEventEncoded.mutation,
|
152
|
+
argsJson: mutationEventEncoded.args ?? {},
|
153
|
+
clientId,
|
154
|
+
sessionId,
|
155
|
+
schemaHash: mutationDefSchemaHash,
|
156
|
+
syncMetadataJson: mutationEventEncoded.meta.syncMetadata,
|
157
|
+
},
|
158
|
+
}),
|
159
|
+
)
|
160
|
+
})
|
161
|
+
|
162
|
+
export const updateSyncMetadata = (items: ReadonlyArray<MutationEvent.EncodedWithMeta>) =>
|
163
|
+
Effect.gen(function* () {
|
164
|
+
const { dbMutationLog } = yield* LeaderThreadCtx
|
165
|
+
|
166
|
+
// TODO try to do this in a single query
|
167
|
+
for (let i = 0; i < items.length; i++) {
|
168
|
+
const mutationEvent = items[i]!
|
169
|
+
|
170
|
+
yield* execSql(
|
171
|
+
dbMutationLog,
|
172
|
+
...updateRows({
|
173
|
+
tableName: MUTATION_LOG_META_TABLE,
|
174
|
+
columns: mutationLogMetaTable.sqliteDef.columns,
|
175
|
+
where: { idGlobal: mutationEvent.id.global, idClient: mutationEvent.id.client },
|
176
|
+
updateValues: { syncMetadataJson: mutationEvent.meta.syncMetadata },
|
177
|
+
}),
|
178
|
+
)
|
179
|
+
}
|
180
|
+
})
|
181
|
+
|
182
|
+
export const getSyncBackendCursorInfo = (remoteHead: EventId.GlobalEventId) =>
|
183
|
+
Effect.gen(function* () {
|
184
|
+
const { dbMutationLog } = yield* LeaderThreadCtx
|
185
|
+
|
186
|
+
if (remoteHead === EventId.ROOT.global) return Option.none()
|
187
|
+
|
188
|
+
const MutationlogQuerySchema = Schema.Struct({
|
189
|
+
syncMetadataJson: Schema.parseJson(Schema.Option(Schema.JsonValue)),
|
190
|
+
}).pipe(Schema.pluck('syncMetadataJson'), Schema.Array, Schema.head)
|
191
|
+
|
192
|
+
const syncMetadataOption = yield* Effect.sync(() =>
|
193
|
+
dbMutationLog.select<{ syncMetadataJson: string }>(
|
194
|
+
sql`SELECT syncMetadataJson FROM ${MUTATION_LOG_META_TABLE} WHERE idGlobal = ${remoteHead} ORDER BY idClient ASC LIMIT 1`,
|
195
|
+
),
|
196
|
+
).pipe(Effect.andThen(Schema.decode(MutationlogQuerySchema)), Effect.map(Option.flatten), Effect.orDie)
|
197
|
+
|
198
|
+
return Option.some({
|
199
|
+
cursor: { global: remoteHead, client: EventId.clientDefault },
|
200
|
+
metadata: syncMetadataOption,
|
201
|
+
}) satisfies InitialSyncInfo
|
202
|
+
}).pipe(Effect.withSpan('@livestore/common:mutationlog:getSyncBackendCursorInfo', { attributes: { remoteHead } }))
|
@@ -12,7 +12,7 @@ export const recreateDb: Effect.Effect<
|
|
12
12
|
UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
|
13
13
|
LeaderThreadCtx | HttpClient.HttpClient
|
14
14
|
> = Effect.gen(function* () {
|
15
|
-
const { dbReadModel, dbMutationLog, schema, bootStatusQueue } = yield* LeaderThreadCtx
|
15
|
+
const { dbReadModel, dbMutationLog, schema, bootStatusQueue, applyMutation } = yield* LeaderThreadCtx
|
16
16
|
|
17
17
|
const migrationOptions = schema.migrationOptions
|
18
18
|
let migrationsReport: MigrationsReport
|
@@ -56,10 +56,11 @@ export const recreateDb: Effect.Effect<
|
|
56
56
|
migrationsReport = initResult.migrationsReport
|
57
57
|
|
58
58
|
yield* rehydrateFromMutationLog({
|
59
|
-
db: initResult.tmpDb,
|
60
|
-
|
59
|
+
// db: initResult.tmpDb,
|
60
|
+
dbMutationLog,
|
61
61
|
schema,
|
62
62
|
migrationOptions,
|
63
|
+
applyMutation,
|
63
64
|
onProgress: ({ done, total }) =>
|
64
65
|
Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
|
65
66
|
})
|