@livestore/common 0.0.0-snapshot-f6ec49b1a18859aad769f0a0d8edf8bae231ed07 → 0.0.0-snapshot-2ef046b02334f52613d31dbe06af53487685edc0
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 +7 -12
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +1 -7
- 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 +16 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +227 -215
- 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.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +2 -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 +22 -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 +20 -3
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/mutationlog.js +106 -12
- package/dist/leader-thread/mutationlog.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 +35 -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 +4 -0
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +7 -1
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts +87 -18
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +35 -6
- 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 +11 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +54 -47
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/sync.d.ts +16 -5
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +81 -83
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +159 -125
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +97 -138
- 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 +5 -12
- package/src/devtools/devtools-messages-common.ts +9 -0
- package/src/devtools/devtools-messages-leader.ts +1 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +398 -370
- package/src/leader-thread/apply-mutation.ts +81 -71
- package/src/leader-thread/leader-worker-devtools.ts +3 -8
- package/src/leader-thread/make-leader-thread-layer.ts +27 -41
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/mutationlog.ts +167 -13
- package/src/leader-thread/recreate-db.ts +4 -3
- package/src/leader-thread/types.ts +34 -23
- package/src/rehydrate-from-mutationlog.ts +12 -12
- package/src/schema/EventId.ts +8 -1
- package/src/schema/MutationEvent.ts +42 -10
- package/src/schema/schema.ts +1 -1
- package/src/schema/system-tables.ts +20 -1
- package/src/sync/ClientSessionSyncProcessor.ts +64 -50
- package/src/sync/sync.ts +16 -9
- package/src/sync/syncstate.test.ts +173 -217
- package/src/sync/syncstate.ts +184 -151
- package/src/version.ts +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts +0 -7
- package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
- package/dist/leader-thread/pull-queue-set.js +0 -48
- package/dist/leader-thread/pull-queue-set.js.map +0 -1
- 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,8 @@ export const bootDevtools = (options: DevtoolsOptions) =>
|
|
|
33
33
|
Effect.ignoreLogged,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// TODO close queue when devtools disconnects
|
|
39
|
-
const pullQueue = yield* connectedClientSessionPullQueues.makeQueue(localHead)
|
|
40
|
-
|
|
41
|
-
yield* Stream.fromQueue(pullQueue).pipe(
|
|
42
|
-
Stream.tap((msg) => sendMessage(Devtools.Leader.SyncPull.make({ payload: msg.payload, liveStoreVersion }))),
|
|
36
|
+
yield* syncProcessor.pull({ cursor: syncProcessor.getMergeCounter() }).pipe(
|
|
37
|
+
Stream.tap(({ payload }) => sendMessage(Devtools.Leader.SyncPull.make({ payload, liveStoreVersion }))),
|
|
43
38
|
Stream.runDrain,
|
|
44
39
|
Effect.forkScoped,
|
|
45
40
|
)
|
|
@@ -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,12 @@ 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
|
-
|
|
82
|
+
onError: syncOptions?.onSyncError ?? 'ignore',
|
|
74
83
|
})
|
|
75
84
|
|
|
76
85
|
const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
|
|
@@ -85,6 +94,8 @@ export const makeLeaderThreadLayer = ({
|
|
|
85
94
|
}
|
|
86
95
|
: { enabled: false as const }
|
|
87
96
|
|
|
97
|
+
const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
|
|
98
|
+
|
|
88
99
|
const ctx = {
|
|
89
100
|
schema,
|
|
90
101
|
bootStatusQueue,
|
|
@@ -98,7 +109,7 @@ export const makeLeaderThreadLayer = ({
|
|
|
98
109
|
shutdownChannel,
|
|
99
110
|
syncBackend,
|
|
100
111
|
syncProcessor,
|
|
101
|
-
|
|
112
|
+
applyMutation,
|
|
102
113
|
extraIncomingMessagesQueue,
|
|
103
114
|
devtools: devtoolsContext,
|
|
104
115
|
// State will be set during `bootLeaderThread`
|
|
@@ -111,7 +122,7 @@ export const makeLeaderThreadLayer = ({
|
|
|
111
122
|
const layer = Layer.succeed(LeaderThreadCtx, ctx)
|
|
112
123
|
|
|
113
124
|
ctx.initialState = yield* bootLeaderThread({
|
|
114
|
-
|
|
125
|
+
dbReadModelMissing,
|
|
115
126
|
initialBlockingSyncContext,
|
|
116
127
|
devtoolsOptions,
|
|
117
128
|
}).pipe(Effect.provide(layer))
|
|
@@ -121,6 +132,7 @@ export const makeLeaderThreadLayer = ({
|
|
|
121
132
|
Effect.withSpan('@livestore/common:leader-thread:boot'),
|
|
122
133
|
Effect.withSpanScoped('@livestore/common:leader-thread'),
|
|
123
134
|
UnexpectedError.mapToUnexpectedError,
|
|
135
|
+
Effect.tapCauseLogPretty,
|
|
124
136
|
Layer.unwrapScoped,
|
|
125
137
|
)
|
|
126
138
|
|
|
@@ -176,11 +188,11 @@ const makeInitialBlockingSyncContext = ({
|
|
|
176
188
|
* It also starts various background processes (e.g. syncing)
|
|
177
189
|
*/
|
|
178
190
|
const bootLeaderThread = ({
|
|
179
|
-
|
|
191
|
+
dbReadModelMissing,
|
|
180
192
|
initialBlockingSyncContext,
|
|
181
193
|
devtoolsOptions,
|
|
182
194
|
}: {
|
|
183
|
-
|
|
195
|
+
dbReadModelMissing: boolean
|
|
184
196
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
|
185
197
|
devtoolsOptions: DevtoolsOptions
|
|
186
198
|
}): Effect.Effect<
|
|
@@ -191,44 +203,18 @@ const bootLeaderThread = ({
|
|
|
191
203
|
Effect.gen(function* () {
|
|
192
204
|
const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
|
193
205
|
|
|
194
|
-
yield*
|
|
195
|
-
db: dbMutationLog,
|
|
196
|
-
behaviour: 'create-if-not-exists',
|
|
197
|
-
tableAst: mutationLogMetaTable.sqliteDef.ast,
|
|
198
|
-
skipMetaTable: true,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
yield* migrateTable({
|
|
202
|
-
db: dbMutationLog,
|
|
203
|
-
behaviour: 'create-if-not-exists',
|
|
204
|
-
tableAst: syncStatusTable.sqliteDef.ast,
|
|
205
|
-
skipMetaTable: true,
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// Create sync status row if it doesn't exist
|
|
209
|
-
yield* execSql(
|
|
210
|
-
dbMutationLog,
|
|
211
|
-
sql`INSERT INTO ${SYNC_STATUS_TABLE} (head)
|
|
212
|
-
SELECT ${EventId.ROOT.global}
|
|
213
|
-
WHERE NOT EXISTS (SELECT 1 FROM ${SYNC_STATUS_TABLE})`,
|
|
214
|
-
{},
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
const dbReady = yield* Deferred.make<void>()
|
|
218
|
-
|
|
219
|
-
// We're already starting pulling from the sync backend concurrently but wait until the db is ready before
|
|
220
|
-
// processing any incoming mutations
|
|
221
|
-
const { initialLeaderHead } = yield* syncProcessor.boot({ dbReady })
|
|
206
|
+
yield* Mutationlog.initMutationLogDb(dbMutationLog)
|
|
222
207
|
|
|
223
208
|
let migrationsReport: MigrationsReport
|
|
224
|
-
if (
|
|
209
|
+
if (dbReadModelMissing) {
|
|
225
210
|
const recreateResult = yield* recreateDb
|
|
226
211
|
migrationsReport = recreateResult.migrationsReport
|
|
227
212
|
} else {
|
|
228
213
|
migrationsReport = { migrations: [] }
|
|
229
214
|
}
|
|
230
215
|
|
|
231
|
-
|
|
216
|
+
// NOTE the sync processor depends on the dbs being initialized properly
|
|
217
|
+
const { initialLeaderHead } = yield* syncProcessor.boot
|
|
232
218
|
|
|
233
219
|
if (initialBlockingSyncContext.blockingDeferred !== undefined) {
|
|
234
220
|
// Provides a syncing status right away before the first pull response comes in
|
package/src/leader-thread/mod.ts
CHANGED
|
@@ -1,17 +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
|
+
|
|
48
|
+
/** Exclusive of the "since event" */
|
|
10
49
|
export const getMutationEventsSince = (
|
|
11
50
|
since: EventId.EventId,
|
|
12
|
-
): Effect.Effect<ReadonlyArray<MutationEvent.
|
|
51
|
+
): Effect.Effect<ReadonlyArray<MutationEvent.EncodedWithMeta>, never, LeaderThreadCtx> =>
|
|
13
52
|
Effect.gen(function* () {
|
|
14
|
-
const { dbMutationLog } = yield* LeaderThreadCtx
|
|
53
|
+
const { dbMutationLog, dbReadModel } = yield* LeaderThreadCtx
|
|
15
54
|
|
|
16
55
|
const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
|
|
17
56
|
const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
|
|
@@ -19,16 +58,44 @@ export const getMutationEventsSince = (
|
|
|
19
58
|
pendingMutationEventsRaw,
|
|
20
59
|
)
|
|
21
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
|
+
|
|
22
70
|
return pendingMutationEvents
|
|
23
|
-
.map((
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
})
|
|
31
97
|
.filter((_) => EventId.compare(_.id, since) > 0)
|
|
98
|
+
.sort((a, b) => EventId.compare(a.id, b.id))
|
|
32
99
|
})
|
|
33
100
|
|
|
34
101
|
export const getClientHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
|
|
@@ -46,3 +113,90 @@ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEve
|
|
|
46
113
|
// TODO use prepared statements
|
|
47
114
|
export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
|
|
48
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
|
})
|