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

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 (181) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +47 -35
  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/devtool-message-leader.d.ts +2 -0
  9. package/dist/devtools/devtool-message-leader.d.ts.map +1 -0
  10. package/dist/devtools/devtool-message-leader.js +2 -0
  11. package/dist/devtools/devtool-message-leader.js.map +1 -0
  12. package/dist/devtools/devtools-bridge.d.ts +2 -1
  13. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  14. package/dist/devtools/devtools-messages-client-session.d.ts +297 -0
  15. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -0
  16. package/dist/devtools/devtools-messages-client-session.js +61 -0
  17. package/dist/devtools/devtools-messages-client-session.js.map +1 -0
  18. package/dist/devtools/devtools-messages-common.d.ts +65 -0
  19. package/dist/devtools/devtools-messages-common.d.ts.map +1 -0
  20. package/dist/devtools/devtools-messages-common.js +35 -0
  21. package/dist/devtools/devtools-messages-common.js.map +1 -0
  22. package/dist/devtools/devtools-messages-leader.d.ts +261 -0
  23. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -0
  24. package/dist/devtools/devtools-messages-leader.js +85 -0
  25. package/dist/devtools/devtools-messages-leader.js.map +1 -0
  26. package/dist/devtools/devtools-messages.d.ts +3 -592
  27. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  28. package/dist/devtools/devtools-messages.js +3 -171
  29. package/dist/devtools/devtools-messages.js.map +1 -1
  30. package/dist/index.d.ts +0 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/init-singleton-tables.d.ts +2 -2
  33. package/dist/init-singleton-tables.d.ts.map +1 -1
  34. package/dist/init-singleton-tables.js.map +1 -1
  35. package/dist/leader-thread/LeaderSyncProcessor.d.ts +37 -0
  36. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -0
  37. package/dist/leader-thread/LeaderSyncProcessor.js +432 -0
  38. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -0
  39. package/dist/leader-thread/apply-mutation.d.ts +5 -2
  40. package/dist/leader-thread/apply-mutation.d.ts.map +1 -1
  41. package/dist/leader-thread/apply-mutation.js +41 -29
  42. package/dist/leader-thread/apply-mutation.js.map +1 -1
  43. package/dist/leader-thread/connection.d.ts +4 -4
  44. package/dist/leader-thread/connection.d.ts.map +1 -1
  45. package/dist/leader-thread/connection.js +5 -5
  46. package/dist/leader-thread/connection.js.map +1 -1
  47. package/dist/leader-thread/leader-sync-processor.d.ts +2 -2
  48. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -1
  49. package/dist/leader-thread/leader-sync-processor.js +20 -12
  50. package/dist/leader-thread/leader-sync-processor.js.map +1 -1
  51. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  52. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  53. package/dist/leader-thread/leader-worker-devtools.js +37 -81
  54. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  55. package/dist/leader-thread/make-leader-thread-layer.d.ts +12 -11
  56. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  57. package/dist/leader-thread/make-leader-thread-layer.js +33 -14
  58. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  59. package/dist/leader-thread/mutationlog.d.ts +6 -19
  60. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  61. package/dist/leader-thread/mutationlog.js +7 -6
  62. package/dist/leader-thread/mutationlog.js.map +1 -1
  63. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  64. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  65. package/dist/leader-thread/recreate-db.js +24 -18
  66. package/dist/leader-thread/recreate-db.js.map +1 -1
  67. package/dist/leader-thread/types.d.ts +36 -16
  68. package/dist/leader-thread/types.d.ts.map +1 -1
  69. package/dist/leader-thread/types.js.map +1 -1
  70. package/dist/mutation.d.ts +9 -2
  71. package/dist/mutation.d.ts.map +1 -1
  72. package/dist/mutation.js +5 -5
  73. package/dist/mutation.js.map +1 -1
  74. package/dist/query-builder/impl.d.ts +1 -1
  75. package/dist/rehydrate-from-mutationlog.d.ts +5 -5
  76. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  77. package/dist/rehydrate-from-mutationlog.js +13 -19
  78. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  79. package/dist/schema/EventId.d.ts +16 -14
  80. package/dist/schema/EventId.d.ts.map +1 -1
  81. package/dist/schema/EventId.js +15 -7
  82. package/dist/schema/EventId.js.map +1 -1
  83. package/dist/schema/EventId.test.d.ts +2 -0
  84. package/dist/schema/EventId.test.d.ts.map +1 -0
  85. package/dist/schema/EventId.test.js +11 -0
  86. package/dist/schema/EventId.test.js.map +1 -0
  87. package/dist/schema/MutationEvent.d.ts +49 -80
  88. package/dist/schema/MutationEvent.d.ts.map +1 -1
  89. package/dist/schema/MutationEvent.js +32 -15
  90. package/dist/schema/MutationEvent.js.map +1 -1
  91. package/dist/schema/MutationEvent.test.d.ts +2 -0
  92. package/dist/schema/MutationEvent.test.d.ts.map +1 -0
  93. package/dist/schema/MutationEvent.test.js +2 -0
  94. package/dist/schema/MutationEvent.test.js.map +1 -0
  95. package/dist/schema/system-tables.d.ts +26 -26
  96. package/dist/schema/system-tables.d.ts.map +1 -1
  97. package/dist/schema/system-tables.js +19 -11
  98. package/dist/schema/system-tables.js.map +1 -1
  99. package/dist/schema-management/common.d.ts +3 -3
  100. package/dist/schema-management/common.d.ts.map +1 -1
  101. package/dist/schema-management/common.js.map +1 -1
  102. package/dist/schema-management/migrations.d.ts +4 -4
  103. package/dist/schema-management/migrations.d.ts.map +1 -1
  104. package/dist/schema-management/migrations.js +6 -6
  105. package/dist/schema-management/migrations.js.map +1 -1
  106. package/dist/sync/ClientSessionSyncProcessor.d.ts +43 -0
  107. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -0
  108. package/dist/sync/ClientSessionSyncProcessor.js +141 -0
  109. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -0
  110. package/dist/sync/client-session-sync-processor.d.ts +4 -4
  111. package/dist/sync/client-session-sync-processor.d.ts.map +1 -1
  112. package/dist/sync/index.d.ts +1 -1
  113. package/dist/sync/index.d.ts.map +1 -1
  114. package/dist/sync/index.js +1 -1
  115. package/dist/sync/index.js.map +1 -1
  116. package/dist/sync/next/history-dag-common.d.ts +1 -4
  117. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  118. package/dist/sync/next/history-dag-common.js +1 -1
  119. package/dist/sync/next/history-dag-common.js.map +1 -1
  120. package/dist/sync/next/rebase-events.d.ts +3 -3
  121. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  122. package/dist/sync/next/rebase-events.js +3 -2
  123. package/dist/sync/next/rebase-events.js.map +1 -1
  124. package/dist/sync/next/test/mutation-fixtures.d.ts +7 -7
  125. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  126. package/dist/sync/next/test/mutation-fixtures.js +3 -9
  127. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  128. package/dist/sync/sync.d.ts +21 -11
  129. package/dist/sync/sync.d.ts.map +1 -1
  130. package/dist/sync/sync.js.map +1 -1
  131. package/dist/sync/syncstate.d.ts +45 -23
  132. package/dist/sync/syncstate.d.ts.map +1 -1
  133. package/dist/sync/syncstate.js +56 -12
  134. package/dist/sync/syncstate.js.map +1 -1
  135. package/dist/sync/syncstate.test.js +125 -69
  136. package/dist/sync/syncstate.test.js.map +1 -1
  137. package/dist/sync/validate-push-payload.d.ts +2 -2
  138. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  139. package/dist/sync/validate-push-payload.js +2 -2
  140. package/dist/sync/validate-push-payload.js.map +1 -1
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.d.ts.map +1 -1
  143. package/dist/version.js +1 -1
  144. package/dist/version.js.map +1 -1
  145. package/package.json +6 -5
  146. package/src/adapter-types.ts +39 -40
  147. package/src/derived-mutations.test.ts +1 -1
  148. package/src/derived-mutations.ts +9 -5
  149. package/src/devtools/devtools-bridge.ts +2 -1
  150. package/src/devtools/devtools-messages-client-session.ts +109 -0
  151. package/src/devtools/devtools-messages-common.ts +52 -0
  152. package/src/devtools/devtools-messages-leader.ts +115 -0
  153. package/src/devtools/devtools-messages.ts +3 -243
  154. package/src/index.ts +0 -6
  155. package/src/init-singleton-tables.ts +2 -2
  156. package/src/leader-thread/{leader-sync-processor.ts → LeaderSyncProcessor.ts} +306 -268
  157. package/src/leader-thread/apply-mutation.ts +53 -35
  158. package/src/leader-thread/connection.ts +7 -7
  159. package/src/leader-thread/leader-worker-devtools.ts +52 -124
  160. package/src/leader-thread/make-leader-thread-layer.ts +62 -30
  161. package/src/leader-thread/mutationlog.ts +14 -10
  162. package/src/leader-thread/recreate-db.ts +24 -20
  163. package/src/leader-thread/types.ts +41 -20
  164. package/src/mutation.ts +17 -7
  165. package/src/rehydrate-from-mutationlog.ts +18 -26
  166. package/src/schema/EventId.test.ts +12 -0
  167. package/src/schema/EventId.ts +23 -9
  168. package/src/schema/MutationEvent.ts +46 -24
  169. package/src/schema/system-tables.ts +19 -11
  170. package/src/schema-management/common.ts +3 -3
  171. package/src/schema-management/migrations.ts +10 -10
  172. package/src/sync/{client-session-sync-processor.ts → ClientSessionSyncProcessor.ts} +26 -19
  173. package/src/sync/index.ts +1 -1
  174. package/src/sync/next/history-dag-common.ts +1 -1
  175. package/src/sync/next/rebase-events.ts +7 -7
  176. package/src/sync/next/test/mutation-fixtures.ts +3 -10
  177. package/src/sync/sync.ts +19 -6
  178. package/src/sync/syncstate.test.ts +127 -67
  179. package/src/sync/syncstate.ts +21 -19
  180. package/src/sync/validate-push-payload.ts +7 -4
  181. package/src/version.ts +1 -1
@@ -1,17 +1,20 @@
1
1
  import { Effect, Schema } from '@livestore/utils/effect'
2
2
 
3
- import type { SynchronousDatabase } from '../adapter-types.js'
3
+ import type { SqliteDb } from '../adapter-types.js'
4
4
  import * as EventId from '../schema/EventId.js'
5
+ import type * as MutationEvent from '../schema/MutationEvent.js'
5
6
  import { MUTATION_LOG_META_TABLE, mutationLogMetaTable, SYNC_STATUS_TABLE } from '../schema/system-tables.js'
6
7
  import { prepareBindValues, sql } from '../util.js'
7
8
  import { LeaderThreadCtx } from './types.js'
8
9
 
9
- export const getMutationEventsSince = (since: EventId.EventId) =>
10
+ export const getMutationEventsSince = (
11
+ since: EventId.EventId,
12
+ ): Effect.Effect<ReadonlyArray<MutationEvent.AnyEncoded>, never, LeaderThreadCtx> =>
10
13
  Effect.gen(function* () {
11
- const { dbLog } = yield* LeaderThreadCtx
14
+ const { dbMutationLog } = yield* LeaderThreadCtx
12
15
 
13
16
  const query = mutationLogMetaTable.query.where('idGlobal', '>=', since.global).asSql()
14
- const pendingMutationEventsRaw = dbLog.select(query.query, prepareBindValues(query.bindValues, query.query))
17
+ const pendingMutationEventsRaw = dbMutationLog.select(query.query, prepareBindValues(query.bindValues, query.query))
15
18
  const pendingMutationEvents = Schema.decodeUnknownSync(mutationLogMetaTable.schema.pipe(Schema.Array))(
16
19
  pendingMutationEventsRaw,
17
20
  )
@@ -26,17 +29,18 @@ export const getMutationEventsSince = (since: EventId.EventId) =>
26
29
  .filter((_) => EventId.compare(_.id, since) > 0)
27
30
  })
28
31
 
29
- export const getLocalHeadFromDb = (dbLog: SynchronousDatabase) => {
30
- const res = dbLog.select<{ idGlobal: number; idLocal: number }>(
32
+ export const getLocalHeadFromDb = (dbMutationLog: SqliteDb): EventId.EventId => {
33
+ const res = dbMutationLog.select<{ idGlobal: EventId.GlobalEventId; idLocal: EventId.LocalEventId }>(
31
34
  sql`select idGlobal, idLocal from ${MUTATION_LOG_META_TABLE} order by idGlobal DESC, idLocal DESC limit 1`,
32
35
  )[0]
33
36
 
34
37
  return res ? { global: res.idGlobal, local: res.idLocal } : EventId.ROOT
35
38
  }
36
39
 
37
- export const getBackendHeadFromDb = (dbLog: SynchronousDatabase) =>
38
- dbLog.select<{ head: number }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ?? EventId.ROOT.global
40
+ export const getBackendHeadFromDb = (dbMutationLog: SqliteDb): EventId.GlobalEventId =>
41
+ dbMutationLog.select<{ head: EventId.GlobalEventId }>(sql`select head from ${SYNC_STATUS_TABLE}`)[0]?.head ??
42
+ EventId.ROOT.global
39
43
 
40
44
  // TODO use prepared statements
41
- export const updateBackendHead = (dbLog: SynchronousDatabase, head: EventId.EventId) =>
42
- dbLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
45
+ export const updateBackendHead = (dbMutationLog: SqliteDb, head: EventId.EventId) =>
46
+ dbMutationLog.execute(sql`UPDATE ${SYNC_STATUS_TABLE} SET head = ${head.global}`)
@@ -12,54 +12,56 @@ export const recreateDb: Effect.Effect<
12
12
  UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
13
  LeaderThreadCtx | HttpClient.HttpClient
14
14
  > = Effect.gen(function* () {
15
- const { db, dbLog, makeSyncDb, schema, bootStatusQueue } = yield* LeaderThreadCtx
15
+ const { dbReadModel, dbMutationLog, schema, bootStatusQueue } = yield* LeaderThreadCtx
16
16
 
17
17
  const migrationOptions = schema.migrationOptions
18
18
 
19
19
  yield* Effect.addFinalizer(
20
20
  Effect.fn('recreateDb:finalizer')(function* (ex) {
21
- if (ex._tag === 'Failure') db.destroy()
21
+ if (ex._tag === 'Failure') dbReadModel.destroy()
22
22
  }),
23
23
  )
24
24
 
25
25
  // NOTE to speed up the operations below, we're creating a temporary in-memory database
26
26
  // and later we'll overwrite the persisted database with the new data
27
- const tmpSyncDb = yield* makeSyncDb({ _tag: 'in-memory' })
28
- yield* configureConnection(tmpSyncDb, { fkEnabled: true })
27
+ // TODO bring back this optimization
28
+ // const tmpDb = yield* makeSqliteDb({ _tag: 'in-memory' })
29
+ const tmpDb = dbReadModel
30
+ yield* configureConnection(tmpDb, { fkEnabled: true })
29
31
 
30
32
  const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
31
33
  Effect.gen(function* () {
32
- yield* Effect.tryAll(() => hooks?.init?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
34
+ yield* Effect.tryAll(() => hooks?.init?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
33
35
 
34
36
  yield* migrateDb({
35
- db: tmpSyncDb,
37
+ db: tmpDb,
36
38
  schema,
37
39
  onProgress: ({ done, total }) =>
38
40
  Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
39
41
  })
40
42
 
41
- initializeSingletonTables(schema, tmpSyncDb)
43
+ initializeSingletonTables(schema, tmpDb)
42
44
 
43
- yield* Effect.tryAll(() => hooks?.pre?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
45
+ yield* Effect.tryAll(() => hooks?.pre?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
44
46
 
45
- return tmpSyncDb
47
+ return tmpDb
46
48
  })
47
49
 
48
50
  switch (migrationOptions.strategy) {
49
51
  case 'from-mutation-log': {
50
52
  const hooks = migrationOptions.hooks
51
- const tmpSyncDb = yield* initDb(hooks)
53
+ const tmpDb = yield* initDb(hooks)
52
54
 
53
55
  yield* rehydrateFromMutationLog({
54
- db: tmpSyncDb,
55
- logDb: dbLog,
56
+ db: tmpDb,
57
+ logDb: dbMutationLog,
56
58
  schema,
57
59
  migrationOptions,
58
60
  onProgress: ({ done, total }) =>
59
61
  Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
60
62
  })
61
63
 
62
- yield* Effect.tryAll(() => hooks?.post?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
64
+ yield* Effect.tryAll(() => hooks?.post?.(tmpDb)).pipe(UnexpectedError.mapToUnexpectedError)
63
65
 
64
66
  break
65
67
  }
@@ -74,13 +76,13 @@ export const recreateDb: Effect.Effect<
74
76
  break
75
77
  }
76
78
  case 'manual': {
77
- const oldDbData = db.export()
79
+ const oldDbData = dbReadModel.export()
78
80
 
79
81
  const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
80
82
  UnexpectedError.mapToUnexpectedError,
81
83
  )
82
84
 
83
- tmpSyncDb.import(newDbData)
85
+ tmpDb.import(newDbData)
84
86
 
85
87
  // TODO validate schema
86
88
 
@@ -91,17 +93,19 @@ export const recreateDb: Effect.Effect<
91
93
  }
92
94
  }
93
95
 
96
+ // TODO bring back
94
97
  // Import the temporary in-memory database into the persistent database
95
- yield* Effect.sync(() => db.import(tmpSyncDb)).pipe(
96
- Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
97
- )
98
+ // yield* Effect.sync(() => db.import(tmpDb)).pipe(
99
+ // Effect.withSpan('@livestore/common:leader-thread:recreateDb:import'),
100
+ // )
98
101
 
99
102
  // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
100
103
  // We've disabled this for now as it made the code too complex, as we often run syncing right after
101
104
  // so the snapshot is no longer up to date
102
- // const snapshotFromTmpDb = tmpSyncDb.export()
105
+ // const snapshotFromTmpDb = tmpDb.export()
103
106
 
104
- tmpSyncDb.close()
107
+ // TODO bring back
108
+ // tmpDb.close()
105
109
  }).pipe(
106
110
  Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
107
111
  Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
@@ -6,6 +6,7 @@ import type {
6
6
  Option,
7
7
  Queue,
8
8
  Scope,
9
+ Subscribable,
9
10
  SubscriptionRef,
10
11
  WebChannel,
11
12
  } from '@livestore/utils/effect'
@@ -15,14 +16,14 @@ import type {
15
16
  BootStatus,
16
17
  Devtools,
17
18
  InvalidPushError,
18
- MakeSynchronousDatabase,
19
+ MakeSqliteDb,
19
20
  PersistenceInfo,
21
+ SqliteDb,
20
22
  SyncBackend,
21
- SynchronousDatabase,
22
23
  UnexpectedError,
23
24
  } from '../index.js'
24
25
  import type { EventId, LiveStoreSchema, MutationEvent } from '../schema/mod.js'
25
- import type { PayloadUpstream, SyncState } from '../sync/syncstate.js'
26
+ import type * as SyncState from '../sync/syncstate.js'
26
27
  import type { ShutdownChannel } from './shutdown-channel.js'
27
28
 
28
29
  export type ShutdownState = 'running' | 'shutting-down'
@@ -55,8 +56,8 @@ export type InitialSyncInfo = Option.Option<{
55
56
  // | { _tag: 'Recreate'; snapshotRef: Ref.Ref<Uint8Array | undefined>; syncInfo: InitialSyncInfo }
56
57
  // | { _tag: 'Reuse'; syncInfo: InitialSyncInfo }
57
58
 
58
- export type LeaderDatabase = SynchronousDatabase<{ dbPointer: number; persistenceInfo: PersistenceInfo }>
59
- export type PersistenceInfoPair = { db: PersistenceInfo; mutationLog: PersistenceInfo }
59
+ export type LeaderSqliteDb = SqliteDb<{ dbPointer: number; persistenceInfo: PersistenceInfo }>
60
+ export type PersistenceInfoPair = { readModel: PersistenceInfo; mutationLog: PersistenceInfo }
60
61
 
61
62
  export type DevtoolsOptions =
62
63
  | {
@@ -64,10 +65,9 @@ export type DevtoolsOptions =
64
65
  }
65
66
  | {
66
67
  enabled: true
67
- makeContext: Effect.Effect<
68
+ makeBootContext: Effect.Effect<
68
69
  {
69
70
  devtoolsWebChannel: WebChannel.WebChannel<Devtools.MessageToAppLeader, Devtools.MessageFromAppLeader>
70
- shutdownChannel: ShutdownChannel
71
71
  persistenceInfo: PersistenceInfoPair
72
72
  },
73
73
  UnexpectedError,
@@ -75,23 +75,40 @@ export type DevtoolsOptions =
75
75
  >
76
76
  }
77
77
 
78
+ export type DevtoolsContext =
79
+ | {
80
+ enabled: true
81
+ syncBackendPullLatch: Effect.Latch
82
+ syncBackendPushLatch: Effect.Latch
83
+ }
84
+ | {
85
+ enabled: false
86
+ }
87
+
78
88
  export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
79
89
  LeaderThreadCtx,
80
90
  {
81
91
  schema: LiveStoreSchema
82
92
  storeId: string
83
- originId: string
84
- makeSyncDb: MakeSynchronousDatabase
85
- db: LeaderDatabase
86
- dbLog: LeaderDatabase
93
+ clientId: string
94
+ makeSqliteDb: MakeSqliteDb
95
+ dbReadModel: LeaderSqliteDb
96
+ dbMutationLog: LeaderSqliteDb
87
97
  bootStatusQueue: Queue.Queue<BootStatus>
88
98
  // TODO we should find a more elegant way to handle cases which need this ref for their implementation
89
99
  shutdownStateSubRef: SubscriptionRef.SubscriptionRef<ShutdownState>
100
+ shutdownChannel: ShutdownChannel
90
101
  mutationEventSchema: MutationEvent.ForMutationDefRecord<any>
91
- // devtools: DevtoolsContext
102
+ devtools: DevtoolsContext
92
103
  syncBackend: SyncBackend | undefined
93
- syncProcessor: SyncProcessor
104
+ syncProcessor: LeaderSyncProcessor
94
105
  connectedClientSessionPullQueues: PullQueueSet
106
+ /**
107
+ * e.g. used for `store._dev` APIs
108
+ *
109
+ * This is currently separated from `.devtools` as it also needs to work when devtools are disabled
110
+ */
111
+ extraIncomingMessagesQueue: Queue.Queue<Devtools.MessageToAppLeader>
95
112
  }
96
113
  >() {}
97
114
 
@@ -101,24 +118,28 @@ export type InitialBlockingSyncContext = {
101
118
  }
102
119
 
103
120
  export type PullQueueItem = {
104
- // mutationEvents: ReadonlyArray<MutationEvent.AnyEncoded>
105
- // backendHead: number
106
- payload: PayloadUpstream
107
- // TODO move `remaining` into `PayloadUpstream`
121
+ payload: SyncState.PayloadUpstream
108
122
  remaining: number
109
123
  }
110
124
 
111
- export interface SyncProcessor {
125
+ export interface LeaderSyncProcessor {
112
126
  push: (
113
127
  /** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
114
128
  batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
115
- ) => Effect.Effect<void, UnexpectedError | InvalidPushError, HttpClient.HttpClient | LeaderThreadCtx>
129
+ options?: {
130
+ /**
131
+ * If true, the effect will only finish when the local push has been processed (i.e. succeeded or was rejected).
132
+ * @default false
133
+ */
134
+ waitForProcessing?: boolean
135
+ },
136
+ ) => Effect.Effect<void, InvalidPushError>
116
137
 
117
138
  pushPartial: (mutationEvent: MutationEvent.PartialAnyEncoded) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
118
139
  boot: (args: {
119
140
  dbReady: Deferred.Deferred<void>
120
141
  }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient>
121
- syncState: Effect.Effect<SyncState, UnexpectedError>
142
+ syncState: Subscribable.Subscribable<SyncState.SyncState>
122
143
  }
123
144
 
124
145
  export interface PullQueueSet {
package/src/mutation.ts CHANGED
@@ -8,10 +8,19 @@ import { prepareBindValues } from './util.js'
8
8
 
9
9
  export const getExecArgsFromMutation = ({
10
10
  mutationDef,
11
- mutationEventDecoded,
11
+ mutationEvent,
12
12
  }: {
13
13
  mutationDef: MutationDef.Any
14
- mutationEventDecoded: MutationEvent.Any | MutationEvent.PartialAny
14
+ /** Both encoded and decoded mutation events are supported to reduce the number of times we need to decode/encode */
15
+ mutationEvent:
16
+ | {
17
+ decoded: MutationEvent.AnyDecoded | MutationEvent.PartialAnyDecoded
18
+ encoded: undefined
19
+ }
20
+ | {
21
+ decoded: undefined
22
+ encoded: MutationEvent.AnyEncoded | MutationEvent.PartialAnyEncoded
23
+ }
15
24
  }): ReadonlyArray<{
16
25
  statementSql: string
17
26
  bindValues: PreparedBindValues
@@ -23,7 +32,9 @@ export const getExecArgsFromMutation = ({
23
32
 
24
33
  switch (typeof mutationDef.sql) {
25
34
  case 'function': {
26
- const res = mutationDef.sql(mutationEventDecoded.args)
35
+ const mutationArgsDecoded =
36
+ mutationEvent.decoded?.args ?? Schema.decodeUnknownSync(mutationDef.schema)(mutationEvent.encoded!.args)
37
+ const res = mutationDef.sql(mutationArgsDecoded)
27
38
  statementRes = Array.isArray(res) ? res : [res]
28
39
  break
29
40
  }
@@ -40,10 +51,9 @@ export const getExecArgsFromMutation = ({
40
51
  return statementRes.map((statementRes) => {
41
52
  const statementSql = typeof statementRes === 'string' ? statementRes : statementRes.sql
42
53
 
43
- const bindValues =
44
- typeof statementRes === 'string'
45
- ? Schema.encodeUnknownSync(mutationDef.schema)(mutationEventDecoded.args)
46
- : statementRes.bindValues
54
+ const mutationArgsEncoded =
55
+ mutationEvent.encoded?.args ?? Schema.encodeUnknownSync(mutationDef.schema)(mutationEvent.decoded!.args)
56
+ const bindValues = typeof statementRes === 'string' ? mutationArgsEncoded : statementRes.bindValues
47
57
 
48
58
  const writeTables = typeof statementRes === 'string' ? undefined : statementRes.writeTables
49
59
 
@@ -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
- import { type MigrationOptionsFromMutationLog, type SynchronousDatabase, UnexpectedError } from './adapter-types.js'
5
- import { getExecArgsFromMutation } from './mutation.js'
4
+ import { type MigrationOptionsFromMutationLog, type SqliteDb, UnexpectedError } from './adapter-types.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,13 +10,14 @@ 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,
17
18
  }: {
18
- logDb: SynchronousDatabase
19
- db: SynchronousDatabase
19
+ logDb: SqliteDb
20
+ db: SqliteDb
20
21
  schema: LiveStoreSchema
21
22
  migrationOptions: MigrationOptionsFromMutationLog
22
23
  onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
@@ -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>