@livestore/common 0.3.0-dev.26 → 0.3.0-dev.28
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 +29 -7
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +259 -199
- 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 +15 -3
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +29 -34
- 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 +22 -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 +25 -19
- 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 +47 -71
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +136 -139
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +203 -284
- 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 +457 -351
- 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 +60 -53
- 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 +26 -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 +35 -23
- package/src/sync/sync.ts +6 -9
- package/src/sync/syncstate.test.ts +228 -315
- package/src/sync/syncstate.ts +202 -187
- 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 {
|
@@ -24,6 +23,30 @@ import type {
|
|
24
23
|
} from './types.js'
|
25
24
|
import { LeaderThreadCtx } from './types.js'
|
26
25
|
|
26
|
+
export interface MakeLeaderThreadLayerParams {
|
27
|
+
storeId: string
|
28
|
+
syncPayload: Schema.JsonValue | undefined
|
29
|
+
clientId: string
|
30
|
+
schema: LiveStoreSchema
|
31
|
+
makeSqliteDb: MakeSqliteDb
|
32
|
+
syncOptions: SyncOptions | undefined
|
33
|
+
dbReadModel: LeaderSqliteDb
|
34
|
+
dbMutationLog: LeaderSqliteDb
|
35
|
+
devtoolsOptions: DevtoolsOptions
|
36
|
+
shutdownChannel: ShutdownChannel
|
37
|
+
params?: {
|
38
|
+
localPushBatchSize?: number
|
39
|
+
backendPushBatchSize?: number
|
40
|
+
}
|
41
|
+
testing?: {
|
42
|
+
syncProcessor?: {
|
43
|
+
delays?: {
|
44
|
+
localPushProcessing?: Effect.Effect<void>
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
27
50
|
export const makeLeaderThreadLayer = ({
|
28
51
|
schema,
|
29
52
|
storeId,
|
@@ -35,24 +58,18 @@ export const makeLeaderThreadLayer = ({
|
|
35
58
|
dbMutationLog,
|
36
59
|
devtoolsOptions,
|
37
60
|
shutdownChannel,
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
clientId: string
|
42
|
-
schema: LiveStoreSchema
|
43
|
-
makeSqliteDb: MakeSqliteDb
|
44
|
-
syncOptions: SyncOptions | undefined
|
45
|
-
dbReadModel: LeaderSqliteDb
|
46
|
-
dbMutationLog: LeaderSqliteDb
|
47
|
-
devtoolsOptions: DevtoolsOptions
|
48
|
-
shutdownChannel: ShutdownChannel
|
49
|
-
}): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
|
61
|
+
params,
|
62
|
+
testing,
|
63
|
+
}: MakeLeaderThreadLayerParams): Layer.Layer<LeaderThreadCtx, UnexpectedError, Scope.Scope | HttpClient.HttpClient> =>
|
50
64
|
Effect.gen(function* () {
|
51
65
|
const bootStatusQueue = yield* Queue.unbounded<BootStatus>().pipe(Effect.acquireRelease(Queue.shutdown))
|
52
66
|
|
53
67
|
// TODO do more validation here than just checking the count of tables
|
54
68
|
// Either happens on initial boot or if schema changes
|
55
|
-
const
|
69
|
+
const dbMutationLogMissing =
|
70
|
+
dbMutationLog.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
|
71
|
+
|
72
|
+
const dbReadModelMissing =
|
56
73
|
dbReadModel.select<{ count: number }>(sql`select count(*) as count from sqlite_master`)[0]!.count === 0
|
57
74
|
|
58
75
|
const syncBackend =
|
@@ -60,6 +77,11 @@ export const makeLeaderThreadLayer = ({
|
|
60
77
|
? undefined
|
61
78
|
: yield* syncOptions.backend({ storeId, clientId, payload: syncPayload })
|
62
79
|
|
80
|
+
if (syncBackend !== undefined) {
|
81
|
+
// We're already connecting to the sync backend concurrently
|
82
|
+
yield* syncBackend.connect.pipe(Effect.tapCauseLogPretty, Effect.forkScoped)
|
83
|
+
}
|
84
|
+
|
63
85
|
const initialBlockingSyncContext = yield* makeInitialBlockingSyncContext({
|
64
86
|
initialSyncOptions: syncOptions?.initialSyncOptions ?? { _tag: 'Skip' },
|
65
87
|
bootStatusQueue,
|
@@ -67,11 +89,19 @@ export const makeLeaderThreadLayer = ({
|
|
67
89
|
|
68
90
|
const syncProcessor = yield* makeLeaderSyncProcessor({
|
69
91
|
schema,
|
70
|
-
|
92
|
+
dbMutationLogMissing,
|
71
93
|
dbMutationLog,
|
94
|
+
dbReadModel,
|
95
|
+
dbReadModelMissing,
|
72
96
|
initialBlockingSyncContext,
|
73
|
-
clientId,
|
74
97
|
onError: syncOptions?.onSyncError ?? 'ignore',
|
98
|
+
params: {
|
99
|
+
localPushBatchSize: params?.localPushBatchSize,
|
100
|
+
backendPushBatchSize: params?.backendPushBatchSize,
|
101
|
+
},
|
102
|
+
testing: {
|
103
|
+
delays: testing?.syncProcessor?.delays,
|
104
|
+
},
|
75
105
|
})
|
76
106
|
|
77
107
|
const extraIncomingMessagesQueue = yield* Queue.unbounded<Devtools.Leader.MessageToApp>().pipe(
|
@@ -86,6 +116,8 @@ export const makeLeaderThreadLayer = ({
|
|
86
116
|
}
|
87
117
|
: { enabled: false as const }
|
88
118
|
|
119
|
+
const applyMutation = yield* makeApplyMutation({ schema, dbReadModel, dbMutationLog })
|
120
|
+
|
89
121
|
const ctx = {
|
90
122
|
schema,
|
91
123
|
bootStatusQueue,
|
@@ -99,7 +131,7 @@ export const makeLeaderThreadLayer = ({
|
|
99
131
|
shutdownChannel,
|
100
132
|
syncBackend,
|
101
133
|
syncProcessor,
|
102
|
-
|
134
|
+
applyMutation,
|
103
135
|
extraIncomingMessagesQueue,
|
104
136
|
devtools: devtoolsContext,
|
105
137
|
// State will be set during `bootLeaderThread`
|
@@ -112,7 +144,7 @@ export const makeLeaderThreadLayer = ({
|
|
112
144
|
const layer = Layer.succeed(LeaderThreadCtx, ctx)
|
113
145
|
|
114
146
|
ctx.initialState = yield* bootLeaderThread({
|
115
|
-
|
147
|
+
dbReadModelMissing,
|
116
148
|
initialBlockingSyncContext,
|
117
149
|
devtoolsOptions,
|
118
150
|
}).pipe(Effect.provide(layer))
|
@@ -122,6 +154,7 @@ export const makeLeaderThreadLayer = ({
|
|
122
154
|
Effect.withSpan('@livestore/common:leader-thread:boot'),
|
123
155
|
Effect.withSpanScoped('@livestore/common:leader-thread'),
|
124
156
|
UnexpectedError.mapToUnexpectedError,
|
157
|
+
Effect.tapCauseLogPretty,
|
125
158
|
Layer.unwrapScoped,
|
126
159
|
)
|
127
160
|
|
@@ -177,11 +210,11 @@ const makeInitialBlockingSyncContext = ({
|
|
177
210
|
* It also starts various background processes (e.g. syncing)
|
178
211
|
*/
|
179
212
|
const bootLeaderThread = ({
|
180
|
-
|
213
|
+
dbReadModelMissing,
|
181
214
|
initialBlockingSyncContext,
|
182
215
|
devtoolsOptions,
|
183
216
|
}: {
|
184
|
-
|
217
|
+
dbReadModelMissing: boolean
|
185
218
|
initialBlockingSyncContext: InitialBlockingSyncContext
|
186
219
|
devtoolsOptions: DevtoolsOptions
|
187
220
|
}): Effect.Effect<
|
@@ -192,44 +225,18 @@ const bootLeaderThread = ({
|
|
192
225
|
Effect.gen(function* () {
|
193
226
|
const { dbMutationLog, bootStatusQueue, syncProcessor } = yield* LeaderThreadCtx
|
194
227
|
|
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 })
|
228
|
+
yield* Mutationlog.initMutationLogDb(dbMutationLog)
|
223
229
|
|
224
230
|
let migrationsReport: MigrationsReport
|
225
|
-
if (
|
231
|
+
if (dbReadModelMissing) {
|
226
232
|
const recreateResult = yield* recreateDb
|
227
233
|
migrationsReport = recreateResult.migrationsReport
|
228
234
|
} else {
|
229
235
|
migrationsReport = { migrations: [] }
|
230
236
|
}
|
231
237
|
|
232
|
-
|
238
|
+
// NOTE the sync processor depends on the dbs being initialized properly
|
239
|
+
const { initialLeaderHead } = yield* syncProcessor.boot
|
233
240
|
|
234
241
|
if (initialBlockingSyncContext.blockingDeferred !== undefined) {
|
235
242
|
// 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 } }))
|