@livestore/common 0.3.0-dev.1 → 0.3.0-dev.10

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.
Files changed (146) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +26 -23
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js.map +1 -1
  5. package/dist/derived-mutations.d.ts +4 -4
  6. package/dist/derived-mutations.d.ts.map +1 -1
  7. package/dist/derived-mutations.test.js.map +1 -1
  8. package/dist/devtools/devtools-bridge.d.ts +2 -1
  9. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  10. package/dist/devtools/devtools-messages.d.ts +98 -110
  11. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  12. package/dist/devtools/devtools-messages.js +9 -6
  13. package/dist/devtools/devtools-messages.js.map +1 -1
  14. package/dist/index.d.ts +0 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/leader-thread/LeaderSyncProcessor.d.ts +37 -0
  17. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
  18. package/dist/leader-thread/LeaderSyncProcessor.js +417 -0
  19. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
  20. package/dist/leader-thread/apply-mutation.d.ts +5 -2
  21. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  22. package/dist/leader-thread/apply-mutation.js +38 -26
  23. package/dist/leader-thread/apply-mutation.js.map +1 -1
  24. package/dist/leader-thread/leader-sync-processor.d.ts +2 -2
  25. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
  26. package/dist/leader-thread/leader-sync-processor.js +20 -12
  27. package/dist/leader-thread/leader-sync-processor.js.map +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  29. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  30. package/dist/leader-thread/leader-worker-devtools.js +22 -66
  31. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.d.ts +8 -7
  33. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.js +11 -5
  35. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  36. package/dist/leader-thread/mutationlog.d.ts +4 -17
  37. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  38. package/dist/leader-thread/mutationlog.js +2 -1
  39. package/dist/leader-thread/mutationlog.js.map +1 -1
  40. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  41. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  42. package/dist/leader-thread/recreate-db.js +9 -3
  43. package/dist/leader-thread/recreate-db.js.map +1 -1
  44. package/dist/leader-thread/types.d.ts +17 -9
  45. package/dist/leader-thread/types.d.ts.map +1 -1
  46. package/dist/leader-thread/types.js.map +1 -1
  47. package/dist/mutation.d.ts +9 -2
  48. package/dist/mutation.d.ts.map +1 -1
  49. package/dist/mutation.js +5 -5
  50. package/dist/mutation.js.map +1 -1
  51. package/dist/query-builder/impl.d.ts +1 -1
  52. package/dist/rehydrate-from-mutationlog.d.ts +2 -2
  53. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  54. package/dist/rehydrate-from-mutationlog.js +13 -19
  55. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  56. package/dist/schema/EventId.d.ts +16 -14
  57. package/dist/schema/EventId.d.ts.map +1 -1
  58. package/dist/schema/EventId.js +15 -7
  59. package/dist/schema/EventId.js.map +1 -1
  60. package/dist/schema/EventId.test.d.ts +2 -0
  61. package/dist/schema/EventId.test.d.ts.map +1 -0
  62. package/dist/schema/EventId.test.js +11 -0
  63. package/dist/schema/EventId.test.js.map +1 -0
  64. package/dist/schema/MutationEvent.d.ts +49 -80
  65. package/dist/schema/MutationEvent.d.ts.map +1 -1
  66. package/dist/schema/MutationEvent.js +32 -15
  67. package/dist/schema/MutationEvent.js.map +1 -1
  68. package/dist/schema/MutationEvent.test.d.ts +2 -0
  69. package/dist/schema/MutationEvent.test.d.ts.map +1 -0
  70. package/dist/schema/MutationEvent.test.js +2 -0
  71. package/dist/schema/MutationEvent.test.js.map +1 -0
  72. package/dist/schema/system-tables.d.ts +26 -26
  73. package/dist/schema/system-tables.d.ts.map +1 -1
  74. package/dist/schema/system-tables.js +19 -11
  75. package/dist/schema/system-tables.js.map +1 -1
  76. package/dist/schema-management/migrations.js +6 -6
  77. package/dist/schema-management/migrations.js.map +1 -1
  78. package/dist/sync/ClientSessionSyncProcessor.d.ts +45 -0
  79. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
  80. package/dist/sync/ClientSessionSyncProcessor.js +134 -0
  81. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
  82. package/dist/sync/client-session-sync-processor.d.ts +4 -4
  83. package/dist/sync/client-session-sync-processor.d.ts.map +1 -1
  84. package/dist/sync/index.d.ts +1 -1
  85. package/dist/sync/index.d.ts.map +1 -1
  86. package/dist/sync/index.js +1 -1
  87. package/dist/sync/index.js.map +1 -1
  88. package/dist/sync/next/history-dag-common.d.ts +1 -4
  89. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  90. package/dist/sync/next/history-dag-common.js +1 -1
  91. package/dist/sync/next/history-dag-common.js.map +1 -1
  92. package/dist/sync/next/rebase-events.d.ts +3 -3
  93. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  94. package/dist/sync/next/rebase-events.js +3 -2
  95. package/dist/sync/next/rebase-events.js.map +1 -1
  96. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  97. package/dist/sync/next/test/mutation-fixtures.js +3 -9
  98. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  99. package/dist/sync/sync.d.ts +21 -11
  100. package/dist/sync/sync.d.ts.map +1 -1
  101. package/dist/sync/sync.js.map +1 -1
  102. package/dist/sync/syncstate.d.ts +45 -23
  103. package/dist/sync/syncstate.d.ts.map +1 -1
  104. package/dist/sync/syncstate.js +56 -12
  105. package/dist/sync/syncstate.js.map +1 -1
  106. package/dist/sync/syncstate.test.js +125 -69
  107. package/dist/sync/syncstate.test.js.map +1 -1
  108. package/dist/sync/validate-push-payload.d.ts +2 -2
  109. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  110. package/dist/sync/validate-push-payload.js +2 -2
  111. package/dist/sync/validate-push-payload.js.map +1 -1
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.d.ts.map +1 -1
  114. package/dist/version.js +1 -1
  115. package/dist/version.js.map +1 -1
  116. package/package.json +6 -5
  117. package/src/adapter-types.ts +22 -24
  118. package/src/derived-mutations.test.ts +1 -1
  119. package/src/derived-mutations.ts +9 -5
  120. package/src/devtools/devtools-bridge.ts +2 -1
  121. package/src/devtools/devtools-messages.ts +9 -6
  122. package/src/index.ts +0 -6
  123. package/src/leader-thread/{leader-sync-processor.ts → LeaderSyncProcessor.ts} +235 -230
  124. package/src/leader-thread/apply-mutation.ts +49 -31
  125. package/src/leader-thread/leader-worker-devtools.ts +30 -109
  126. package/src/leader-thread/make-leader-thread-layer.ts +24 -13
  127. package/src/leader-thread/mutationlog.ts +9 -5
  128. package/src/leader-thread/recreate-db.ts +9 -5
  129. package/src/leader-thread/types.ts +18 -11
  130. package/src/mutation.ts +17 -7
  131. package/src/rehydrate-from-mutationlog.ts +15 -23
  132. package/src/schema/EventId.test.ts +12 -0
  133. package/src/schema/EventId.ts +23 -9
  134. package/src/schema/MutationEvent.ts +46 -24
  135. package/src/schema/system-tables.ts +19 -11
  136. package/src/schema-management/migrations.ts +6 -6
  137. package/src/sync/{client-session-sync-processor.ts → ClientSessionSyncProcessor.ts} +11 -9
  138. package/src/sync/index.ts +1 -1
  139. package/src/sync/next/history-dag-common.ts +1 -1
  140. package/src/sync/next/rebase-events.ts +7 -7
  141. package/src/sync/next/test/mutation-fixtures.ts +3 -10
  142. package/src/sync/sync.ts +19 -6
  143. package/src/sync/syncstate.test.ts +127 -67
  144. package/src/sync/syncstate.ts +21 -19
  145. package/src/sync/validate-push-payload.ts +7 -4
  146. package/src/version.ts +1 -1
@@ -1,8 +1,8 @@
1
- import { isDevEnv, memoizeByRef, shouldNeverHappen } from '@livestore/utils'
1
+ import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
2
2
  import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
3
3
 
4
4
  import { type MigrationOptionsFromMutationLog, type SynchronousDatabase, UnexpectedError } from './adapter-types.js'
5
- import { getExecArgsFromMutation } from './mutation.js'
5
+ import { makeApplyMutation } from './leader-thread/apply-mutation.js'
6
6
  import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
7
7
  import { EventId, MUTATION_LOG_META_TABLE } from './schema/mod.js'
8
8
  import type { PreparedBindValues } from './util.js'
@@ -10,7 +10,8 @@ import { sql } from './util.js'
10
10
 
11
11
  export const rehydrateFromMutationLog = ({
12
12
  logDb,
13
- db,
13
+ // TODO re-use this db when bringing back the boot in-memory db implementation
14
+ // db,
14
15
  schema,
15
16
  migrationOptions,
16
17
  onProgress,
@@ -28,6 +29,8 @@ export const rehydrateFromMutationLog = ({
28
29
 
29
30
  const hashMutation = memoizeByRef((mutation: MutationDef.Any) => Schema.hash(mutation.schema))
30
31
 
32
+ const applyMutation = yield* makeApplyMutation
33
+
31
34
  const processMutation = (row: MutationLogMetaRow) =>
32
35
  Effect.gen(function* () {
33
36
  const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
@@ -40,7 +43,10 @@ export const rehydrateFromMutationLog = ({
40
43
  )
41
44
  }
42
45
 
43
- const argsDecoded = yield* Schema.decodeUnknown(Schema.parseJson(mutationDef.schema))(row.argsJson).pipe(
46
+ const args = JSON.parse(row.argsJson)
47
+
48
+ // Checking whether the schema has changed in an incompatible way
49
+ yield* Schema.decodeUnknown(mutationDef.schema)(args).pipe(
44
50
  Effect.mapError((cause) =>
45
51
  UnexpectedError.make({
46
52
  cause,
@@ -53,28 +59,14 @@ This likely means the schema has changed in an incompatible way.
53
59
  ),
54
60
  )
55
61
 
56
- const mutationEventDecoded = {
62
+ const mutationEventEncoded = {
57
63
  id: { global: row.idGlobal, local: row.idLocal },
58
64
  parentId: { global: row.parentIdGlobal, local: row.parentIdLocal },
59
65
  mutation: row.mutation,
60
- args: argsDecoded,
61
- } satisfies MutationEvent.Any
62
-
63
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
64
-
65
- const makeExecuteOptions = (statementSql: string, bindValues: any) => ({
66
- onRowsChanged: (rowsChanged: number) => {
67
- if (rowsChanged === 0 && migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true) {
68
- console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
69
- }
70
- },
71
- })
72
-
73
- for (const { statementSql, bindValues } of execArgsArr) {
74
- // TODO cache prepared statements for mutations
75
- db.execute(statementSql, bindValues, isDevEnv() ? makeExecuteOptions(statementSql, bindValues) : undefined)
76
- // console.log(`Re-executed mutation ${mutationSql}`, bindValues)
77
- }
66
+ args,
67
+ } satisfies MutationEvent.AnyEncoded
68
+
69
+ yield* applyMutation(mutationEventEncoded, { skipMutationLog: true })
78
70
  }).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
79
71
 
80
72
  const CHUNK_SIZE = 100
@@ -0,0 +1,12 @@
1
+ import { Vitest } from '@livestore/utils/node-vitest'
2
+ import { expect } from 'vitest'
3
+
4
+ import { EventId } from './mod.js'
5
+
6
+ Vitest.describe('EventId', () => {
7
+ Vitest.test('nextPair', () => {
8
+ const e_0_0 = EventId.make({ global: 0, local: 0 })
9
+ expect(EventId.nextPair(e_0_0, false).id).toStrictEqual({ global: 1, local: 0 })
10
+ expect(EventId.nextPair(e_0_0, true).id).toStrictEqual({ global: 0, local: 1 })
11
+ })
12
+ })
@@ -1,4 +1,14 @@
1
- import { Schema } from '@livestore/utils/effect'
1
+ import { Brand, Schema } from '@livestore/utils/effect'
2
+
3
+ export type LocalEventId = Brand.Branded<number, 'LocalEventId'>
4
+ export const localEventId = Brand.nominal<LocalEventId>()
5
+ export const LocalEventId = Schema.fromBrand(localEventId)(Schema.Int)
6
+
7
+ export type GlobalEventId = Brand.Branded<number, 'GlobalEventId'>
8
+ export const globalEventId = Brand.nominal<GlobalEventId>()
9
+ export const GlobalEventId = Schema.fromBrand(globalEventId)(Schema.Int)
10
+
11
+ export const localDefault = 0 as any as LocalEventId
2
12
 
3
13
  /**
4
14
  * LiveStore event id value consisting of a globally unique event sequence number
@@ -6,11 +16,11 @@ import { Schema } from '@livestore/utils/effect'
6
16
  *
7
17
  * The local sequence number is only used for localOnly mutations and starts from 0 for each global sequence number.
8
18
  */
9
- export type EventId = { global: number; local: number }
19
+ export type EventId = { global: GlobalEventId; local: LocalEventId }
10
20
 
11
21
  export const EventId = Schema.Struct({
12
- global: Schema.Number,
13
- local: Schema.Number,
22
+ global: GlobalEventId,
23
+ local: LocalEventId,
14
24
  }).annotations({ title: 'LiveStore.EventId' })
15
25
 
16
26
  /**
@@ -27,20 +37,24 @@ export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.lo
27
37
 
28
38
  export type EventIdPair = { id: EventId; parentId: EventId }
29
39
 
30
- export const ROOT = { global: -1, local: 0 } satisfies EventId
40
+ export const ROOT = { global: -1 as any as GlobalEventId, local: localDefault } satisfies EventId
31
41
 
32
42
  export const isGreaterThan = (a: EventId, b: EventId) => {
33
43
  return a.global > b.global || (a.global === b.global && a.local > b.local)
34
44
  }
35
45
 
36
- export const nextPair = (id: EventId, isLocal: boolean) => {
46
+ export const make = (id: EventId | typeof EventId.Encoded): EventId => {
47
+ return Schema.is(EventId)(id) ? id : Schema.decodeSync(EventId)(id)
48
+ }
49
+
50
+ export const nextPair = (id: EventId, isLocal: boolean): EventIdPair => {
37
51
  if (isLocal) {
38
- return { id: { global: id.global, local: id.local + 1 }, parentId: id }
52
+ return { id: { global: id.global, local: (id.local + 1) as any as LocalEventId }, parentId: id }
39
53
  }
40
54
 
41
55
  return {
42
- id: { global: id.global + 1, local: 0 },
56
+ id: { global: (id.global + 1) as any as GlobalEventId, local: localDefault },
43
57
  // NOTE we always point to `local: 0` for non-localOnly mutations
44
- parentId: { global: id.global, local: 0 },
58
+ parentId: { global: id.global, local: localDefault },
45
59
  }
46
60
  }
@@ -1,5 +1,4 @@
1
1
  import { memoizeByRef } from '@livestore/utils'
2
- import type { Deferred } from '@livestore/utils/effect'
3
2
  import { Schema } from '@livestore/utils/effect'
4
3
 
5
4
  import * as EventId from './EventId.js'
@@ -30,10 +29,31 @@ export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
30
29
  parentId: EventId.EventId
31
30
  }
32
31
 
33
- export type Any = MutationEvent<MutationDef.Any>
32
+ export type AnyDecoded = MutationEvent<MutationDef.Any>
33
+ export const AnyDecoded = Schema.Struct({
34
+ mutation: Schema.String,
35
+ args: Schema.Any,
36
+ id: EventId.EventId,
37
+ parentId: EventId.EventId,
38
+ }).annotations({ title: 'MutationEvent.AnyDecoded' })
39
+
34
40
  export type AnyEncoded = MutationEventEncoded<MutationDef.Any>
41
+ export const AnyEncoded = Schema.Struct({
42
+ mutation: Schema.String,
43
+ args: Schema.Any,
44
+ id: EventId.EventId,
45
+ parentId: EventId.EventId,
46
+ }).annotations({ title: 'MutationEvent.AnyEncoded' })
47
+
48
+ export const AnyEncodedGlobal = Schema.Struct({
49
+ mutation: Schema.String,
50
+ args: Schema.Any,
51
+ id: EventId.GlobalEventId,
52
+ parentId: EventId.GlobalEventId,
53
+ }).annotations({ title: 'MutationEvent.AnyEncodedGlobal' })
54
+ export type AnyEncodedGlobal = typeof AnyEncodedGlobal.Type
35
55
 
36
- export type PartialAny = MutationEventPartial<MutationDef.Any>
56
+ export type PartialAnyDecoded = MutationEventPartial<MutationDef.Any>
37
57
  export type PartialAnyEncoded = MutationEventPartialEncoded<MutationDef.Any>
38
58
 
39
59
  export type PartialForSchema<TSchema extends LiveStoreSchema> = {
@@ -44,8 +64,9 @@ export type ForSchema<TSchema extends LiveStoreSchema> = {
44
64
  [K in keyof TSchema['_MutationDefMapType']]: MutationEvent<TSchema['_MutationDefMapType'][K]>
45
65
  }[keyof TSchema['_MutationDefMapType']]
46
66
 
47
- export const isPartialMutationEvent = (mutationEvent: Any | PartialAny): mutationEvent is PartialAny =>
48
- 'id' in mutationEvent === false && 'parentId' in mutationEvent === false
67
+ export const isPartialMutationEvent = (
68
+ mutationEvent: AnyDecoded | PartialAnyDecoded,
69
+ ): mutationEvent is PartialAnyDecoded => 'id' in mutationEvent === false && 'parentId' in mutationEvent === false
49
70
 
50
71
  export type ForMutationDefRecord<TMutationsDefRecord extends MutationDefRecord> = Schema.Schema<
51
72
  {
@@ -105,33 +126,21 @@ export const makeMutationEventPartialSchema = <TSchema extends LiveStoreSchema>(
105
126
  args: def.schema,
106
127
  }),
107
128
  ),
108
- ).annotations({ title: 'MutationEventSchemaPartial' }) as any
129
+ ).annotations({ title: 'MutationEventPartial' }) as any
109
130
 
110
131
  export const makeMutationEventSchemaMemo = memoizeByRef(makeMutationEventSchema)
111
132
 
112
- export const Any = Schema.Struct({
113
- mutation: Schema.String,
114
- args: Schema.Any,
115
- id: EventId.EventId,
116
- parentId: EventId.EventId,
117
- }).annotations({ title: 'MutationEvent.Any' })
118
-
119
- export const DecodedAny = Schema.typeSchema(Any).annotations({
120
- title: 'MutationEvent.DecodedAny',
121
- })
122
-
123
- export const EncodedAny = Schema.encodedSchema(Any).annotations({
124
- title: 'MutationEvent.EncodedAny',
125
- })
126
-
127
- /** Equivalent to EncodedAny but with a meta field and some convenience methods */
133
+ /** Equivalent to AnyEncoded but with a meta field and some convenience methods */
128
134
  export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEvent.EncodedWithMeta')({
129
135
  mutation: Schema.String,
130
136
  args: Schema.Any,
131
137
  id: EventId.EventId,
132
138
  parentId: EventId.EventId,
139
+ // TODO get rid of `meta` again by cleaning up the usage implementations
133
140
  meta: Schema.optionalWith(
134
- Schema.Any as Schema.Schema<{ deferred?: Deferred.Deferred<void>; sessionChangeset?: Uint8Array }>,
141
+ Schema.Any as Schema.Schema<{
142
+ sessionChangeset?: Uint8Array
143
+ }>,
135
144
  { default: () => ({}) },
136
145
  ),
137
146
  }) {
@@ -149,8 +158,21 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
149
158
  rebase = (parentId: EventId.EventId, isLocal: boolean) =>
150
159
  new EncodedWithMeta({
151
160
  ...this,
152
- ...EventId.nextPair(this.id, isLocal),
161
+ ...EventId.nextPair(parentId, isLocal),
153
162
  })
163
+
164
+ static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
165
+ new EncodedWithMeta({
166
+ ...mutationEvent,
167
+ id: { global: mutationEvent.id, local: EventId.localDefault },
168
+ parentId: { global: mutationEvent.parentId, local: EventId.localDefault },
169
+ })
170
+
171
+ toGlobal = (): AnyEncodedGlobal => ({
172
+ ...this,
173
+ id: this.id.global,
174
+ parentId: this.parentId.global,
175
+ })
154
176
  }
155
177
 
156
178
  export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
@@ -1,6 +1,7 @@
1
1
  import { type SqliteAst as __SqliteAst, SqliteDsl } from '@livestore/db-schema'
2
2
  import { Schema } from '@livestore/utils/effect'
3
3
 
4
+ import * as EventId from './EventId.js'
4
5
  import type { FromTable } from './table-def.js'
5
6
  import { table } from './table-def.js'
6
7
 
@@ -46,14 +47,15 @@ export const sessionChangesetMetaTable = table(
46
47
  SESSION_CHANGESET_META_TABLE,
47
48
  {
48
49
  // TODO bring back primary key
49
- idGlobal: SqliteDsl.integer({}),
50
- idLocal: SqliteDsl.integer({}),
51
- // idGlobal: SqliteDsl.integer({ primaryKey: true }),
52
- // idLocal: SqliteDsl.integer({ primaryKey: true }),
53
- changeset: SqliteDsl.blob({}),
50
+ idGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
51
+ idLocal: SqliteDsl.integer({ schema: EventId.LocalEventId }),
52
+ changeset: SqliteDsl.blob({ nullable: true }),
54
53
  debug: SqliteDsl.json({ nullable: true }),
55
54
  },
56
- { disableAutomaticIdColumn: true },
55
+ {
56
+ disableAutomaticIdColumn: true,
57
+ indexes: [{ columns: ['idGlobal', 'idLocal'], name: 'idx_session_changeset_id' }],
58
+ },
57
59
  )
58
60
 
59
61
  export type SessionChangesetMetaRow = FromTable.RowDecoded<typeof sessionChangesetMetaTable>
@@ -70,16 +72,22 @@ export const MUTATION_LOG_META_TABLE = 'mutation_log'
70
72
  export const mutationLogMetaTable = table(
71
73
  MUTATION_LOG_META_TABLE,
72
74
  {
73
- idGlobal: SqliteDsl.integer({ primaryKey: true }),
74
- idLocal: SqliteDsl.integer({ primaryKey: true }),
75
- parentIdGlobal: SqliteDsl.integer({}),
76
- parentIdLocal: SqliteDsl.integer({}),
75
+ idGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventId.GlobalEventId }),
76
+ idLocal: SqliteDsl.integer({ primaryKey: true, schema: EventId.LocalEventId }),
77
+ parentIdGlobal: SqliteDsl.integer({ schema: EventId.GlobalEventId }),
78
+ parentIdLocal: SqliteDsl.integer({ schema: EventId.LocalEventId }),
77
79
  mutation: SqliteDsl.text({}),
78
80
  argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
79
81
  schemaHash: SqliteDsl.integer({}),
80
82
  syncMetadataJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Option(Schema.JsonValue)) }),
81
83
  },
82
- { disableAutomaticIdColumn: true, indexes: [] },
84
+ {
85
+ disableAutomaticIdColumn: true,
86
+ indexes: [
87
+ { columns: ['idGlobal'], name: 'idx_idGlobal' },
88
+ { columns: ['idGlobal', 'idLocal'], name: 'idx_mutationlog_id' },
89
+ ],
90
+ },
83
91
  )
84
92
 
85
93
  export type MutationLogMetaRow = FromTable.RowDecoded<typeof mutationLogMetaTable>
@@ -129,10 +129,10 @@ export const migrateTable = ({
129
129
 
130
130
  if (behaviour === 'drop-and-recreate') {
131
131
  // TODO need to possibly handle cascading deletes due to foreign keys
132
- dbExecute(db, sql`drop table if exists ${tableName}`)
133
- dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
132
+ dbExecute(db, sql`drop table if exists '${tableName}'`)
133
+ dbExecute(db, sql`create table if not exists '${tableName}' (${columnSpec}) strict`)
134
134
  } else if (behaviour === 'create-if-not-exists') {
135
- dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
135
+ dbExecute(db, sql`create table if not exists '${tableName}' (${columnSpec}) strict`)
136
136
  }
137
137
 
138
138
  for (const index of tableAst.indexes) {
@@ -162,11 +162,11 @@ export const migrateTable = ({
162
162
 
163
163
  const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
164
164
  const uniqueStr = index.unique ? 'UNIQUE' : ''
165
- return sql`create ${uniqueStr} index if not exists ${index.name} on ${tableName} (${index.columns.join(', ')})`
165
+ return sql`create ${uniqueStr} index if not exists '${index.name}' on '${tableName}' (${index.columns.join(', ')})`
166
166
  }
167
167
 
168
168
  export const makeColumnSpec = (tableAst: SqliteAst.Table) => {
169
- const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => _.name)
169
+ const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => `'${_.name}'`)
170
170
  const columnDefStrs = tableAst.columns.map(toSqliteColumnSpec)
171
171
  if (primaryKeys.length > 0) {
172
172
  columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
@@ -191,5 +191,5 @@ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
191
191
  return `default ${encodedDefaultValue}`
192
192
  })()
193
193
 
194
- return `${column.name} ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
194
+ return `'${column.name}' ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
195
195
  }
@@ -3,12 +3,11 @@ import type { Scope } from '@livestore/utils/effect'
3
3
  import { Effect, Schema, Stream } from '@livestore/utils/effect'
4
4
  import * as otel from '@opentelemetry/api'
5
5
 
6
- import type { Coordinator, UnexpectedError } from '../adapter-types.js'
6
+ import type { ClientSessionLeaderThreadProxy, UnexpectedError } from '../adapter-types.js'
7
7
  import * as EventId from '../schema/EventId.js'
8
8
  import { type LiveStoreSchema } from '../schema/mod.js'
9
9
  import * as MutationEvent from '../schema/MutationEvent.js'
10
- import type { SyncState } from './syncstate.js'
11
- import { updateSyncState } from './syncstate.js'
10
+ import { SyncState, updateSyncState } from './syncstate.js'
12
11
 
13
12
  /**
14
13
  * Rebase behaviour:
@@ -32,9 +31,9 @@ export const makeClientSessionSyncProcessor = ({
32
31
  schema: LiveStoreSchema
33
32
  initialLeaderHead: EventId.EventId
34
33
  pushToLeader: (batch: ReadonlyArray<MutationEvent.AnyEncoded>) => void
35
- pullFromLeader: Coordinator['mutations']['pull']
34
+ pullFromLeader: ClientSessionLeaderThreadProxy['mutations']['pull']
36
35
  applyMutation: (
37
- mutationEventDecoded: MutationEvent.PartialAny,
36
+ mutationEventDecoded: MutationEvent.PartialAnyDecoded,
38
37
  options: { otelContext: otel.Context; withChangeset: boolean },
39
38
  ) => {
40
39
  writeTables: Set<string>
@@ -42,19 +41,18 @@ export const makeClientSessionSyncProcessor = ({
42
41
  }
43
42
  rollback: (changeset: Uint8Array) => void
44
43
  refreshTables: (tables: Set<string>) => void
45
- // rebaseBehaviour: 'auto-rebase' | 'manual-rebase'
46
44
  span: otel.Span
47
45
  }): ClientSessionSyncProcessor => {
48
46
  const mutationEventSchema = MutationEvent.makeMutationEventSchemaMemo(schema)
49
47
 
50
48
  const syncStateRef = {
51
- current: {
49
+ current: new SyncState({
52
50
  localHead: initialLeaderHead,
53
51
  upstreamHead: initialLeaderHead,
54
52
  pending: [],
55
53
  // TODO init rollbackTail from leader to be ready for backend rebasing
56
54
  rollbackTail: [],
57
- } as SyncState,
55
+ }),
58
56
  }
59
57
 
60
58
  const isLocalEvent = (mutationEventEncoded: MutationEvent.EncodedWithMeta) => {
@@ -104,6 +102,7 @@ export const makeClientSessionSyncProcessor = ({
104
102
  mutationEvent.meta.sessionChangeset = res.sessionChangeset
105
103
  }
106
104
 
105
+ // console.debug('pushToLeader', encodedMutationEvents.length, ...encodedMutationEvents.map((_) => _.toJSON()))
107
106
  pushToLeader(encodedMutationEvents)
108
107
 
109
108
  return { writeTables }
@@ -155,6 +154,8 @@ export const makeClientSessionSyncProcessor = ({
155
154
  event.meta.sessionChangeset = undefined
156
155
  }
157
156
  }
157
+
158
+ pushToLeader(updateResult.newSyncState.pending)
158
159
  } else {
159
160
  span.addEvent('pull:advance', {
160
161
  payloadTag: payload._tag,
@@ -182,6 +183,7 @@ export const makeClientSessionSyncProcessor = ({
182
183
  }),
183
184
  ),
184
185
  Stream.runDrain,
186
+ Effect.forever, // NOTE Whenever the leader changes, we need to re-start the stream
185
187
  Effect.tapCauseLogPretty,
186
188
  Effect.forkScoped,
187
189
  )
@@ -196,7 +198,7 @@ export const makeClientSessionSyncProcessor = ({
196
198
 
197
199
  export interface ClientSessionSyncProcessor {
198
200
  push: (
199
- batch: ReadonlyArray<MutationEvent.PartialAny>,
201
+ batch: ReadonlyArray<MutationEvent.PartialAnyDecoded>,
200
202
  options: { otelContext: otel.Context },
201
203
  ) => {
202
204
  writeTables: Set<string>
package/src/sync/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './sync.js'
2
2
  export * from './validate-push-payload.js'
3
- export * from './client-session-sync-processor.js'
3
+ export * from './ClientSessionSyncProcessor.js'
@@ -20,7 +20,7 @@ export const emptyHistoryDag = (): HistoryDag =>
20
20
  })
21
21
 
22
22
  // TODO consider making `ROOT_ID` parent to itself
23
- export const rootParentId = { global: EventId.ROOT.global - 1, local: 0 } satisfies EventId.EventId
23
+ export const rootParentId = EventId.make({ global: EventId.ROOT.global - 1, local: EventId.localDefault })
24
24
 
25
25
  export type HistoryDagNode = {
26
26
  id: EventId.EventId
@@ -1,4 +1,4 @@
1
- import type * as EventId from '../../schema/EventId.js'
1
+ import * as EventId from '../../schema/EventId.js'
2
2
  import type * as MutationEvent from '../../schema/MutationEvent.js'
3
3
  import type { MutationDef, MutationEventFactsSnapshot } from '../../schema/mutations.js'
4
4
  import {
@@ -19,13 +19,13 @@ export type RebaseInput = {
19
19
  newRemoteEvents: RebaseEventWithConflict[]
20
20
  pendingLocalEvents: RebaseEventWithConflict[]
21
21
  validate: (args: {
22
- rebasedLocalEvents: MutationEvent.PartialAny[]
22
+ rebasedLocalEvents: MutationEvent.PartialAnyDecoded[]
23
23
  mutationDefs: Record<string, MutationDef.Any>
24
24
  }) => FactValidationResult
25
25
  }
26
26
 
27
27
  export type RebaseOutput = {
28
- rebasedLocalEvents: MutationEvent.PartialAny[]
28
+ rebasedLocalEvents: MutationEvent.PartialAnyDecoded[]
29
29
  }
30
30
 
31
31
  export type RebaseFn = (input: RebaseInput) => RebaseOutput
@@ -48,7 +48,7 @@ export const rebaseEvents = ({
48
48
  newRemoteEvents: HistoryDagNode[]
49
49
  rebaseFn: RebaseFn
50
50
  currentFactsSnapshot: MutationEventFactsSnapshot
51
- }): MutationEvent.Any[] => {
51
+ }): ReadonlyArray<MutationEvent.AnyDecoded> => {
52
52
  const initialSnapshot = new Map(currentFactsSnapshot)
53
53
  applyFactGroups(
54
54
  newRemoteEvents.map((event) => event.factsGroup),
@@ -89,10 +89,10 @@ export const rebaseEvents = ({
89
89
  return rebasedLocalEvents.map(
90
90
  (event, index) =>
91
91
  ({
92
- id: { global: headGlobalId + index + 1, local: 0 } satisfies EventId.EventId,
93
- parentId: { global: headGlobalId + index, local: 0 } satisfies EventId.EventId,
92
+ id: EventId.make({ global: headGlobalId + index + 1, local: EventId.localDefault }),
93
+ parentId: EventId.make({ global: headGlobalId + index, local: EventId.localDefault }),
94
94
  mutation: event.mutation,
95
95
  args: event.args,
96
- }) satisfies MutationEvent.Any,
96
+ }) satisfies MutationEvent.AnyDecoded,
97
97
  )
98
98
  }
@@ -140,16 +140,9 @@ export const toEventNodes = (
140
140
 
141
141
  let currentEventId: EventId.EventId = EventId.ROOT
142
142
 
143
- const getNextEventId = (mutationDef: MutationDef.Any): EventId.EventId => {
144
- if (mutationDef.options.localOnly) {
145
- return { global: currentEventId.global, local: currentEventId.local + 1 }
146
- }
147
- return { global: currentEventId.global + 1, local: 0 }
148
- }
149
-
150
143
  const eventNodes = partialEvents.map((partialEvent) => {
151
144
  const mutationDef = mutationDefs[partialEvent.mutation]!
152
- const eventId = getNextEventId(mutationDef)
145
+ const eventId = EventId.nextPair(currentEventId, mutationDef.options.localOnly).id
153
146
  currentEventId = eventId
154
147
 
155
148
  const factsSnapshot = factsSnapshotForDag(historyDagFromNodes(nodesAcc, { skipFactsCheck: true }), undefined)
@@ -224,8 +217,8 @@ const getParentId = (eventId: EventId.EventId): EventId.EventId => {
224
217
  const localParentId = eventId.local - 1
225
218
 
226
219
  if (localParentId < 0) {
227
- return { global: globalParentId - 1, local: 0 }
220
+ return EventId.make({ global: globalParentId - 1, local: EventId.localDefault })
228
221
  }
229
222
 
230
- return { global: globalParentId, local: localParentId }
223
+ return EventId.make({ global: globalParentId, local: localParentId })
231
224
  }
package/src/sync/sync.ts CHANGED
@@ -1,12 +1,23 @@
1
- import type { Effect, HttpClient, Option, Stream, SubscriptionRef } from '@livestore/utils/effect'
1
+ import type { Effect, HttpClient, Option, Scope, Stream, SubscriptionRef } from '@livestore/utils/effect'
2
2
  import { Schema } from '@livestore/utils/effect'
3
3
 
4
+ import type { UnexpectedError } from '../adapter-types.js'
5
+ import type { InitialSyncOptions } from '../leader-thread/types.js'
4
6
  import * as EventId from '../schema/EventId.js'
5
7
  import type * as MutationEvent from '../schema/MutationEvent.js'
6
8
 
7
- export interface SyncBackendOptionsBase {
8
- type: string
9
- [key: string]: Schema.JsonValue
9
+ /**
10
+ * Those arguments can be used to implement multi-tenancy etc and are passed in from the store.
11
+ */
12
+ export type MakeBackendArgs = {
13
+ storeId: string
14
+ clientId: string
15
+ }
16
+
17
+ export type SyncOptions = {
18
+ makeBackend: (args: MakeBackendArgs) => Effect.Effect<SyncBackend<any>, UnexpectedError, Scope.Scope>
19
+ /** @default { _tag: 'Skip' } */
20
+ initialSyncOptions?: InitialSyncOptions
10
21
  }
11
22
 
12
23
  export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
@@ -18,7 +29,7 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
18
29
  ) => Stream.Stream<
19
30
  {
20
31
  batch: ReadonlyArray<{
21
- mutationEventEncoded: MutationEvent.AnyEncoded
32
+ mutationEventEncoded: MutationEvent.AnyEncodedGlobal
22
33
  metadata: Option.Option<TSyncMetadata>
23
34
  }>
24
35
  remaining: number
@@ -33,7 +44,7 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
33
44
  * - Number of events: 1-100
34
45
  * - event ids must be in ascending order
35
46
  * */
36
- batch: ReadonlyArray<MutationEvent.AnyEncoded>,
47
+ batch: ReadonlyArray<MutationEvent.AnyEncodedGlobal>,
37
48
  ) => Effect.Effect<
38
49
  {
39
50
  /** Indexes are relative to `batch` */
@@ -46,6 +57,7 @@ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
46
57
  }
47
58
 
48
59
  export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {}) {}
60
+
49
61
  export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('InvalidPushError', {
50
62
  reason: Schema.Union(
51
63
  Schema.TaggedStruct('Unexpected', {
@@ -61,6 +73,7 @@ export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('In
61
73
  }),
62
74
  ),
63
75
  }) {}
76
+
64
77
  export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {
65
78
  message: Schema.String,
66
79
  }) {}