@livestore/common 0.2.0 → 0.3.0-dev.0

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 (244) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +163 -1
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/fixture.js +3 -1
  5. package/dist/__tests__/fixture.js.map +1 -1
  6. package/dist/adapter-types.d.ts +53 -38
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -7
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/bounded-collections.d.ts +2 -2
  11. package/dist/bounded-collections.d.ts.map +1 -1
  12. package/dist/debug-info.d.ts +13 -13
  13. package/dist/derived-mutations.d.ts +1 -1
  14. package/dist/derived-mutations.d.ts.map +1 -1
  15. package/dist/devtools/devtools-bridge.d.ts +2 -2
  16. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  17. package/dist/devtools/devtools-messages.d.ts +84 -196
  18. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  19. package/dist/devtools/devtools-messages.js +55 -61
  20. package/dist/devtools/devtools-messages.js.map +1 -1
  21. package/dist/devtools/index.d.ts.map +1 -1
  22. package/dist/devtools/index.js +1 -2
  23. package/dist/devtools/index.js.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/init-singleton-tables.d.ts +1 -1
  29. package/dist/init-singleton-tables.d.ts.map +1 -1
  30. package/dist/leader-thread/apply-mutation.d.ts +8 -0
  31. package/dist/leader-thread/apply-mutation.d.ts.map +1 -0
  32. package/dist/leader-thread/apply-mutation.js +95 -0
  33. package/dist/leader-thread/apply-mutation.js.map +1 -0
  34. package/dist/leader-thread/connection.d.ts +11 -0
  35. package/dist/leader-thread/connection.d.ts.map +1 -0
  36. package/dist/leader-thread/connection.js +44 -0
  37. package/dist/leader-thread/connection.js.map +1 -0
  38. package/dist/leader-thread/leader-sync-processor.d.ts +47 -0
  39. package/dist/leader-thread/leader-sync-processor.d.ts.map +1 -0
  40. package/dist/leader-thread/leader-sync-processor.js +422 -0
  41. package/dist/leader-thread/leader-sync-processor.js.map +1 -0
  42. package/dist/leader-thread/leader-worker-devtools.d.ts +6 -0
  43. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -0
  44. package/dist/leader-thread/leader-worker-devtools.js +216 -0
  45. package/dist/leader-thread/leader-worker-devtools.js.map +1 -0
  46. package/dist/leader-thread/make-leader-thread-layer.d.ts +20 -0
  47. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -0
  48. package/dist/leader-thread/make-leader-thread-layer.js +106 -0
  49. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -0
  50. package/dist/leader-thread/mod.d.ts +7 -0
  51. package/dist/leader-thread/mod.d.ts.map +1 -0
  52. package/dist/leader-thread/mod.js +7 -0
  53. package/dist/leader-thread/mod.js.map +1 -0
  54. package/dist/leader-thread/mutationlog.d.ts +23 -0
  55. package/dist/leader-thread/mutationlog.d.ts.map +1 -0
  56. package/dist/leader-thread/mutationlog.js +27 -0
  57. package/dist/leader-thread/mutationlog.js.map +1 -0
  58. package/dist/leader-thread/pull-queue-set.d.ts +7 -0
  59. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -0
  60. package/dist/leader-thread/pull-queue-set.js +39 -0
  61. package/dist/leader-thread/pull-queue-set.js.map +1 -0
  62. package/dist/leader-thread/recreate-db.d.ts +7 -0
  63. package/dist/leader-thread/recreate-db.d.ts.map +1 -0
  64. package/dist/leader-thread/recreate-db.js +69 -0
  65. package/dist/leader-thread/recreate-db.js.map +1 -0
  66. package/dist/leader-thread/shutdown-channel.d.ts +15 -0
  67. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -0
  68. package/dist/leader-thread/shutdown-channel.js +7 -0
  69. package/dist/leader-thread/shutdown-channel.js.map +1 -0
  70. package/dist/leader-thread/types.d.ts +87 -0
  71. package/dist/leader-thread/types.d.ts.map +1 -0
  72. package/dist/leader-thread/types.js +11 -0
  73. package/dist/leader-thread/types.js.map +1 -0
  74. package/dist/mutation.d.ts +3 -4
  75. package/dist/mutation.d.ts.map +1 -1
  76. package/dist/mutation.js +0 -14
  77. package/dist/mutation.js.map +1 -1
  78. package/dist/otel.d.ts +7 -0
  79. package/dist/otel.d.ts.map +1 -0
  80. package/dist/otel.js +11 -0
  81. package/dist/otel.js.map +1 -0
  82. package/dist/query-builder/api.d.ts +2 -2
  83. package/dist/query-builder/api.d.ts.map +1 -1
  84. package/dist/query-builder/api.js.map +1 -1
  85. package/dist/query-builder/impl.d.ts +1 -1
  86. package/dist/query-builder/impl.d.ts.map +1 -1
  87. package/dist/query-builder/impl.js +21 -4
  88. package/dist/query-builder/impl.js.map +1 -1
  89. package/dist/query-builder/impl.test.js +24 -1
  90. package/dist/query-builder/impl.test.js.map +1 -1
  91. package/dist/query-info.d.ts +1 -1
  92. package/dist/query-info.d.ts.map +1 -1
  93. package/dist/rehydrate-from-mutationlog.d.ts +1 -1
  94. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  95. package/dist/rehydrate-from-mutationlog.js +6 -6
  96. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  97. package/dist/schema/EventId.d.ts +37 -0
  98. package/dist/schema/EventId.d.ts.map +1 -0
  99. package/dist/schema/EventId.js +30 -0
  100. package/dist/schema/EventId.js.map +1 -0
  101. package/dist/schema/MutationEvent.d.ts +191 -0
  102. package/dist/schema/MutationEvent.d.ts.map +1 -0
  103. package/dist/schema/MutationEvent.js +56 -0
  104. package/dist/schema/MutationEvent.js.map +1 -0
  105. package/dist/schema/mod.d.ts +8 -0
  106. package/dist/schema/mod.d.ts.map +1 -0
  107. package/dist/schema/mod.js +8 -0
  108. package/dist/schema/mod.js.map +1 -0
  109. package/dist/schema/mutations.d.ts +3 -123
  110. package/dist/schema/mutations.d.ts.map +1 -1
  111. package/dist/schema/mutations.js +0 -26
  112. package/dist/schema/mutations.js.map +1 -1
  113. package/dist/schema/{index.d.ts → schema.d.ts} +1 -5
  114. package/dist/schema/schema.d.ts.map +1 -0
  115. package/dist/schema/{index.js → schema.js} +1 -5
  116. package/dist/schema/schema.js.map +1 -0
  117. package/dist/schema/system-tables.d.ts +55 -29
  118. package/dist/schema/system-tables.d.ts.map +1 -1
  119. package/dist/schema/system-tables.js +10 -5
  120. package/dist/schema/system-tables.js.map +1 -1
  121. package/dist/schema-management/migrations.d.ts +1 -1
  122. package/dist/schema-management/migrations.d.ts.map +1 -1
  123. package/dist/schema-management/migrations.js +6 -1
  124. package/dist/schema-management/migrations.js.map +1 -1
  125. package/dist/schema-management/validate-mutation-defs.d.ts +1 -1
  126. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
  127. package/dist/sync/client-session-sync-processor.d.ts +45 -0
  128. package/dist/sync/client-session-sync-processor.d.ts.map +1 -0
  129. package/dist/sync/client-session-sync-processor.js +131 -0
  130. package/dist/sync/client-session-sync-processor.js.map +1 -0
  131. package/dist/sync/index.d.ts +2 -0
  132. package/dist/sync/index.d.ts.map +1 -1
  133. package/dist/sync/index.js +2 -0
  134. package/dist/sync/index.js.map +1 -1
  135. package/dist/sync/next/compact-events.d.ts +1 -1
  136. package/dist/sync/next/compact-events.d.ts.map +1 -1
  137. package/dist/sync/next/compact-events.js +2 -1
  138. package/dist/sync/next/compact-events.js.map +1 -1
  139. package/dist/sync/next/facts.d.ts +5 -5
  140. package/dist/sync/next/facts.d.ts.map +1 -1
  141. package/dist/sync/next/facts.js +1 -1
  142. package/dist/sync/next/facts.js.map +1 -1
  143. package/dist/sync/next/history-dag-common.d.ts +30 -0
  144. package/dist/sync/next/history-dag-common.d.ts.map +1 -0
  145. package/dist/sync/next/history-dag-common.js +20 -0
  146. package/dist/sync/next/history-dag-common.js.map +1 -0
  147. package/dist/sync/next/history-dag.d.ts +4 -27
  148. package/dist/sync/next/history-dag.d.ts.map +1 -1
  149. package/dist/sync/next/history-dag.js +1 -19
  150. package/dist/sync/next/history-dag.js.map +1 -1
  151. package/dist/sync/next/mod.d.ts +1 -0
  152. package/dist/sync/next/mod.d.ts.map +1 -1
  153. package/dist/sync/next/mod.js +1 -0
  154. package/dist/sync/next/mod.js.map +1 -1
  155. package/dist/sync/next/rebase-events.d.ts +3 -2
  156. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  157. package/dist/sync/next/rebase-events.js.map +1 -1
  158. package/dist/sync/next/test/compact-events.test.d.ts.map +1 -1
  159. package/dist/sync/next/test/compact-events.test.js +2 -1
  160. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  161. package/dist/sync/next/test/mutation-fixtures.d.ts +1 -1
  162. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -1
  163. package/dist/sync/next/test/mutation-fixtures.js +4 -3
  164. package/dist/sync/next/test/mutation-fixtures.js.map +1 -1
  165. package/dist/sync/sync.d.ts +33 -12
  166. package/dist/sync/sync.d.ts.map +1 -1
  167. package/dist/sync/sync.js +10 -1
  168. package/dist/sync/sync.js.map +1 -1
  169. package/dist/sync/syncstate.d.ts +123 -0
  170. package/dist/sync/syncstate.d.ts.map +1 -0
  171. package/dist/sync/syncstate.js +248 -0
  172. package/dist/sync/syncstate.js.map +1 -0
  173. package/dist/sync/syncstate.test.d.ts +2 -0
  174. package/dist/sync/syncstate.test.d.ts.map +1 -0
  175. package/dist/sync/syncstate.test.js +399 -0
  176. package/dist/sync/syncstate.test.js.map +1 -0
  177. package/dist/sync/validate-push-payload.d.ts +5 -0
  178. package/dist/sync/validate-push-payload.d.ts.map +1 -0
  179. package/dist/sync/validate-push-payload.js +15 -0
  180. package/dist/sync/validate-push-payload.js.map +1 -0
  181. package/dist/util.d.ts +2 -2
  182. package/dist/util.d.ts.map +1 -1
  183. package/dist/version.d.ts +2 -2
  184. package/dist/version.d.ts.map +1 -1
  185. package/dist/version.js +2 -2
  186. package/dist/version.js.map +1 -1
  187. package/package.json +13 -6
  188. package/src/__tests__/fixture.ts +5 -1
  189. package/src/adapter-types.ts +60 -34
  190. package/src/derived-mutations.test.ts +1 -1
  191. package/src/derived-mutations.ts +1 -1
  192. package/src/devtools/devtools-bridge.ts +2 -2
  193. package/src/devtools/devtools-messages.ts +70 -74
  194. package/src/devtools/index.ts +1 -2
  195. package/src/index.ts +2 -1
  196. package/src/init-singleton-tables.ts +1 -1
  197. package/src/leader-thread/apply-mutation.ts +143 -0
  198. package/src/leader-thread/connection.ts +67 -0
  199. package/src/leader-thread/leader-sync-processor.ts +666 -0
  200. package/src/leader-thread/leader-worker-devtools.ts +358 -0
  201. package/src/leader-thread/make-leader-thread-layer.ts +192 -0
  202. package/src/leader-thread/mod.ts +6 -0
  203. package/src/leader-thread/mutationlog.ts +42 -0
  204. package/src/leader-thread/pull-queue-set.ts +58 -0
  205. package/src/leader-thread/recreate-db.ts +109 -0
  206. package/src/leader-thread/shutdown-channel.ts +13 -0
  207. package/src/leader-thread/types.ts +129 -0
  208. package/src/mutation.ts +3 -21
  209. package/src/otel.ts +20 -0
  210. package/src/query-builder/api.ts +3 -2
  211. package/src/query-builder/impl.test.ts +28 -1
  212. package/src/query-builder/impl.ts +21 -5
  213. package/src/query-info.ts +1 -1
  214. package/src/rehydrate-from-mutationlog.ts +7 -11
  215. package/src/schema/EventId.ts +46 -0
  216. package/src/schema/MutationEvent.ts +161 -0
  217. package/src/schema/mod.ts +7 -0
  218. package/src/schema/mutations.ts +5 -126
  219. package/src/schema/{index.ts → schema.ts} +0 -5
  220. package/src/schema/system-tables.ts +18 -5
  221. package/src/schema-management/migrations.ts +9 -2
  222. package/src/schema-management/validate-mutation-defs.ts +1 -1
  223. package/src/sync/client-session-sync-processor.ts +207 -0
  224. package/src/sync/index.ts +2 -0
  225. package/src/sync/next/compact-events.ts +3 -2
  226. package/src/sync/next/facts.ts +11 -5
  227. package/src/sync/next/history-dag-common.ts +44 -0
  228. package/src/sync/next/history-dag.ts +3 -45
  229. package/src/sync/next/mod.ts +1 -0
  230. package/src/sync/next/rebase-events.ts +6 -5
  231. package/src/sync/next/test/compact-events.test.ts +3 -2
  232. package/src/sync/next/test/mutation-fixtures.ts +7 -6
  233. package/src/sync/sync.ts +32 -12
  234. package/src/sync/syncstate.test.ts +464 -0
  235. package/src/sync/syncstate.ts +385 -0
  236. package/src/sync/validate-push-payload.ts +18 -0
  237. package/src/version.ts +2 -2
  238. package/dist/schema/index.d.ts.map +0 -1
  239. package/dist/schema/index.js.map +0 -1
  240. package/dist/sync/next-mutation-event-id-pair.d.ts +0 -14
  241. package/dist/sync/next-mutation-event-id-pair.d.ts.map +0 -1
  242. package/dist/sync/next-mutation-event-id-pair.js +0 -13
  243. package/dist/sync/next-mutation-event-id-pair.js.map +0 -1
  244. package/src/sync/next-mutation-event-id-pair.ts +0 -20
@@ -0,0 +1,109 @@
1
+ import { casesHandled } from '@livestore/utils'
2
+ import type { HttpClient } from '@livestore/utils/effect'
3
+ import { Effect, Queue } from '@livestore/utils/effect'
4
+
5
+ import type { InvalidPullError, IsOfflineError, MigrationHooks, SqliteError } from '../index.js'
6
+ import { initializeSingletonTables, migrateDb, rehydrateFromMutationLog, UnexpectedError } from '../index.js'
7
+ import { configureConnection } from './connection.js'
8
+ import { LeaderThreadCtx } from './types.js'
9
+
10
+ export const recreateDb: Effect.Effect<
11
+ void,
12
+ UnexpectedError | SqliteError | IsOfflineError | InvalidPullError,
13
+ LeaderThreadCtx | HttpClient.HttpClient
14
+ > = Effect.gen(function* () {
15
+ const { db, dbLog, makeSyncDb, schema, bootStatusQueue } = yield* LeaderThreadCtx
16
+
17
+ const migrationOptions = schema.migrationOptions
18
+
19
+ yield* Effect.addFinalizer(
20
+ Effect.fn('recreateDb:finalizer')(function* (ex) {
21
+ if (ex._tag === 'Failure') db.destroy()
22
+ }),
23
+ )
24
+
25
+ // NOTE to speed up the operations below, we're creating a temporary in-memory database
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 })
29
+
30
+ const initDb = (hooks: Partial<MigrationHooks> | undefined) =>
31
+ Effect.gen(function* () {
32
+ yield* Effect.tryAll(() => hooks?.init?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
33
+
34
+ yield* migrateDb({
35
+ db: tmpSyncDb,
36
+ schema,
37
+ onProgress: ({ done, total }) =>
38
+ Queue.offer(bootStatusQueue, { stage: 'migrating', progress: { done, total } }),
39
+ })
40
+
41
+ initializeSingletonTables(schema, tmpSyncDb)
42
+
43
+ yield* Effect.tryAll(() => hooks?.pre?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
44
+
45
+ return tmpSyncDb
46
+ })
47
+
48
+ switch (migrationOptions.strategy) {
49
+ case 'from-mutation-log': {
50
+ const hooks = migrationOptions.hooks
51
+ const tmpSyncDb = yield* initDb(hooks)
52
+
53
+ yield* rehydrateFromMutationLog({
54
+ db: tmpSyncDb,
55
+ logDb: dbLog,
56
+ schema,
57
+ migrationOptions,
58
+ onProgress: ({ done, total }) =>
59
+ Queue.offer(bootStatusQueue, { stage: 'rehydrating', progress: { done, total } }),
60
+ })
61
+
62
+ yield* Effect.tryAll(() => hooks?.post?.(tmpSyncDb)).pipe(UnexpectedError.mapToUnexpectedError)
63
+
64
+ break
65
+ }
66
+ case 'hard-reset': {
67
+ const hooks = migrationOptions.hooks
68
+ const tmpInMemoryDb = yield* initDb(hooks)
69
+
70
+ // The database is migrated but empty now, so nothing else to do
71
+
72
+ yield* Effect.tryAll(() => hooks?.post?.(tmpInMemoryDb)).pipe(UnexpectedError.mapToUnexpectedError)
73
+
74
+ break
75
+ }
76
+ case 'manual': {
77
+ const oldDbData = db.export()
78
+
79
+ const newDbData = yield* Effect.tryAll(() => migrationOptions.migrate(oldDbData)).pipe(
80
+ UnexpectedError.mapToUnexpectedError,
81
+ )
82
+
83
+ tmpSyncDb.import(newDbData)
84
+
85
+ // TODO validate schema
86
+
87
+ break
88
+ }
89
+ default: {
90
+ casesHandled(migrationOptions)
91
+ }
92
+ }
93
+
94
+ // 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
+
99
+ // TODO maybe bring back re-using this initial snapshot to avoid calling `.export()` again
100
+ // We've disabled this for now as it made the code too complex, as we often run syncing right after
101
+ // so the snapshot is no longer up to date
102
+ // const snapshotFromTmpDb = tmpSyncDb.export()
103
+
104
+ tmpSyncDb.close()
105
+ }).pipe(
106
+ Effect.scoped, // NOTE we're closing the scope here so finalizers are called when the effect is done
107
+ Effect.withSpan('@livestore/common:leader-thread:recreateDb'),
108
+ Effect.withPerformanceMeasure('@livestore/common:leader-thread:recreateDb'),
109
+ )
@@ -0,0 +1,13 @@
1
+ import type { WebChannel } from '@livestore/utils/effect'
2
+ import { Schema } from '@livestore/utils/effect'
3
+
4
+ import { IntentionalShutdownCause } from '../index.js'
5
+
6
+ export class DedicatedWorkerDisconnectBroadcast extends Schema.TaggedStruct('DedicatedWorkerDisconnectBroadcast', {}) {}
7
+
8
+ export class All extends Schema.Union(IntentionalShutdownCause, DedicatedWorkerDisconnectBroadcast) {}
9
+
10
+ /**
11
+ * Used internally by an adapter to shutdown gracefully.
12
+ */
13
+ export type ShutdownChannel = WebChannel.WebChannel<typeof All.Type, typeof All.Type>
@@ -0,0 +1,129 @@
1
+ import type {
2
+ Deferred,
3
+ Effect,
4
+ Fiber,
5
+ HttpClient,
6
+ Option,
7
+ Queue,
8
+ Scope,
9
+ SubscriptionRef,
10
+ WebChannel,
11
+ } from '@livestore/utils/effect'
12
+ import { Context, Schema } from '@livestore/utils/effect'
13
+
14
+ import type {
15
+ BootStatus,
16
+ Devtools,
17
+ InvalidPushError,
18
+ MakeSynchronousDatabase,
19
+ PersistenceInfo,
20
+ SyncBackend,
21
+ SynchronousDatabase,
22
+ UnexpectedError,
23
+ } from '../index.js'
24
+ import type { EventId, LiveStoreSchema, MutationEvent } from '../schema/mod.js'
25
+ import type { PayloadUpstream, SyncState } from '../sync/syncstate.js'
26
+ import type { ShutdownChannel } from './shutdown-channel.js'
27
+
28
+ export type ShutdownState = 'running' | 'shutting-down'
29
+
30
+ export class OuterWorkerCtx extends Context.Tag('OuterWorkerCtx')<
31
+ OuterWorkerCtx,
32
+ {
33
+ innerFiber: Fiber.RuntimeFiber<any, any>
34
+ }
35
+ >() {}
36
+
37
+ export const InitialSyncOptionsSkip = Schema.TaggedStruct('Skip', {})
38
+ export type InitialSyncOptionsSkip = typeof InitialSyncOptionsSkip.Type
39
+
40
+ export const InitialSyncOptionsBlocking = Schema.TaggedStruct('Blocking', {
41
+ timeout: Schema.DurationFromMillis,
42
+ })
43
+
44
+ export type InitialSyncOptionsBlocking = typeof InitialSyncOptionsBlocking.Type
45
+
46
+ export const InitialSyncOptions = Schema.Union(InitialSyncOptionsSkip, InitialSyncOptionsBlocking)
47
+ export type InitialSyncOptions = typeof InitialSyncOptions.Type
48
+
49
+ export type InitialSyncInfo = Option.Option<{
50
+ cursor: EventId.EventId
51
+ metadata: Option.Option<Schema.JsonValue>
52
+ }>
53
+
54
+ // export type InitialSetup =
55
+ // | { _tag: 'Recreate'; snapshotRef: Ref.Ref<Uint8Array | undefined>; syncInfo: InitialSyncInfo }
56
+ // | { _tag: 'Reuse'; syncInfo: InitialSyncInfo }
57
+
58
+ export type LeaderDatabase = SynchronousDatabase<{ dbPointer: number; persistenceInfo: PersistenceInfo }>
59
+ export type PersistenceInfoPair = { db: PersistenceInfo; mutationLog: PersistenceInfo }
60
+
61
+ export type DevtoolsOptions =
62
+ | {
63
+ enabled: false
64
+ }
65
+ | {
66
+ enabled: true
67
+ makeContext: Effect.Effect<
68
+ {
69
+ devtoolsWebChannel: WebChannel.WebChannel<Devtools.MessageToAppLeader, Devtools.MessageFromAppLeader>
70
+ shutdownChannel: ShutdownChannel
71
+ persistenceInfo: PersistenceInfoPair
72
+ },
73
+ UnexpectedError,
74
+ Scope.Scope
75
+ >
76
+ }
77
+
78
+ export class LeaderThreadCtx extends Context.Tag('LeaderThreadCtx')<
79
+ LeaderThreadCtx,
80
+ {
81
+ schema: LiveStoreSchema
82
+ storeId: string
83
+ originId: string
84
+ makeSyncDb: MakeSynchronousDatabase
85
+ db: LeaderDatabase
86
+ dbLog: LeaderDatabase
87
+ bootStatusQueue: Queue.Queue<BootStatus>
88
+ // TODO we should find a more elegant way to handle cases which need this ref for their implementation
89
+ shutdownStateSubRef: SubscriptionRef.SubscriptionRef<ShutdownState>
90
+ mutationEventSchema: MutationEvent.ForMutationDefRecord<any>
91
+ // devtools: DevtoolsContext
92
+ syncBackend: SyncBackend | undefined
93
+ syncProcessor: SyncProcessor
94
+ connectedClientSessionPullQueues: PullQueueSet
95
+ }
96
+ >() {}
97
+
98
+ export type InitialBlockingSyncContext = {
99
+ blockingDeferred: Deferred.Deferred<void> | undefined
100
+ update: (_: { remaining: number; processed: number }) => Effect.Effect<void>
101
+ }
102
+
103
+ export type PullQueueItem = {
104
+ // mutationEvents: ReadonlyArray<MutationEvent.AnyEncoded>
105
+ // backendHead: number
106
+ payload: PayloadUpstream
107
+ // TODO move `remaining` into `PayloadUpstream`
108
+ remaining: number
109
+ }
110
+
111
+ export interface SyncProcessor {
112
+ push: (
113
+ /** `batch` needs to follow the same rules as `batch` in `SyncBackend.push` */
114
+ batch: ReadonlyArray<MutationEvent.EncodedWithMeta>,
115
+ ) => Effect.Effect<void, UnexpectedError | InvalidPushError, HttpClient.HttpClient | LeaderThreadCtx>
116
+
117
+ pushPartial: (mutationEvent: MutationEvent.PartialAnyEncoded) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
118
+ boot: (args: {
119
+ dbReady: Deferred.Deferred<void>
120
+ }) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx | Scope.Scope | HttpClient.HttpClient>
121
+ syncState: Effect.Effect<SyncState, UnexpectedError>
122
+ }
123
+
124
+ export interface PullQueueSet {
125
+ makeQueue: (
126
+ since: EventId.EventId,
127
+ ) => Effect.Effect<Queue.Queue<PullQueueItem>, UnexpectedError, Scope.Scope | LeaderThreadCtx>
128
+ offer: (item: PullQueueItem) => Effect.Effect<void, UnexpectedError, LeaderThreadCtx>
129
+ }
package/src/mutation.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
2
1
  import { Schema } from '@livestore/utils/effect'
3
2
 
4
3
  import { SessionIdSymbol } from './adapter-types.js'
5
- import type { LiveStoreSchema } from './schema/index.js'
6
- import type { MutationDef, MutationEvent } from './schema/mutations.js'
4
+ import type * as MutationEvent from './schema/MutationEvent.js'
5
+ import type { MutationDef } from './schema/mutations.js'
7
6
  import type { PreparedBindValues } from './util.js'
8
7
  import { prepareBindValues } from './util.js'
9
8
 
@@ -12,7 +11,7 @@ export const getExecArgsFromMutation = ({
12
11
  mutationEventDecoded,
13
12
  }: {
14
13
  mutationDef: MutationDef.Any
15
- mutationEventDecoded: MutationEvent.Any
14
+ mutationEventDecoded: MutationEvent.Any | MutationEvent.PartialAny
16
15
  }): ReadonlyArray<{
17
16
  statementSql: string
18
17
  bindValues: PreparedBindValues
@@ -52,23 +51,6 @@ export const getExecArgsFromMutation = ({
52
51
  })
53
52
  }
54
53
 
55
- export const makeShouldExcludeMutationFromLog = memoizeByRef((schema: LiveStoreSchema) => {
56
- const migrationOptions = schema.migrationOptions
57
- const mutationLogExclude =
58
- migrationOptions.strategy === 'from-mutation-log'
59
- ? (migrationOptions.excludeMutations ?? new Set(['livestore.RawSql']))
60
- : new Set(['livestore.RawSql'])
61
-
62
- return (mutationName: string, mutationEventDecoded: MutationEvent.Any): boolean => {
63
- if (mutationLogExclude.has(mutationName)) return true
64
-
65
- const mutationDef = schema.mutations.get(mutationName) ?? shouldNeverHappen(`Unknown mutation: ${mutationName}`)
66
- const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
67
-
68
- return execArgsArr.some((_) => _.statementSql.includes('__livestore'))
69
- }
70
- })
71
-
72
54
  // NOTE we should explore whether there is a more elegant solution
73
55
  // e.g. by leveraging the schema to replace the sessionIdSymbol
74
56
  export const replaceSessionIdSymbol = (
package/src/otel.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { makeNoopTracer } from '@livestore/utils'
2
+ import { Effect, identity, Layer, OtelTracer } from '@livestore/utils/effect'
3
+ import * as otel from '@opentelemetry/api'
4
+
5
+ export const provideOtel =
6
+ ({ otelTracer, parentSpanContext }: { otelTracer?: otel.Tracer; parentSpanContext?: otel.Context }) =>
7
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, Exclude<R, OtelTracer.OtelTracer>> => {
8
+ const OtelTracerLive = Layer.succeed(OtelTracer.OtelTracer, otelTracer ?? makeNoopTracer())
9
+
10
+ const TracingLive = Layer.unwrapEffect(Effect.map(OtelTracer.make, Layer.setTracer)).pipe(
11
+ Layer.provideMerge(OtelTracerLive),
12
+ ) as any as Layer.Layer<OtelTracer.OtelTracer>
13
+
14
+ return effect.pipe(
15
+ parentSpanContext
16
+ ? Effect.withParentSpan(OtelTracer.makeExternalSpan(otel.trace.getSpanContext(parentSpanContext)!))
17
+ : identity,
18
+ Effect.provide(TracingLive),
19
+ )
20
+ }
@@ -3,7 +3,7 @@ import { type Option, Predicate, type Schema } from '@livestore/utils/effect'
3
3
 
4
4
  import type { SessionIdSymbol } from '../adapter-types.js'
5
5
  import type { QueryInfo } from '../query-info.js'
6
- import type { DbSchema } from '../schema/index.js'
6
+ import type { DbSchema } from '../schema/mod.js'
7
7
  import type { SqliteDsl } from '../schema/table-def.js'
8
8
  import type { SqlValue } from '../util.js'
9
9
 
@@ -243,8 +243,9 @@ export namespace QueryBuilder {
243
243
  >
244
244
 
245
245
  /**
246
- *
246
+ * Gets a single row from the table and will create it if it doesn't exist yet.
247
247
  */
248
+ // TODO maybe call `getsert`?
248
249
  readonly row: TTableDef['options']['isSingleton'] extends true
249
250
  ? () => QueryBuilder<RowQuery.Result<TTableDef>, TTableDef, QueryBuilder.ApiFeature, QueryInfo.Row>
250
251
  : TTableDef['options']['deriveMutations']['enabled'] extends false
@@ -1,7 +1,7 @@
1
1
  import { Schema } from '@livestore/utils/effect'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
- import { DbSchema } from '../schema/index.js'
4
+ import { DbSchema } from '../schema/mod.js'
5
5
  import { getResultSchema } from './impl.js'
6
6
 
7
7
  const todos = DbSchema.table(
@@ -94,6 +94,33 @@ describe('query builder', () => {
94
94
  "query": "SELECT id, text FROM 'todos' WHERE deletedAt <= ?",
95
95
  }
96
96
  `)
97
+ expect(
98
+ db.todos
99
+ .select('id', 'text')
100
+ .where({ status: { op: 'IN', value: ['active'] } })
101
+ .asSql(),
102
+ ).toMatchInlineSnapshot(`
103
+ {
104
+ "bindValues": [
105
+ "active",
106
+ ],
107
+ "query": "SELECT id, text FROM 'todos' WHERE status IN (?)",
108
+ }
109
+ `)
110
+ expect(
111
+ db.todos
112
+ .select('id', 'text')
113
+ .where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })
114
+ .asSql(),
115
+ ).toMatchInlineSnapshot(`
116
+ {
117
+ "bindValues": [
118
+ "active",
119
+ "completed",
120
+ ],
121
+ "query": "SELECT id, text FROM 'todos' WHERE status NOT IN (?, ?)",
122
+ }
123
+ `)
97
124
  })
98
125
 
99
126
  it('should handle OFFSET and LIMIT clauses', () => {
@@ -1,7 +1,7 @@
1
1
  import { Option, Predicate, Schema } from '@livestore/utils/effect'
2
2
 
3
3
  import type { QueryInfo } from '../query-info.js'
4
- import type { DbSchema } from '../schema/index.js'
4
+ import type { DbSchema } from '../schema/mod.js'
5
5
  import type { QueryBuilder, QueryBuilderAst } from './api.js'
6
6
  import { QueryBuilderAstSymbol, TypeId } from './api.js'
7
7
 
@@ -155,7 +155,14 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
155
155
  [TypeId]: TypeId,
156
156
  [QueryBuilderAstSymbol]: ast,
157
157
  asSql: () => astToSql(ast),
158
- toString: () => astToSql(ast).query,
158
+ toString: () => {
159
+ try {
160
+ return astToSql(ast).query
161
+ } catch (cause) {
162
+ console.debug(`QueryBuilder.toString(): Error converting query builder to string`, cause, ast)
163
+ return `Error converting query builder to string`
164
+ }
165
+ },
159
166
  ...api,
160
167
  } satisfies QueryBuilder<TResult, TTableDef>
161
168
  }
@@ -198,9 +205,18 @@ const astToSql = (ast: QueryBuilderAst) => {
198
205
  if (colDef === undefined) {
199
206
  throw new Error(`Column ${col} not found`)
200
207
  }
201
- const encodedValue = Schema.encodeSync(colDef.schema)(value)
202
- bindValues.push(encodedValue)
203
- return `${col} ${op} ?`
208
+ const isArray = op === 'IN' || op === 'NOT IN'
209
+ const colSchema = isArray ? Schema.Array(colDef.schema) : colDef.schema
210
+ const encodedValue = Schema.encodeSync(colSchema)(value)
211
+
212
+ if (isArray) {
213
+ bindValues.push(...encodedValue)
214
+ const placeholders = Array.from({ length: encodedValue.length }, () => '?').join(', ')
215
+ return `${col} ${op} (${placeholders})`
216
+ } else {
217
+ bindValues.push(encodedValue)
218
+ return `${col} ${op} ?`
219
+ }
204
220
  }
205
221
  })
206
222
  .join(' AND ')}`
package/src/query-info.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { SessionIdSymbol } from './adapter-types.js'
2
- import type { DbSchema } from './schema/index.js'
2
+ import type { DbSchema } from './schema/mod.js'
3
3
 
4
4
  /**
5
5
  * Semantic information about a query with supported cases being:
@@ -1,10 +1,10 @@
1
- import { memoizeByRef, shouldNeverHappen } from '@livestore/utils'
1
+ import { isDevEnv, 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
5
  import { getExecArgsFromMutation } from './mutation.js'
6
- import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/index.js'
7
- import { MUTATION_LOG_META_TABLE } from './schema/index.js'
6
+ import type { LiveStoreSchema, MutationDef, MutationEvent, MutationLogMetaRow } from './schema/mod.js'
7
+ import { EventId, MUTATION_LOG_META_TABLE } from './schema/mod.js'
8
8
  import type { PreparedBindValues } from './util.js'
9
9
  import { sql } from './util.js'
10
10
 
@@ -72,11 +72,7 @@ This likely means the schema has changed in an incompatible way.
72
72
 
73
73
  for (const { statementSql, bindValues } of execArgsArr) {
74
74
  // TODO cache prepared statements for mutations
75
- db.execute(
76
- statementSql,
77
- bindValues,
78
- import.meta.env.DEV ? makeExecuteOptions(statementSql, bindValues) : undefined,
79
- )
75
+ db.execute(statementSql, bindValues, isDevEnv() ? makeExecuteOptions(statementSql, bindValues) : undefined)
80
76
  // console.log(`Re-executed mutation ${mutationSql}`, bindValues)
81
77
  }
82
78
  }).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
@@ -85,7 +81,7 @@ This likely means the schema has changed in an incompatible way.
85
81
 
86
82
  const stmt = logDb.prepare(sql`\
87
83
  SELECT * FROM ${MUTATION_LOG_META_TABLE}
88
- WHERE idGlobal > COALESCE($idGlobal, '') AND idLocal > COALESCE($idLocal, '')
84
+ WHERE idGlobal > $idGlobal OR (idGlobal = $idGlobal AND idLocal > $idLocal)
89
85
  ORDER BY idGlobal ASC, idLocal ASC
90
86
  LIMIT ${CHUNK_SIZE}
91
87
  `)
@@ -101,9 +97,9 @@ LIMIT ${CHUNK_SIZE}
101
97
  const lastId = Chunk.isChunk(item)
102
98
  ? Chunk.last(item).pipe(
103
99
  Option.map((_) => ({ global: _.idGlobal, local: _.idLocal })),
104
- Option.getOrUndefined,
100
+ Option.getOrElse(() => EventId.ROOT),
105
101
  )
106
- : undefined
102
+ : EventId.ROOT
107
103
  const nextItem = Chunk.fromIterable(
108
104
  stmt.select<MutationLogMetaRow>({
109
105
  $idGlobal: lastId?.global,
@@ -0,0 +1,46 @@
1
+ import { Schema } from '@livestore/utils/effect'
2
+
3
+ /**
4
+ * LiveStore event id value consisting of a globally unique event sequence number
5
+ * and a local sequence number.
6
+ *
7
+ * The local sequence number is only used for localOnly mutations and starts from 0 for each global sequence number.
8
+ */
9
+ export type EventId = { global: number; local: number }
10
+
11
+ export const EventId = Schema.Struct({
12
+ global: Schema.Number,
13
+ local: Schema.Number,
14
+ }).annotations({ title: 'LiveStore.EventId' })
15
+
16
+ /**
17
+ * Compare two event ids i.e. checks if the first event id is less than the second.
18
+ */
19
+ export const compare = (a: EventId, b: EventId) => {
20
+ if (a.global !== b.global) {
21
+ return a.global - b.global
22
+ }
23
+ return a.local - b.local
24
+ }
25
+
26
+ export const isEqual = (a: EventId, b: EventId) => a.global === b.global && a.local === b.local
27
+
28
+ export type EventIdPair = { id: EventId; parentId: EventId }
29
+
30
+ export const ROOT = { global: -1, local: 0 } satisfies EventId
31
+
32
+ export const isGreaterThan = (a: EventId, b: EventId) => {
33
+ return a.global > b.global || (a.global === b.global && a.local > b.local)
34
+ }
35
+
36
+ export const nextPair = (id: EventId, isLocal: boolean) => {
37
+ if (isLocal) {
38
+ return { id: { global: id.global, local: id.local + 1 }, parentId: id }
39
+ }
40
+
41
+ return {
42
+ id: { global: id.global + 1, local: 0 },
43
+ // NOTE we always point to `local: 0` for non-localOnly mutations
44
+ parentId: { global: id.global, local: 0 },
45
+ }
46
+ }