@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
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
Option,
|
|
6
6
|
Queue,
|
|
7
7
|
Scope,
|
|
8
|
+
Stream,
|
|
8
9
|
Subscribable,
|
|
9
10
|
SubscriptionRef,
|
|
10
11
|
WebChannel,
|
|
11
12
|
} from '@livestore/utils/effect'
|
|
12
13
|
import { Context, Schema } from '@livestore/utils/effect'
|
|
13
14
|
|
|
15
|
+
import type { SqliteError } from '../adapter-types.js'
|
|
14
16
|
import type {
|
|
15
17
|
BootStatus,
|
|
16
18
|
Devtools,
|
|
@@ -97,7 +99,7 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
|
|
|
97
99
|
devtools: DevtoolsContext
|
|
98
100
|
syncBackend: SyncBackend | undefined
|
|
99
101
|
syncProcessor: LeaderSyncProcessor
|
|
100
|
-
|
|
102
|
+
applyMutation: ApplyMutation
|
|
101
103
|
initialState: {
|
|
102
104
|
leaderHead: EventId.EventId
|
|
103
105
|
migrationsReport: MigrationsReport
|
|
@@ -111,27 +113,42 @@ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
|
|
|
111
113
|
}
|
|
112
114
|
>() {}
|
|
113
115
|
|
|
116
|
+
export type ApplyMutation = (
|
|
117
|
+
mutationEventEncoded: MutationEvent.EncodedWithMeta,
|
|
118
|
+
options?: {
|
|
119
|
+
/** Needed for rehydrateFromMutationLog */
|
|
120
|
+
skipMutationLog?: boolean
|
|
121
|
+
},
|
|
122
|
+
) => Effect.Effect<
|
|
123
|
+
{ sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } },
|
|
124
|
+
SqliteError | UnexpectedError
|
|
125
|
+
>
|
|
126
|
+
|
|
114
127
|
export type InitialBlockingSyncContext = {
|
|
115
128
|
blockingDeferred: Deferred.Deferred<void> | undefined
|
|
116
129
|
update: (_: { remaining: number; processed: number }) => Effect.Effect<void>
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
export type PullQueueItem = {
|
|
120
|
-
payload: SyncState.PayloadUpstream
|
|
121
|
-
remaining: number
|
|
122
|
-
}
|
|
123
|
-
|
|
124
132
|
export interface LeaderSyncProcessor {
|
|
133
|
+
/** Used by client sessions to subscribe to upstream sync state changes */
|
|
134
|
+
pull: (args: {
|
|
135
|
+
/** Leader merge counter */
|
|
136
|
+
cursor: number
|
|
137
|
+
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }, UnexpectedError>
|
|
138
|
+
/** The `pullQueue` API can be used instead of `pull` when more convenient */
|
|
139
|
+
pullQueue: (args: {
|
|
140
|
+
cursor: number
|
|
141
|
+
}) => Effect.Effect<
|
|
142
|
+
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
|
143
|
+
UnexpectedError,
|
|
144
|
+
Scope.Scope
|
|
145
|
+
>
|
|
146
|
+
|
|
147
|
+
/** Used by client sessions to push mutations to the leader thread */
|
|
125
148
|
push: (
|
|
126
149
|
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
|
127
150
|
batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
|
128
151
|
options?: {
|
|
129
|
-
/**
|
|
130
|
-
* This generation number is used to automatically reject subsequent pushes
|
|
131
|
-
* of a previously rejected push from a client session. This might occur in
|
|
132
|
-
* certain concurrent scenarios.
|
|
133
|
-
*/
|
|
134
|
-
// generation: number
|
|
135
152
|
/**
|
|
136
153
|
* If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
|
|
137
154
|
* @default false
|
|
@@ -140,24 +157,18 @@ export interface LeaderSyncProcessor {
|
|
|
140
157
|
},
|
|
141
158
|
) => Effect.Effect<void, LeaderAheadError>
|
|
142
159
|
|
|
160
|
+
/** Currently only used by devtools which don't provide their own event numbers */
|
|
143
161
|
pushPartial: (args: {
|
|
144
162
|
mutationEvent: MutationEvent.PartialAnyEncoded
|
|
145
163
|
clientId: string
|
|
146
164
|
sessionId: string
|
|
147
|
-
}) => Effect.Effect<void, UnexpectedError
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}) => Effect.Effect<
|
|
165
|
+
}) => Effect.Effect<void, UnexpectedError>
|
|
166
|
+
|
|
167
|
+
boot: Effect.Effect<
|
|
151
168
|
{ initialLeaderHead: EventId.EventId },
|
|
152
169
|
UnexpectedError,
|
|
153
170
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
|
154
171
|
>
|
|
155
172
|
syncState: Subscribable.Subscribable<SyncState.SyncState>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export interface PullQueueSet {
|
|
159
|
-
makeQueue: (
|
|
160
|
-
since: EventId.EventId,
|
|
161
|
-
) => Effect.Effect<Queue.Queue<PullQueueItem>, UnexpectedError, Scope.Scope | LeaderThreadCtx>
|
|
162
|
-
offer: (item: PullQueueItem) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
|
|
173
|
+
getMergeCounter: () => number
|
|
163
174
|
}
|
|
@@ -2,35 +2,35 @@ import { memoizeByRef } from '@livestore/utils'
|
|
|
2
2
|
import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
4
|
import { type MigrationOptionsFromMutationLog, type SqliteDb, UnexpectedError } from './adapter-types.js'
|
|
5
|
-
import {
|
|
6
|
-
import type { LiveStoreSchema, MutationDef,
|
|
7
|
-
import { EventId, getMutationDef, MUTATION_LOG_META_TABLE } from './schema/mod.js'
|
|
5
|
+
import type { ApplyMutation } from './leader-thread/mod.js'
|
|
6
|
+
import type { LiveStoreSchema, MutationDef, MutationLogMetaRow } from './schema/mod.js'
|
|
7
|
+
import { EventId, getMutationDef, MUTATION_LOG_META_TABLE, MutationEvent } from './schema/mod.js'
|
|
8
8
|
import type { PreparedBindValues } from './util.js'
|
|
9
9
|
import { sql } from './util.js'
|
|
10
10
|
|
|
11
11
|
export const rehydrateFromMutationLog = ({
|
|
12
|
-
|
|
12
|
+
dbMutationLog,
|
|
13
13
|
// TODO re-use this db when bringing back the boot in-memory db implementation
|
|
14
14
|
// db,
|
|
15
15
|
schema,
|
|
16
16
|
migrationOptions,
|
|
17
17
|
onProgress,
|
|
18
|
+
applyMutation,
|
|
18
19
|
}: {
|
|
19
|
-
|
|
20
|
-
db: SqliteDb
|
|
20
|
+
dbMutationLog: SqliteDb
|
|
21
|
+
// db: SqliteDb
|
|
21
22
|
schema: LiveStoreSchema
|
|
22
23
|
migrationOptions: MigrationOptionsFromMutationLog
|
|
23
24
|
onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
|
|
25
|
+
applyMutation: ApplyMutation
|
|
24
26
|
}) =>
|
|
25
27
|
Effect.gen(function* () {
|
|
26
|
-
const mutationsCount =
|
|
28
|
+
const mutationsCount = dbMutationLog.select<{ count: number }>(
|
|
27
29
|
`SELECT COUNT(*) AS count FROM ${MUTATION_LOG_META_TABLE}`,
|
|
28
30
|
)[0]!.count
|
|
29
31
|
|
|
30
32
|
const hashMutation = memoizeByRef((mutation: MutationDef.Any) => Schema.hash(mutation.schema))
|
|
31
33
|
|
|
32
|
-
const applyMutation = yield* makeApplyMutation
|
|
33
|
-
|
|
34
34
|
const processMutation = (row: MutationLogMetaRow) =>
|
|
35
35
|
Effect.gen(function* () {
|
|
36
36
|
const mutationDef = getMutationDef(schema, row.mutation)
|
|
@@ -59,21 +59,21 @@ This likely means the schema has changed in an incompatible way.
|
|
|
59
59
|
),
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
const mutationEventEncoded = {
|
|
62
|
+
const mutationEventEncoded = MutationEvent.EncodedWithMeta.make({
|
|
63
63
|
id: { global: row.idGlobal, client: row.idClient },
|
|
64
64
|
parentId: { global: row.parentIdGlobal, client: row.parentIdClient },
|
|
65
65
|
mutation: row.mutation,
|
|
66
66
|
args,
|
|
67
67
|
clientId: row.clientId,
|
|
68
68
|
sessionId: row.sessionId,
|
|
69
|
-
}
|
|
69
|
+
})
|
|
70
70
|
|
|
71
71
|
yield* applyMutation(mutationEventEncoded, { skipMutationLog: true })
|
|
72
72
|
}).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
|
|
73
73
|
|
|
74
74
|
const CHUNK_SIZE = 100
|
|
75
75
|
|
|
76
|
-
const stmt =
|
|
76
|
+
const stmt = dbMutationLog.prepare(sql`\
|
|
77
77
|
SELECT * FROM ${MUTATION_LOG_META_TABLE}
|
|
78
78
|
WHERE idGlobal > $idGlobal OR (idGlobal = $idGlobal AND idClient > $idClient)
|
|
79
79
|
ORDER BY idGlobal ASC, idClient ASC
|
package/src/schema/EventId.ts
CHANGED
|
@@ -18,8 +18,15 @@ export const clientDefault = 0 as any as ClientEventId
|
|
|
18
18
|
*/
|
|
19
19
|
export type EventId = { global: GlobalEventId; client: ClientEventId }
|
|
20
20
|
|
|
21
|
+
// export const EventSequenceNumber = Schema.Struct({})
|
|
22
|
+
// export const EventNumber = Schema.Struct({})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* NOTE: Client mutation events with a non-0 client id, won't be synced to the sync backend.
|
|
26
|
+
*/
|
|
21
27
|
export const EventId = Schema.Struct({
|
|
22
28
|
global: GlobalEventId,
|
|
29
|
+
/** Only increments for clientOnly mutations */
|
|
23
30
|
client: ClientEventId,
|
|
24
31
|
}).annotations({ title: 'LiveStore.EventId' })
|
|
25
32
|
|
|
@@ -36,7 +43,7 @@ export const compare = (a: EventId, b: EventId) => {
|
|
|
36
43
|
/**
|
|
37
44
|
* Convert an event id to a string representation.
|
|
38
45
|
*/
|
|
39
|
-
export const toString = (id: EventId) => `
|
|
46
|
+
export const toString = (id: EventId) => (id.client === 0 ? `e${id.global}` : `e${id.global}+${id.client}`)
|
|
40
47
|
|
|
41
48
|
/**
|
|
42
49
|
* Convert a string representation of an event id to an event id.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memoizeByRef } from '@livestore/utils'
|
|
2
|
-
import { Schema } from '@livestore/utils/effect'
|
|
2
|
+
import { Option, Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
4
|
import * as EventId from './EventId.js'
|
|
5
5
|
import type { MutationDef, MutationDefRecord } from './mutations.js'
|
|
@@ -155,16 +155,29 @@ export const makeMutationEventSchemaMemo = memoizeByRef(makeMutationEventSchema)
|
|
|
155
155
|
export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEvent.EncodedWithMeta')({
|
|
156
156
|
mutation: Schema.String,
|
|
157
157
|
args: Schema.Any,
|
|
158
|
+
// TODO rename to `.num` / `.parentNum`
|
|
158
159
|
id: EventId.EventId,
|
|
159
160
|
parentId: EventId.EventId,
|
|
160
161
|
clientId: Schema.String,
|
|
161
162
|
sessionId: Schema.String,
|
|
162
163
|
// TODO get rid of `meta` again by cleaning up the usage implementations
|
|
163
|
-
meta: Schema.
|
|
164
|
-
|
|
165
|
-
sessionChangeset
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
meta: Schema.Struct({
|
|
165
|
+
sessionChangeset: Schema.Union(
|
|
166
|
+
Schema.TaggedStruct('sessionChangeset', {
|
|
167
|
+
data: Schema.Uint8Array,
|
|
168
|
+
debug: Schema.Any.pipe(Schema.optional),
|
|
169
|
+
}),
|
|
170
|
+
Schema.TaggedStruct('no-op', {}),
|
|
171
|
+
Schema.TaggedStruct('unset', {}),
|
|
172
|
+
),
|
|
173
|
+
syncMetadata: Schema.Option(Schema.JsonValue),
|
|
174
|
+
}).pipe(
|
|
175
|
+
Schema.mutable,
|
|
176
|
+
Schema.optional,
|
|
177
|
+
Schema.withDefaults({
|
|
178
|
+
constructor: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
|
|
179
|
+
decoding: () => ({ sessionChangeset: { _tag: 'unset' as const }, syncMetadata: Option.none() }),
|
|
180
|
+
}),
|
|
168
181
|
),
|
|
169
182
|
}) {
|
|
170
183
|
toJSON = (): any => {
|
|
@@ -172,23 +185,41 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
|
172
185
|
// - More readable way to print the id + parentId
|
|
173
186
|
// - not including `meta`, `clientId`, `sessionId`
|
|
174
187
|
return {
|
|
175
|
-
id:
|
|
188
|
+
id: `${EventId.toString(this.id)} → ${EventId.toString(this.parentId)}`,
|
|
176
189
|
mutation: this.mutation,
|
|
177
190
|
args: this.args,
|
|
178
191
|
}
|
|
179
192
|
}
|
|
180
193
|
|
|
181
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Example: (global event)
|
|
196
|
+
* For event id e2 → e1 which should be rebased on event id e3 → e2
|
|
197
|
+
* the resulting event id will be e4 → e3
|
|
198
|
+
*
|
|
199
|
+
* Example: (client event)
|
|
200
|
+
* For event id e2+1 → e2 which should be rebased on event id e3 → e2
|
|
201
|
+
* the resulting event id will be e3+1 → e3
|
|
202
|
+
*
|
|
203
|
+
* Syntax: e2+2 → e2+1
|
|
204
|
+
* ^ ^ ^ ^
|
|
205
|
+
* | | | +- client parent id
|
|
206
|
+
* | | +--- global parent id
|
|
207
|
+
* | +-- client id
|
|
208
|
+
* +---- global id
|
|
209
|
+
* Client id is ommitted for global events
|
|
210
|
+
*/
|
|
211
|
+
rebase = (parentId: EventId.EventId, isClient: boolean) =>
|
|
182
212
|
new EncodedWithMeta({
|
|
183
213
|
...this,
|
|
184
|
-
...EventId.nextPair(parentId,
|
|
214
|
+
...EventId.nextPair(parentId, isClient),
|
|
185
215
|
})
|
|
186
216
|
|
|
187
|
-
static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
|
|
217
|
+
static fromGlobal = (mutationEvent: AnyEncodedGlobal, syncMetadata: Option.Option<Schema.JsonValue>) =>
|
|
188
218
|
new EncodedWithMeta({
|
|
189
219
|
...mutationEvent,
|
|
190
220
|
id: { global: mutationEvent.id, client: EventId.clientDefault },
|
|
191
221
|
parentId: { global: mutationEvent.parentId, client: EventId.clientDefault },
|
|
222
|
+
meta: { sessionChangeset: { _tag: 'unset' as const }, syncMetadata },
|
|
192
223
|
})
|
|
193
224
|
|
|
194
225
|
toGlobal = (): AnyEncodedGlobal => ({
|
|
@@ -198,6 +229,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
|
198
229
|
})
|
|
199
230
|
}
|
|
200
231
|
|
|
232
|
+
/** NOTE `meta` is not considered for equality */
|
|
201
233
|
export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
|
|
202
234
|
a.id.global === b.id.global &&
|
|
203
235
|
a.id.client === b.id.client &&
|
package/src/schema/schema.ts
CHANGED
|
@@ -112,7 +112,7 @@ export const makeSchema = <TInputSchema extends InputSchema>(
|
|
|
112
112
|
_MutationDefMapType: Symbol.for('livestore.MutationDefMapType') as any,
|
|
113
113
|
tables,
|
|
114
114
|
mutations,
|
|
115
|
-
migrationOptions: inputSchema.migrations ?? { strategy: '
|
|
115
|
+
migrationOptions: inputSchema.migrations ?? { strategy: 'from-mutation-log' },
|
|
116
116
|
hash,
|
|
117
117
|
} satisfies LiveStoreSchema
|
|
118
118
|
}
|
|
@@ -60,7 +60,25 @@ export const sessionChangesetMetaTable = table(
|
|
|
60
60
|
|
|
61
61
|
export type SessionChangesetMetaRow = FromTable.RowDecoded<typeof sessionChangesetMetaTable>
|
|
62
62
|
|
|
63
|
-
export const
|
|
63
|
+
export const LEADER_MERGE_COUNTER_TABLE = '__livestore_leader_merge_counter'
|
|
64
|
+
|
|
65
|
+
export const leaderMergeCounterTable = table(
|
|
66
|
+
LEADER_MERGE_COUNTER_TABLE,
|
|
67
|
+
{
|
|
68
|
+
id: SqliteDsl.integer({ primaryKey: true, schema: Schema.Literal(0) }),
|
|
69
|
+
mergeCounter: SqliteDsl.integer({ primaryKey: true }),
|
|
70
|
+
},
|
|
71
|
+
{ disableAutomaticIdColumn: true },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
export type LeaderMergeCounterRow = FromTable.RowDecoded<typeof leaderMergeCounterTable>
|
|
75
|
+
|
|
76
|
+
export const systemTables = [
|
|
77
|
+
schemaMetaTable,
|
|
78
|
+
schemaMutationsMetaTable,
|
|
79
|
+
sessionChangesetMetaTable,
|
|
80
|
+
leaderMergeCounterTable,
|
|
81
|
+
]
|
|
64
82
|
|
|
65
83
|
/// Mutation log DB
|
|
66
84
|
|
|
@@ -72,6 +90,7 @@ export const MUTATION_LOG_META_TABLE = 'mutation_log'
|
|
|
72
90
|
export const mutationLogMetaTable = table(
|
|
73
91
|
MUTATION_LOG_META_TABLE,
|
|
74
92
|
{
|
|
93
|
+
// TODO Adjust modeling so a global event never needs a client id component
|
|
75
94
|
idGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
|
|
76
95
|
idClient: SqliteDsl.integer({ primaryKey: true, schema: EventId.ClientEventId }),
|
|
77
96
|
parentIdGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
|
|
@@ -5,8 +5,9 @@ import * as otel from '@opentelemetry/api'
|
|
|
5
5
|
|
|
6
6
|
import type { ClientSession, UnexpectedError } from '../adapter-types.js'
|
|
7
7
|
import * as EventId from '../schema/EventId.js'
|
|
8
|
-
import { getMutationDef, type LiveStoreSchema } from '../schema/mod.js'
|
|
8
|
+
import { getMutationDef, LEADER_MERGE_COUNTER_TABLE, type LiveStoreSchema } from '../schema/mod.js'
|
|
9
9
|
import * as MutationEvent from '../schema/MutationEvent.js'
|
|
10
|
+
import { sql } from '../util.js'
|
|
10
11
|
import * as SyncState from './syncstate.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -17,6 +18,8 @@ import * as SyncState from './syncstate.js'
|
|
|
17
18
|
* - The goal is to never block the UI, so we'll interrupt rebasing if a new mutations is pushed by the client session.
|
|
18
19
|
* - We also want to avoid "backwards-jumping" in the UI, so we'll transactionally apply a read model changes during a rebase.
|
|
19
20
|
* - We might need to make the rebase behaviour configurable e.g. to let users manually trigger a rebase
|
|
21
|
+
*
|
|
22
|
+
* Longer term we should evalutate whether we can unify the ClientSessionSyncProcessor with the LeaderSyncProcessor.
|
|
20
23
|
*/
|
|
21
24
|
export const makeClientSessionSyncProcessor = ({
|
|
22
25
|
schema,
|
|
@@ -37,7 +40,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
37
40
|
options: { otelContext: otel.Context; withChangeset: boolean },
|
|
38
41
|
) => {
|
|
39
42
|
writeTables: Set<string>
|
|
40
|
-
sessionChangeset: Uint8Array |
|
|
43
|
+
sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } | { _tag: 'unset' }
|
|
41
44
|
}
|
|
42
45
|
rollback: (changeset: Uint8Array) => void
|
|
43
46
|
refreshTables: (tables: Set<string>) => void
|
|
@@ -54,17 +57,17 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
54
57
|
const mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
|
|
55
58
|
|
|
56
59
|
const syncStateRef = {
|
|
60
|
+
// The initial state is identical to the leader's initial state
|
|
57
61
|
current: new SyncState.SyncState({
|
|
58
62
|
localHead: clientSession.leaderThread.initialState.leaderHead,
|
|
59
63
|
upstreamHead: clientSession.leaderThread.initialState.leaderHead,
|
|
64
|
+
// Given we're starting with the leader's snapshot, we don't have any pending mutations intially
|
|
60
65
|
pending: [],
|
|
61
|
-
// TODO init rollbackTail from leader to be ready for backend rebasing
|
|
62
|
-
rollbackTail: [],
|
|
63
66
|
}),
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
const syncStateUpdateQueue = Queue.unbounded<SyncState.SyncState>().pipe(Effect.runSync)
|
|
67
|
-
const
|
|
70
|
+
const isClientEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) =>
|
|
68
71
|
getMutationDef(schema, mutationEventEncoded.mutation).options.clientOnly
|
|
69
72
|
|
|
70
73
|
/** We're queuing push requests to reduce the number of messages sent to the leader by batching them */
|
|
@@ -74,13 +77,14 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
74
77
|
// TODO validate batch
|
|
75
78
|
|
|
76
79
|
let baseEventId = syncStateRef.current.localHead
|
|
77
|
-
const encodedMutationEvents = batch.map((
|
|
78
|
-
const mutationDef = getMutationDef(schema,
|
|
80
|
+
const encodedMutationEvents = batch.map(({ mutation, args }) => {
|
|
81
|
+
const mutationDef = getMutationDef(schema, mutation)
|
|
79
82
|
const nextIdPair = EventId.nextPair(baseEventId, mutationDef.options.clientOnly)
|
|
80
83
|
baseEventId = nextIdPair.id
|
|
81
84
|
return new MutationEvent.EncodedWithMeta(
|
|
82
85
|
Schema.encodeUnknownSync(mutationEventSchema)({
|
|
83
|
-
|
|
86
|
+
mutation,
|
|
87
|
+
args,
|
|
84
88
|
...nextIdPair,
|
|
85
89
|
clientId: clientSession.clientId,
|
|
86
90
|
sessionId: clientSession.sessionId,
|
|
@@ -88,31 +92,31 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
88
92
|
)
|
|
89
93
|
})
|
|
90
94
|
|
|
91
|
-
const
|
|
95
|
+
const mergeResult = SyncState.merge({
|
|
92
96
|
syncState: syncStateRef.current,
|
|
93
97
|
payload: { _tag: 'local-push', newEvents: encodedMutationEvents },
|
|
94
|
-
|
|
98
|
+
isClientEvent,
|
|
95
99
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
96
100
|
})
|
|
97
101
|
|
|
98
|
-
if (
|
|
99
|
-
return shouldNeverHappen('Unexpected error in client-session-sync-processor',
|
|
102
|
+
if (mergeResult._tag === 'unexpected-error') {
|
|
103
|
+
return shouldNeverHappen('Unexpected error in client-session-sync-processor', mergeResult.cause)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
span.addEvent('local-push', {
|
|
103
107
|
batchSize: encodedMutationEvents.length,
|
|
104
|
-
|
|
108
|
+
mergeResult: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
105
109
|
})
|
|
106
110
|
|
|
107
|
-
if (
|
|
108
|
-
return shouldNeverHappen(`Expected advance, got ${
|
|
111
|
+
if (mergeResult._tag !== 'advance') {
|
|
112
|
+
return shouldNeverHappen(`Expected advance, got ${mergeResult._tag}`)
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
syncStateRef.current =
|
|
112
|
-
syncStateUpdateQueue.offer(
|
|
115
|
+
syncStateRef.current = mergeResult.newSyncState
|
|
116
|
+
syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
|
|
113
117
|
|
|
114
118
|
const writeTables = new Set<string>()
|
|
115
|
-
for (const mutationEvent of
|
|
119
|
+
for (const mutationEvent of mergeResult.newEvents) {
|
|
116
120
|
// TODO avoid encoding and decoding here again
|
|
117
121
|
const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
|
|
118
122
|
const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
|
|
@@ -166,38 +170,45 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
166
170
|
|
|
167
171
|
yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
const getMergeCounter = () =>
|
|
174
|
+
clientSession.sqliteDb.select<{ mergeCounter: number }>(
|
|
175
|
+
sql`SELECT mergeCounter FROM ${LEADER_MERGE_COUNTER_TABLE} WHERE id = 0`,
|
|
176
|
+
)[0]?.mergeCounter ?? 0
|
|
177
|
+
|
|
178
|
+
// NOTE We need to lazily call `.pull` as we want the cursor to be updated
|
|
179
|
+
yield* Stream.suspend(() => clientSession.leaderThread.mutations.pull({ cursor: getMergeCounter() })).pipe(
|
|
180
|
+
Stream.tap(({ payload, mergeCounter: leaderMergeCounter }) =>
|
|
171
181
|
Effect.gen(function* () {
|
|
172
|
-
//
|
|
182
|
+
// yield* Effect.logDebug('ClientSessionSyncProcessor:pull', payload)
|
|
183
|
+
|
|
173
184
|
if (clientSession.devtools.enabled) {
|
|
174
185
|
yield* clientSession.devtools.pullLatch.await
|
|
175
186
|
}
|
|
176
187
|
|
|
177
|
-
const
|
|
188
|
+
const mergeResult = SyncState.merge({
|
|
178
189
|
syncState: syncStateRef.current,
|
|
179
190
|
payload,
|
|
180
|
-
|
|
191
|
+
isClientEvent,
|
|
181
192
|
isEqualEvent: MutationEvent.isEqualEncoded,
|
|
182
193
|
})
|
|
183
194
|
|
|
184
|
-
if (
|
|
185
|
-
return yield* Effect.fail(
|
|
186
|
-
} else if (
|
|
187
|
-
return shouldNeverHappen('Unexpected reject in client-session-sync-processor',
|
|
195
|
+
if (mergeResult._tag === 'unexpected-error') {
|
|
196
|
+
return yield* Effect.fail(mergeResult.cause)
|
|
197
|
+
} else if (mergeResult._tag === 'reject') {
|
|
198
|
+
return shouldNeverHappen('Unexpected reject in client-session-sync-processor', mergeResult)
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
syncStateRef.current =
|
|
191
|
-
syncStateUpdateQueue.offer(
|
|
201
|
+
syncStateRef.current = mergeResult.newSyncState
|
|
202
|
+
syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
|
|
192
203
|
|
|
193
|
-
if (
|
|
194
|
-
span.addEvent('pull:rebase', {
|
|
204
|
+
if (mergeResult._tag === 'rebase') {
|
|
205
|
+
span.addEvent('merge:pull:rebase', {
|
|
195
206
|
payloadTag: payload._tag,
|
|
196
207
|
payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
|
|
197
|
-
newEventsCount:
|
|
198
|
-
rollbackCount:
|
|
199
|
-
res: TRACE_VERBOSE ? JSON.stringify(
|
|
200
|
-
|
|
208
|
+
newEventsCount: mergeResult.newEvents.length,
|
|
209
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
|
210
|
+
res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
211
|
+
leaderMergeCounter,
|
|
201
212
|
})
|
|
202
213
|
|
|
203
214
|
debugInfo.rebaseCount++
|
|
@@ -211,37 +222,39 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
211
222
|
|
|
212
223
|
if (LS_DEV) {
|
|
213
224
|
Effect.logDebug(
|
|
214
|
-
'pull:rebase: rollback',
|
|
215
|
-
|
|
216
|
-
...
|
|
225
|
+
'merge:pull:rebase: rollback',
|
|
226
|
+
mergeResult.rollbackEvents.length,
|
|
227
|
+
...mergeResult.rollbackEvents.slice(0, 10).map((_) => _.toJSON()),
|
|
228
|
+
{ leaderMergeCounter },
|
|
217
229
|
).pipe(Effect.provide(runtime), Effect.runSync)
|
|
218
230
|
}
|
|
219
231
|
|
|
220
|
-
for (let i =
|
|
221
|
-
const event =
|
|
222
|
-
if (event.meta.sessionChangeset) {
|
|
223
|
-
rollback(event.meta.sessionChangeset)
|
|
224
|
-
event.meta.sessionChangeset =
|
|
232
|
+
for (let i = mergeResult.rollbackEvents.length - 1; i >= 0; i--) {
|
|
233
|
+
const event = mergeResult.rollbackEvents[i]!
|
|
234
|
+
if (event.meta.sessionChangeset._tag !== 'no-op' && event.meta.sessionChangeset._tag !== 'unset') {
|
|
235
|
+
rollback(event.meta.sessionChangeset.data)
|
|
236
|
+
event.meta.sessionChangeset = { _tag: 'unset' }
|
|
225
237
|
}
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
yield* BucketQueue.offerAll(leaderPushQueue,
|
|
240
|
+
yield* BucketQueue.offerAll(leaderPushQueue, mergeResult.newSyncState.pending)
|
|
229
241
|
} else {
|
|
230
|
-
span.addEvent('pull:advance', {
|
|
242
|
+
span.addEvent('merge:pull:advance', {
|
|
231
243
|
payloadTag: payload._tag,
|
|
232
244
|
payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
|
|
233
|
-
newEventsCount:
|
|
234
|
-
res: TRACE_VERBOSE ? JSON.stringify(
|
|
235
|
-
|
|
245
|
+
newEventsCount: mergeResult.newEvents.length,
|
|
246
|
+
res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
|
247
|
+
leaderMergeCounter,
|
|
236
248
|
})
|
|
237
249
|
|
|
238
250
|
debugInfo.advanceCount++
|
|
239
251
|
}
|
|
240
252
|
|
|
241
|
-
if (
|
|
253
|
+
if (mergeResult.newEvents.length === 0) return
|
|
242
254
|
|
|
243
255
|
const writeTables = new Set<string>()
|
|
244
|
-
for (const mutationEvent of
|
|
256
|
+
for (const mutationEvent of mergeResult.newEvents) {
|
|
257
|
+
// TODO apply changeset if available (will require tracking of write tables as well)
|
|
245
258
|
const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
|
|
246
259
|
const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
|
|
247
260
|
for (const table of res.writeTables) {
|
|
@@ -259,6 +272,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
|
259
272
|
),
|
|
260
273
|
Stream.runDrain,
|
|
261
274
|
Effect.forever, // NOTE Whenever the leader changes, we need to re-start the stream
|
|
275
|
+
Effect.interruptible,
|
|
262
276
|
Effect.withSpan('client-session-sync-processor:pull'),
|
|
263
277
|
Effect.tapCauseLogPretty,
|
|
264
278
|
Effect.forkScoped,
|
package/src/sync/sync.ts
CHANGED
|
@@ -19,6 +19,16 @@ export type SyncOptions = {
|
|
|
19
19
|
backend?: SyncBackendConstructor<any>
|
|
20
20
|
/** @default { _tag: 'Skip' } */
|
|
21
21
|
initialSyncOptions?: InitialSyncOptions
|
|
22
|
+
/**
|
|
23
|
+
* What to do if there is an error during sync.
|
|
24
|
+
*
|
|
25
|
+
* Options:
|
|
26
|
+
* `shutdown` will stop the sync processor and cause the app to crash.
|
|
27
|
+
* `ignore` will log the error and let the app continue running acting as if it was offline.
|
|
28
|
+
*
|
|
29
|
+
* @default 'ignore'
|
|
30
|
+
* */
|
|
31
|
+
onSyncError?: 'shutdown' | 'ignore'
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
|
|
@@ -26,6 +36,10 @@ export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
|
|
|
26
36
|
) => Effect.Effect<SyncBackend<TSyncMetadata>, UnexpectedError, Scope.Scope | HttpClient.HttpClient>
|
|
27
37
|
|
|
28
38
|
export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
|
|
39
|
+
/**
|
|
40
|
+
* Can be implemented to prepare a connection to the sync backend to speed up the first pull/push.
|
|
41
|
+
*/
|
|
42
|
+
connect: Effect.Effect<void, IsOfflineError | UnexpectedError, HttpClient.HttpClient | Scope.Scope>
|
|
29
43
|
pull: (
|
|
30
44
|
args: Option.Option<{
|
|
31
45
|
cursor: EventId.EventId
|
|
@@ -50,17 +64,10 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
|
|
|
50
64
|
* - event ids must be in ascending order
|
|
51
65
|
* */
|
|
52
66
|
batch: ReadonlyArray<MutationEvent.AnyEncodedGlobal>,
|
|
53
|
-
) => Effect.Effect<
|
|
54
|
-
{
|
|
55
|
-
/** Indexes are relative to `batch` */
|
|
56
|
-
metadata: ReadonlyArray<Option.Option<TSyncMetadata>>
|
|
57
|
-
},
|
|
58
|
-
IsOfflineError | InvalidPushError,
|
|
59
|
-
HttpClient.HttpClient
|
|
60
|
-
>
|
|
67
|
+
) => Effect.Effect<void, IsOfflineError | InvalidPushError, HttpClient.HttpClient>
|
|
61
68
|
isConnected: SubscriptionRef.SubscriptionRef<boolean>
|
|
62
69
|
/**
|
|
63
|
-
* Metadata describing the sync backend.
|
|
70
|
+
* Metadata describing the sync backend. (Currently only used by devtools.)
|
|
64
71
|
*/
|
|
65
72
|
metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
|
|
66
73
|
}
|