@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
@@ -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
|
})
|
@@ -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 { LeaderPullCursor, 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,41 @@ 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
|
+
cursor: LeaderPullCursor
|
136
|
+
}) => Stream.Stream<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }, UnexpectedError>
|
137
|
+
/** The `pullQueue` API can be used instead of `pull` when more convenient */
|
138
|
+
pullQueue: (args: {
|
139
|
+
cursor: LeaderPullCursor
|
140
|
+
}) => Effect.Effect<
|
141
|
+
Queue.Queue<{ payload: typeof SyncState.PayloadUpstream.Type; mergeCounter: number }>,
|
142
|
+
UnexpectedError,
|
143
|
+
Scope.Scope
|
144
|
+
>
|
145
|
+
|
146
|
+
/** Used by client sessions to push mutations to the leader thread */
|
125
147
|
push: (
|
126
148
|
/** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
|
127
149
|
batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
|
128
150
|
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
151
|
/**
|
136
152
|
* If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
|
137
153
|
* @default false
|
@@ -140,24 +156,18 @@ export interface LeaderSyncProcessor {
|
|
140
156
|
},
|
141
157
|
) => Effect.Effect<void, LeaderAheadError>
|
142
158
|
|
159
|
+
/** Currently only used by devtools which don't provide their own event numbers */
|
143
160
|
pushPartial: (args: {
|
144
161
|
mutationEvent: MutationEvent.PartialAnyEncoded
|
145
162
|
clientId: string
|
146
163
|
sessionId: string
|
147
|
-
}) => Effect.Effect<void, UnexpectedError
|
148
|
-
|
149
|
-
|
150
|
-
}) => Effect.Effect<
|
164
|
+
}) => Effect.Effect<void, UnexpectedError>
|
165
|
+
|
166
|
+
boot: Effect.Effect<
|
151
167
|
{ initialLeaderHead: EventId.EventId },
|
152
168
|
UnexpectedError,
|
153
169
|
LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient
|
154
170
|
>
|
155
171
|
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>
|
172
|
+
getMergeCounter: () => number
|
163
173
|
}
|
@@ -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,9 +18,22 @@ 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
|
+
// export const ClientEventNumber = Schema.Struct({})
|
24
|
+
// export const GlobalEventNumber = Schema.Struct({})
|
25
|
+
|
26
|
+
/**
|
27
|
+
* NOTE: Client mutation events with a non-0 client id, won't be synced to the sync backend.
|
28
|
+
*/
|
21
29
|
export const EventId = Schema.Struct({
|
22
30
|
global: GlobalEventId,
|
31
|
+
/** Only increments for clientOnly mutations */
|
23
32
|
client: ClientEventId,
|
33
|
+
|
34
|
+
// TODO: actually add this field
|
35
|
+
// Client only
|
36
|
+
// generation: Schema.Number.pipe(Schema.optional),
|
24
37
|
}).annotations({ title: 'LiveStore.EventId' })
|
25
38
|
|
26
39
|
/**
|
@@ -36,7 +49,7 @@ export const compare = (a: EventId, b: EventId) => {
|
|
36
49
|
/**
|
37
50
|
* Convert an event id to a string representation.
|
38
51
|
*/
|
39
|
-
export const toString = (id: EventId) => `
|
52
|
+
export const toString = (id: EventId) => (id.client === 0 ? `e${id.global}` : `e${id.global}+${id.client}`)
|
40
53
|
|
41
54
|
/**
|
42
55
|
* Convert a string representation of an event id to an event id.
|
@@ -53,7 +66,7 @@ export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.cl
|
|
53
66
|
|
54
67
|
export type EventIdPair = { id: EventId; parentId: EventId }
|
55
68
|
|
56
|
-
export const ROOT = { global:
|
69
|
+
export const ROOT = { global: 0 as any as GlobalEventId, client: clientDefault } satisfies EventId
|
57
70
|
|
58
71
|
export const isGreaterThan = (a: EventId, b: EventId) => {
|
59
72
|
return a.global > b.global || (a.global === b.global && a.client > b.client)
|
@@ -63,6 +76,17 @@ export const isGreaterThanOrEqual = (a: EventId, b: EventId) => {
|
|
63
76
|
return a.global > b.global || (a.global === b.global && a.client >= b.client)
|
64
77
|
}
|
65
78
|
|
79
|
+
export const max = (a: EventId, b: EventId) => {
|
80
|
+
return a.global > b.global || (a.global === b.global && a.client > b.client) ? a : b
|
81
|
+
}
|
82
|
+
|
83
|
+
export const diff = (a: EventId, b: EventId) => {
|
84
|
+
return {
|
85
|
+
global: a.global - b.global,
|
86
|
+
client: a.client - b.client,
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
66
90
|
export const make = (id: EventId | typeof EventId.Encoded): EventId => {
|
67
91
|
return Schema.is(EventId)(id) ? id : Schema.decodeSync(EventId)(id)
|
68
92
|
}
|
@@ -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,7 +185,7 @@ 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)} (${this.clientId}, ${this.sessionId})`,
|
176
189
|
mutation: this.mutation,
|
177
190
|
args: this.args,
|
178
191
|
}
|
@@ -180,19 +193,20 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
180
193
|
|
181
194
|
/**
|
182
195
|
* Example: (global event)
|
183
|
-
* For event id
|
184
|
-
* the resulting event id will be
|
196
|
+
* For event id e2 → e1 which should be rebased on event id e3 → e2
|
197
|
+
* the resulting event id will be e4 → e3
|
185
198
|
*
|
186
199
|
* Example: (client event)
|
187
|
-
* For event id
|
188
|
-
* the resulting event id will be
|
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
|
189
202
|
*
|
190
|
-
* Syntax:
|
191
|
-
* ^ ^
|
192
|
-
* | |
|
193
|
-
* | |
|
203
|
+
* Syntax: e2+2 → e2+1
|
204
|
+
* ^ ^ ^ ^
|
205
|
+
* | | | +- client parent id
|
206
|
+
* | | +--- global parent id
|
194
207
|
* | +-- client id
|
195
208
|
* +---- global id
|
209
|
+
* Client id is ommitted for global events
|
196
210
|
*/
|
197
211
|
rebase = (parentId: EventId.EventId, isClient: boolean) =>
|
198
212
|
new EncodedWithMeta({
|
@@ -200,11 +214,12 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
200
214
|
...EventId.nextPair(parentId, isClient),
|
201
215
|
})
|
202
216
|
|
203
|
-
static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
|
217
|
+
static fromGlobal = (mutationEvent: AnyEncodedGlobal, syncMetadata: Option.Option<Schema.JsonValue>) =>
|
204
218
|
new EncodedWithMeta({
|
205
219
|
...mutationEvent,
|
206
220
|
id: { global: mutationEvent.id, client: EventId.clientDefault },
|
207
221
|
parentId: { global: mutationEvent.parentId, client: EventId.clientDefault },
|
222
|
+
meta: { sessionChangeset: { _tag: 'unset' as const }, syncMetadata },
|
208
223
|
})
|
209
224
|
|
210
225
|
toGlobal = (): AnyEncodedGlobal => ({
|
@@ -214,6 +229,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
|
|
214
229
|
})
|
215
230
|
}
|
216
231
|
|
232
|
+
/** NOTE `meta` is not considered for equality */
|
217
233
|
export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
|
218
234
|
a.id.global === b.id.global &&
|
219
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
|
/**
|
@@ -39,7 +40,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
39
40
|
options: { otelContext: otel.Context; withChangeset: boolean },
|
40
41
|
) => {
|
41
42
|
writeTables: Set<string>
|
42
|
-
sessionChangeset: Uint8Array |
|
43
|
+
sessionChangeset: { _tag: 'sessionChangeset'; data: Uint8Array; debug: any } | { _tag: 'no-op' } | { _tag: 'unset' }
|
43
44
|
}
|
44
45
|
rollback: (changeset: Uint8Array) => void
|
45
46
|
refreshTables: (tables: Set<string>) => void
|
@@ -56,12 +57,12 @@ export const makeClientSessionSyncProcessor = ({
|
|
56
57
|
const mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
|
57
58
|
|
58
59
|
const syncStateRef = {
|
60
|
+
// The initial state is identical to the leader's initial state
|
59
61
|
current: new SyncState.SyncState({
|
60
62
|
localHead: clientSession.leaderThread.initialState.leaderHead,
|
61
63
|
upstreamHead: clientSession.leaderThread.initialState.leaderHead,
|
64
|
+
// Given we're starting with the leader's snapshot, we don't have any pending mutations intially
|
62
65
|
pending: [],
|
63
|
-
// TODO init rollbackTail from leader to be ready for backend rebasing
|
64
|
-
rollbackTail: [],
|
65
66
|
}),
|
66
67
|
}
|
67
68
|
|
@@ -76,13 +77,14 @@ export const makeClientSessionSyncProcessor = ({
|
|
76
77
|
// TODO validate batch
|
77
78
|
|
78
79
|
let baseEventId = syncStateRef.current.localHead
|
79
|
-
const encodedMutationEvents = batch.map((
|
80
|
-
const mutationDef = getMutationDef(schema,
|
80
|
+
const encodedMutationEvents = batch.map(({ mutation, args }) => {
|
81
|
+
const mutationDef = getMutationDef(schema, mutation)
|
81
82
|
const nextIdPair = EventId.nextPair(baseEventId, mutationDef.options.clientOnly)
|
82
83
|
baseEventId = nextIdPair.id
|
83
84
|
return new MutationEvent.EncodedWithMeta(
|
84
85
|
Schema.encodeUnknownSync(mutationEventSchema)({
|
85
|
-
|
86
|
+
mutation,
|
87
|
+
args,
|
86
88
|
...nextIdPair,
|
87
89
|
clientId: clientSession.clientId,
|
88
90
|
sessionId: clientSession.sessionId,
|
@@ -168,13 +170,21 @@ export const makeClientSessionSyncProcessor = ({
|
|
168
170
|
|
169
171
|
yield* FiberHandle.run(leaderPushingFiberHandle, backgroundLeaderPushing)
|
170
172
|
|
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
|
+
|
171
178
|
// NOTE We need to lazily call `.pull` as we want the cursor to be updated
|
172
179
|
yield* Stream.suspend(() =>
|
173
|
-
clientSession.leaderThread.mutations.pull({
|
180
|
+
clientSession.leaderThread.mutations.pull({
|
181
|
+
cursor: { mergeCounter: getMergeCounter(), eventId: syncStateRef.current.localHead },
|
182
|
+
}),
|
174
183
|
).pipe(
|
175
|
-
Stream.tap(({ payload,
|
184
|
+
Stream.tap(({ payload, mergeCounter: leaderMergeCounter }) =>
|
176
185
|
Effect.gen(function* () {
|
177
|
-
//
|
186
|
+
// yield* Effect.logDebug('ClientSessionSyncProcessor:pull', payload)
|
187
|
+
|
178
188
|
if (clientSession.devtools.enabled) {
|
179
189
|
yield* clientSession.devtools.pullLatch.await
|
180
190
|
}
|
@@ -196,13 +206,13 @@ export const makeClientSessionSyncProcessor = ({
|
|
196
206
|
syncStateUpdateQueue.offer(mergeResult.newSyncState).pipe(Effect.runSync)
|
197
207
|
|
198
208
|
if (mergeResult._tag === 'rebase') {
|
199
|
-
span.addEvent('pull:rebase', {
|
209
|
+
span.addEvent('merge:pull:rebase', {
|
200
210
|
payloadTag: payload._tag,
|
201
211
|
payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
|
202
212
|
newEventsCount: mergeResult.newEvents.length,
|
203
|
-
rollbackCount: mergeResult.
|
213
|
+
rollbackCount: mergeResult.rollbackEvents.length,
|
204
214
|
res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
205
|
-
|
215
|
+
leaderMergeCounter,
|
206
216
|
})
|
207
217
|
|
208
218
|
debugInfo.rebaseCount++
|
@@ -216,28 +226,29 @@ export const makeClientSessionSyncProcessor = ({
|
|
216
226
|
|
217
227
|
if (LS_DEV) {
|
218
228
|
Effect.logDebug(
|
219
|
-
'pull:rebase: rollback',
|
220
|
-
mergeResult.
|
221
|
-
...mergeResult.
|
229
|
+
'merge:pull:rebase: rollback',
|
230
|
+
mergeResult.rollbackEvents.length,
|
231
|
+
...mergeResult.rollbackEvents.slice(0, 10).map((_) => _.toJSON()),
|
232
|
+
{ leaderMergeCounter },
|
222
233
|
).pipe(Effect.provide(runtime), Effect.runSync)
|
223
234
|
}
|
224
235
|
|
225
|
-
for (let i = mergeResult.
|
226
|
-
const event = mergeResult.
|
227
|
-
if (event.meta.sessionChangeset) {
|
228
|
-
rollback(event.meta.sessionChangeset)
|
229
|
-
event.meta.sessionChangeset =
|
236
|
+
for (let i = mergeResult.rollbackEvents.length - 1; i >= 0; i--) {
|
237
|
+
const event = mergeResult.rollbackEvents[i]!
|
238
|
+
if (event.meta.sessionChangeset._tag !== 'no-op' && event.meta.sessionChangeset._tag !== 'unset') {
|
239
|
+
rollback(event.meta.sessionChangeset.data)
|
240
|
+
event.meta.sessionChangeset = { _tag: 'unset' }
|
230
241
|
}
|
231
242
|
}
|
232
243
|
|
233
244
|
yield* BucketQueue.offerAll(leaderPushQueue, mergeResult.newSyncState.pending)
|
234
245
|
} else {
|
235
|
-
span.addEvent('pull:advance', {
|
246
|
+
span.addEvent('merge:pull:advance', {
|
236
247
|
payloadTag: payload._tag,
|
237
248
|
payload: TRACE_VERBOSE ? JSON.stringify(payload) : undefined,
|
238
249
|
newEventsCount: mergeResult.newEvents.length,
|
239
250
|
res: TRACE_VERBOSE ? JSON.stringify(mergeResult) : undefined,
|
240
|
-
|
251
|
+
leaderMergeCounter,
|
241
252
|
})
|
242
253
|
|
243
254
|
debugInfo.advanceCount++
|
@@ -247,6 +258,7 @@ export const makeClientSessionSyncProcessor = ({
|
|
247
258
|
|
248
259
|
const writeTables = new Set<string>()
|
249
260
|
for (const mutationEvent of mergeResult.newEvents) {
|
261
|
+
// TODO apply changeset if available (will require tracking of write tables as well)
|
250
262
|
const decodedMutationEvent = Schema.decodeSync(mutationEventSchema)(mutationEvent)
|
251
263
|
const res = applyMutation(decodedMutationEvent, { otelContext, withChangeset: true })
|
252
264
|
for (const table of res.writeTables) {
|
package/src/sync/sync.ts
CHANGED
@@ -36,6 +36,10 @@ export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
|
|
36
36
|
) => Effect.Effect<SyncBackend<TSyncMetadata>, UnexpectedError, Scope.Scope | HttpClient.HttpClient>
|
37
37
|
|
38
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>
|
39
43
|
pull: (
|
40
44
|
args: Option.Option<{
|
41
45
|
cursor: EventId.EventId
|
@@ -60,17 +64,10 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
|
|
60
64
|
* - event ids must be in ascending order
|
61
65
|
* */
|
62
66
|
batch: ReadonlyArray<MutationEvent.AnyEncodedGlobal>,
|
63
|
-
) => Effect.Effect<
|
64
|
-
{
|
65
|
-
/** Indexes are relative to `batch` */
|
66
|
-
metadata: ReadonlyArray<Option.Option<TSyncMetadata>>
|
67
|
-
},
|
68
|
-
IsOfflineError | InvalidPushError,
|
69
|
-
HttpClient.HttpClient
|
70
|
-
>
|
67
|
+
) => Effect.Effect<void, IsOfflineError | InvalidPushError, HttpClient.HttpClient>
|
71
68
|
isConnected: SubscriptionRef.SubscriptionRef<boolean>
|
72
69
|
/**
|
73
|
-
* Metadata describing the sync backend.
|
70
|
+
* Metadata describing the sync backend. (Currently only used by devtools.)
|
74
71
|
*/
|
75
72
|
metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
|
76
73
|
}
|