@livestore/common 0.4.0-dev.0 → 0.4.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 (255) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +7 -2
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
  5. package/dist/adapter-types.d.ts +9 -3
  6. package/dist/adapter-types.d.ts.map +1 -1
  7. package/dist/adapter-types.js.map +1 -1
  8. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  9. package/dist/devtools/devtools-messages-common.d.ts +7 -14
  10. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  11. package/dist/devtools/devtools-messages-common.js +1 -6
  12. package/dist/devtools/devtools-messages-common.js.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.d.ts +27 -25
  14. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  15. package/dist/errors.d.ts +47 -5
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +22 -3
  18. package/dist/errors.js.map +1 -1
  19. package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
  20. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  21. package/dist/leader-thread/LeaderSyncProcessor.js +122 -49
  22. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  23. package/dist/leader-thread/eventlog.d.ts +4 -10
  24. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  25. package/dist/leader-thread/eventlog.js +4 -6
  26. package/dist/leader-thread/eventlog.js.map +1 -1
  27. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.js +6 -2
  29. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  30. package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
  31. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.js +68 -19
  33. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
  35. package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
  36. package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
  37. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
  38. package/dist/leader-thread/materialize-event.d.ts +2 -2
  39. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  40. package/dist/leader-thread/materialize-event.js +23 -9
  41. package/dist/leader-thread/materialize-event.js.map +1 -1
  42. package/dist/leader-thread/recreate-db.d.ts +2 -3
  43. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  44. package/dist/leader-thread/recreate-db.js +1 -1
  45. package/dist/leader-thread/recreate-db.js.map +1 -1
  46. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  47. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  48. package/dist/leader-thread/shutdown-channel.js +2 -2
  49. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  50. package/dist/leader-thread/types.d.ts +7 -5
  51. package/dist/leader-thread/types.d.ts.map +1 -1
  52. package/dist/leader-thread/types.js.map +1 -1
  53. package/dist/materializer-helper.d.ts +1 -1
  54. package/dist/materializer-helper.d.ts.map +1 -1
  55. package/dist/materializer-helper.js +20 -4
  56. package/dist/materializer-helper.js.map +1 -1
  57. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  58. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  59. package/dist/rematerialize-from-eventlog.js +25 -16
  60. package/dist/rematerialize-from-eventlog.js.map +1 -1
  61. package/dist/schema/EventDef.d.ts +3 -0
  62. package/dist/schema/EventDef.d.ts.map +1 -1
  63. package/dist/schema/EventDef.js.map +1 -1
  64. package/dist/schema/LiveStoreEvent.d.ts +1 -1
  65. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  66. package/dist/schema/LiveStoreEvent.js +1 -2
  67. package/dist/schema/LiveStoreEvent.js.map +1 -1
  68. package/dist/schema/mod.d.ts +2 -0
  69. package/dist/schema/mod.d.ts.map +1 -1
  70. package/dist/schema/mod.js +1 -0
  71. package/dist/schema/mod.js.map +1 -1
  72. package/dist/schema/schema.d.ts +15 -0
  73. package/dist/schema/schema.d.ts.map +1 -1
  74. package/dist/schema/schema.js +26 -1
  75. package/dist/schema/schema.js.map +1 -1
  76. package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
  77. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  78. package/dist/schema/state/sqlite/client-document-def.js +95 -4
  79. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  80. package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
  81. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  82. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/column-annotations.js +14 -6
  84. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  85. package/dist/schema/state/sqlite/column-def.d.ts +19 -0
  86. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
  87. package/dist/schema/state/sqlite/column-def.js +179 -0
  88. package/dist/schema/state/sqlite/column-def.js.map +1 -0
  89. package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
  90. package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
  91. package/dist/schema/state/sqlite/column-def.test.js +572 -0
  92. package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
  93. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  94. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  95. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  96. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  97. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  98. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  99. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  100. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  101. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  102. package/dist/schema/state/sqlite/mod.js +1 -1
  103. package/dist/schema/state/sqlite/mod.js.map +1 -1
  104. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  105. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  106. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  108. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  109. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  110. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  111. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  112. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  113. package/dist/schema/state/sqlite/system-tables.js +2 -0
  114. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  115. package/dist/schema/state/sqlite/table-def.d.ts +6 -8
  116. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  117. package/dist/schema/state/sqlite/table-def.js +4 -211
  118. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  119. package/dist/schema/state/sqlite/table-def.test.js +59 -453
  120. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  121. package/dist/schema/unknown-events.d.ts +47 -0
  122. package/dist/schema/unknown-events.d.ts.map +1 -0
  123. package/dist/schema/unknown-events.js +69 -0
  124. package/dist/schema/unknown-events.js.map +1 -0
  125. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  126. package/dist/sql-queries/sql-query-builder.js +2 -1
  127. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  128. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  129. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  130. package/dist/sync/ClientSessionSyncProcessor.js +35 -33
  131. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  132. package/dist/sync/errors.d.ts +61 -0
  133. package/dist/sync/errors.d.ts.map +1 -0
  134. package/dist/sync/errors.js +36 -0
  135. package/dist/sync/errors.js.map +1 -0
  136. package/dist/sync/index.d.ts +3 -0
  137. package/dist/sync/index.d.ts.map +1 -1
  138. package/dist/sync/index.js +3 -0
  139. package/dist/sync/index.js.map +1 -1
  140. package/dist/sync/mock-sync-backend.d.ts +23 -0
  141. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  142. package/dist/sync/mock-sync-backend.js +114 -0
  143. package/dist/sync/mock-sync-backend.js.map +1 -0
  144. package/dist/sync/next/compact-events.d.ts.map +1 -1
  145. package/dist/sync/next/compact-events.js +4 -5
  146. package/dist/sync/next/compact-events.js.map +1 -1
  147. package/dist/sync/next/facts.d.ts.map +1 -1
  148. package/dist/sync/next/facts.js +1 -2
  149. package/dist/sync/next/facts.js.map +1 -1
  150. package/dist/sync/next/history-dag-common.d.ts +50 -11
  151. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  152. package/dist/sync/next/history-dag-common.js +193 -4
  153. package/dist/sync/next/history-dag-common.js.map +1 -1
  154. package/dist/sync/next/history-dag.d.ts.map +1 -1
  155. package/dist/sync/next/history-dag.js +3 -1
  156. package/dist/sync/next/history-dag.js.map +1 -1
  157. package/dist/sync/sync-backend-kv.d.ts +7 -0
  158. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  159. package/dist/sync/sync-backend-kv.js +18 -0
  160. package/dist/sync/sync-backend-kv.js.map +1 -0
  161. package/dist/sync/sync-backend.d.ts +105 -0
  162. package/dist/sync/sync-backend.d.ts.map +1 -0
  163. package/dist/sync/sync-backend.js +61 -0
  164. package/dist/sync/sync-backend.js.map +1 -0
  165. package/dist/sync/sync.d.ts +6 -84
  166. package/dist/sync/sync.d.ts.map +1 -1
  167. package/dist/sync/sync.js +2 -27
  168. package/dist/sync/sync.js.map +1 -1
  169. package/dist/sync/transport-chunking.d.ts +36 -0
  170. package/dist/sync/transport-chunking.d.ts.map +1 -0
  171. package/dist/sync/transport-chunking.js +56 -0
  172. package/dist/sync/transport-chunking.js.map +1 -0
  173. package/dist/sync/validate-push-payload.d.ts +1 -1
  174. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  175. package/dist/sync/validate-push-payload.js +6 -6
  176. package/dist/sync/validate-push-payload.js.map +1 -1
  177. package/dist/testing/event-factory.d.ts +68 -0
  178. package/dist/testing/event-factory.d.ts.map +1 -0
  179. package/dist/testing/event-factory.js +80 -0
  180. package/dist/testing/event-factory.js.map +1 -0
  181. package/dist/testing/mod.d.ts +2 -0
  182. package/dist/testing/mod.d.ts.map +1 -0
  183. package/dist/testing/mod.js +2 -0
  184. package/dist/testing/mod.js.map +1 -0
  185. package/dist/version.d.ts +2 -2
  186. package/dist/version.d.ts.map +1 -1
  187. package/dist/version.js +2 -2
  188. package/dist/version.js.map +1 -1
  189. package/package.json +7 -8
  190. package/src/ClientSessionLeaderThreadProxy.ts +7 -2
  191. package/src/adapter-types.ts +13 -3
  192. package/src/devtools/devtools-messages-common.ts +1 -8
  193. package/src/errors.ts +33 -4
  194. package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
  195. package/src/leader-thread/eventlog.ts +10 -6
  196. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  197. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  198. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  199. package/src/leader-thread/materialize-event.ts +34 -9
  200. package/src/leader-thread/recreate-db.ts +11 -3
  201. package/src/leader-thread/shutdown-channel.ts +16 -2
  202. package/src/leader-thread/types.ts +7 -5
  203. package/src/materializer-helper.ts +22 -5
  204. package/src/rematerialize-from-eventlog.ts +33 -23
  205. package/src/schema/EventDef.ts +3 -0
  206. package/src/schema/LiveStoreEvent.ts +1 -2
  207. package/src/schema/mod.ts +2 -0
  208. package/src/schema/schema.ts +37 -1
  209. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  210. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  211. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  212. package/src/schema/state/sqlite/column-def.test.ts +722 -0
  213. package/src/schema/state/sqlite/column-def.ts +215 -0
  214. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  215. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  216. package/src/schema/state/sqlite/mod.ts +1 -0
  217. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  218. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  219. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  220. package/src/schema/state/sqlite/system-tables.ts +2 -0
  221. package/src/schema/state/sqlite/table-def.test.ts +74 -569
  222. package/src/schema/state/sqlite/table-def.ts +13 -262
  223. package/src/schema/unknown-events.ts +131 -0
  224. package/src/sql-queries/sql-query-builder.ts +2 -1
  225. package/src/sync/ClientSessionSyncProcessor.ts +55 -49
  226. package/src/sync/errors.ts +38 -0
  227. package/src/sync/index.ts +3 -0
  228. package/src/sync/mock-sync-backend.ts +184 -0
  229. package/src/sync/next/compact-events.ts +4 -5
  230. package/src/sync/next/facts.ts +1 -3
  231. package/src/sync/next/history-dag-common.ts +272 -21
  232. package/src/sync/next/history-dag.ts +3 -1
  233. package/src/sync/sync-backend-kv.ts +22 -0
  234. package/src/sync/sync-backend.ts +185 -0
  235. package/src/sync/sync.ts +6 -89
  236. package/src/sync/transport-chunking.ts +90 -0
  237. package/src/sync/validate-push-payload.ts +6 -7
  238. package/src/testing/event-factory.ts +133 -0
  239. package/src/testing/mod.ts +1 -0
  240. package/src/version.ts +2 -2
  241. package/dist/schema-management/migrations.test.d.ts +0 -2
  242. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  243. package/dist/schema-management/migrations.test.js +0 -52
  244. package/dist/schema-management/migrations.test.js.map +0 -1
  245. package/dist/sync/next/graphology.d.ts +0 -8
  246. package/dist/sync/next/graphology.d.ts.map +0 -1
  247. package/dist/sync/next/graphology.js +0 -30
  248. package/dist/sync/next/graphology.js.map +0 -1
  249. package/dist/sync/next/graphology_.d.ts +0 -3
  250. package/dist/sync/next/graphology_.d.ts.map +0 -1
  251. package/dist/sync/next/graphology_.js +0 -3
  252. package/dist/sync/next/graphology_.js.map +0 -1
  253. package/src/sync/next/ambient.d.ts +0 -3
  254. package/src/sync/next/graphology.ts +0 -41
  255. package/src/sync/next/graphology_.ts +0 -2
@@ -0,0 +1,38 @@
1
+ import { Schema } from '@livestore/utils/effect'
2
+ import { UnexpectedError } from '../errors.ts'
3
+ import { EventSequenceNumber } from '../schema/mod.ts'
4
+
5
+ export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {
6
+ cause: Schema.Defect,
7
+ }) {}
8
+
9
+ /** Unique ID generated by the backend when its created. Used to check whether the backend identity has changed. */
10
+ export const BackendId = Schema.String.annotations({ title: '@livestore/sync-cf:BackendId' })
11
+
12
+ export class BackendIdMismatchError extends Schema.TaggedError<BackendIdMismatchError>()('BackendIdMismatchError', {
13
+ expected: BackendId,
14
+ received: BackendId,
15
+ }) {}
16
+
17
+ export class ServerAheadError extends Schema.TaggedError<ServerAheadError>()('ServerAheadError', {
18
+ minimumExpectedNum: EventSequenceNumber.GlobalEventSequenceNumber,
19
+ providedNum: EventSequenceNumber.GlobalEventSequenceNumber,
20
+ }) {}
21
+
22
+ export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('InvalidPushError', {
23
+ cause: Schema.Union(UnexpectedError, ServerAheadError, BackendIdMismatchError),
24
+ }) {}
25
+
26
+ export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {
27
+ cause: Schema.Defect,
28
+ }) {}
29
+
30
+ export class LeaderAheadError extends Schema.TaggedError<LeaderAheadError>()('LeaderAheadError', {
31
+ minimumExpectedNum: EventSequenceNumber.EventSequenceNumber,
32
+ providedNum: EventSequenceNumber.EventSequenceNumber,
33
+ /** Generation number the client session should use for subsequent pushes */
34
+ // nextGeneration: Schema.Number,
35
+ }) {}
36
+
37
+ export const SyncError = Schema.Union(InvalidPushError, InvalidPullError)
38
+ export type SyncError = typeof SyncError.Type
package/src/sync/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from './ClientSessionSyncProcessor.ts'
2
+ export * from './mock-sync-backend.ts'
2
3
  export * from './sync.ts'
4
+ export { NetworkStatus } from './sync-backend.ts'
5
+ export * from './transport-chunking.ts'
3
6
  export * from './validate-push-payload.ts'
@@ -0,0 +1,184 @@
1
+ import type { Schema, Scope } from '@livestore/utils/effect'
2
+ import { Effect, Mailbox, Option, Queue, Stream, SubscriptionRef } from '@livestore/utils/effect'
3
+ import { UnexpectedError } from '../errors.ts'
4
+ import { EventSequenceNumber, type LiveStoreEvent } from '../schema/mod.ts'
5
+ import { InvalidPushError } from './errors.ts'
6
+ import * as SyncBackend from './sync-backend.ts'
7
+ import { validatePushPayload } from './validate-push-payload.ts'
8
+
9
+ export interface MockSyncBackend {
10
+ pushedEvents: Stream.Stream<LiveStoreEvent.AnyEncodedGlobal>
11
+ connect: Effect.Effect<void>
12
+ disconnect: Effect.Effect<void>
13
+ makeSyncBackend: Effect.Effect<SyncBackend.SyncBackend, UnexpectedError, Scope.Scope>
14
+ advance: (...batch: LiveStoreEvent.AnyEncodedGlobal[]) => Effect.Effect<void>
15
+ /** Fail the next N push calls with an InvalidPushError (or custom error) */
16
+ failNextPushes: (
17
+ count: number,
18
+ error?: (batch: ReadonlyArray<LiveStoreEvent.AnyEncodedGlobal>) => Effect.Effect<never, InvalidPushError>,
19
+ ) => Effect.Effect<void>
20
+ }
21
+
22
+ export interface MockSyncBackendOptions {
23
+ /** Chunk size for non-live pulls; defaults to 100 */
24
+ nonLiveChunkSize?: number
25
+ /** Initial connected state; defaults to false */
26
+ startConnected?: boolean
27
+ // TODO add a "flaky" mode to simulate transient network / server failures for pull/push
28
+ }
29
+
30
+ export const makeMockSyncBackend = (
31
+ options?: MockSyncBackendOptions,
32
+ ): Effect.Effect<MockSyncBackend, UnexpectedError, Scope.Scope> =>
33
+ Effect.gen(function* () {
34
+ const syncEventSequenceNumberRef = { current: EventSequenceNumber.ROOT.global }
35
+ const syncPullQueue = yield* Queue.unbounded<LiveStoreEvent.AnyEncodedGlobal>()
36
+ const pushedEventsQueue = yield* Mailbox.make<LiveStoreEvent.AnyEncodedGlobal>()
37
+ const syncIsConnectedRef = yield* SubscriptionRef.make(options?.startConnected ?? false)
38
+ const allEventsRef: { current: LiveStoreEvent.AnyEncodedGlobal[] } = { current: [] }
39
+
40
+ const span = yield* Effect.currentSpan.pipe(Effect.orDie)
41
+
42
+ const semaphore = yield* Effect.makeSemaphore(1)
43
+
44
+ // TODO improve the API and implementation of simulating errors
45
+ const failCounterRef = yield* SubscriptionRef.make(0)
46
+ const failEffectRef = yield* SubscriptionRef.make<
47
+ ((batch: ReadonlyArray<LiveStoreEvent.AnyEncodedGlobal>) => Effect.Effect<never, InvalidPushError>) | undefined
48
+ >(undefined)
49
+
50
+ const makeSyncBackend = Effect.gen(function* () {
51
+ const nonLiveChunkSize = Math.max(1, options?.nonLiveChunkSize ?? 100)
52
+
53
+ // TODO consider making offline state actively error pull/push.
54
+ // Currently, offline only reflects in `isConnected`, while operations still succeed,
55
+ // mirroring how some real providers behave during transient disconnects.
56
+ return SyncBackend.of<Schema.JsonValue>({
57
+ isConnected: syncIsConnectedRef,
58
+ connect: SubscriptionRef.set(syncIsConnectedRef, true),
59
+ ping: Effect.void,
60
+ pull: (cursor, options) =>
61
+ (options?.live
62
+ ? Stream.concat(
63
+ Stream.make(SyncBackend.pullResItemEmpty()),
64
+ Stream.fromQueue(syncPullQueue).pipe(
65
+ Stream.chunks,
66
+ Stream.map((chunk) => ({
67
+ batch: [...chunk].map((eventEncoded) => ({ eventEncoded, metadata: Option.none() })),
68
+ pageInfo: SyncBackend.pageInfoNoMore,
69
+ })),
70
+ ),
71
+ )
72
+ : Stream.fromEffect(
73
+ Effect.sync(() => {
74
+ const lastSeen = cursor.pipe(
75
+ Option.match({
76
+ onNone: () => EventSequenceNumber.ROOT.global,
77
+ onSome: (_) => _.eventSequenceNumber,
78
+ }),
79
+ )
80
+ // All events with seqNum greater than lastSeen
81
+ const slice = allEventsRef.current.filter((e) => e.seqNum > lastSeen)
82
+ // Split into configured chunk size
83
+ const chunks: { events: LiveStoreEvent.AnyEncodedGlobal[]; remaining: number }[] = []
84
+ for (let i = 0; i < slice.length; i += nonLiveChunkSize) {
85
+ const end = Math.min(i + nonLiveChunkSize, slice.length)
86
+ const remaining = Math.max(slice.length - end, 0)
87
+ chunks.push({ events: slice.slice(i, end), remaining })
88
+ }
89
+ if (chunks.length === 0) {
90
+ chunks.push({ events: [], remaining: 0 })
91
+ }
92
+ return chunks
93
+ }),
94
+ ).pipe(
95
+ Stream.flatMap((chunks) =>
96
+ Stream.fromIterable(chunks).pipe(
97
+ Stream.map(({ events, remaining }) => ({
98
+ batch: events.map((eventEncoded) => ({ eventEncoded, metadata: Option.none() })),
99
+ pageInfo: remaining > 0 ? SyncBackend.pageInfoMoreKnown(remaining) : SyncBackend.pageInfoNoMore,
100
+ })),
101
+ ),
102
+ ),
103
+ )
104
+ ).pipe(Stream.withSpan('MockSyncBackend:pull', { parent: span })),
105
+ push: (batch) =>
106
+ Effect.gen(function* () {
107
+ yield* validatePushPayload(batch, syncEventSequenceNumberRef.current)
108
+
109
+ const remaining = yield* SubscriptionRef.get(failCounterRef)
110
+ if (remaining > 0) {
111
+ const maybeFail = yield* SubscriptionRef.get(failEffectRef)
112
+ // decrement counter first
113
+ yield* SubscriptionRef.set(failCounterRef, remaining - 1)
114
+ if (maybeFail) {
115
+ return yield* maybeFail(batch)
116
+ }
117
+ return yield* new InvalidPushError({
118
+ cause: new UnexpectedError({ cause: new Error('MockSyncBackend: simulated push failure') }),
119
+ })
120
+ }
121
+
122
+ yield* Effect.sleep(10).pipe(Effect.withSpan('MockSyncBackend:push:sleep')) // Simulate network latency
123
+
124
+ yield* pushedEventsQueue.offerAll(batch)
125
+ yield* syncPullQueue.offerAll(batch)
126
+ allEventsRef.current = allEventsRef.current.concat(batch)
127
+
128
+ syncEventSequenceNumberRef.current = batch.at(-1)!.seqNum
129
+ }).pipe(
130
+ Effect.withSpan('MockSyncBackend:push', {
131
+ parent: span,
132
+ attributes: {
133
+ nums: batch.map((_) => _.seqNum),
134
+ },
135
+ }),
136
+ semaphore.withPermits(1),
137
+ ),
138
+ metadata: {
139
+ name: '@livestore/mock-sync',
140
+ description: 'Just a mock sync backend',
141
+ },
142
+ supports: {
143
+ pullPageInfoKnown: true,
144
+ pullLive: true,
145
+ },
146
+ })
147
+ })
148
+
149
+ const advance = (...batch: LiveStoreEvent.AnyEncodedGlobal[]) =>
150
+ Effect.gen(function* () {
151
+ syncEventSequenceNumberRef.current = batch.at(-1)!.seqNum
152
+ allEventsRef.current = allEventsRef.current.concat(batch)
153
+ yield* syncPullQueue.offerAll(batch)
154
+ }).pipe(
155
+ Effect.withSpan('MockSyncBackend:advance', {
156
+ parent: span,
157
+ attributes: { nums: batch.map((_) => _.seqNum) },
158
+ }),
159
+ semaphore.withPermits(1),
160
+ )
161
+
162
+ const connect = SubscriptionRef.set(syncIsConnectedRef, true)
163
+ const disconnect = SubscriptionRef.set(syncIsConnectedRef, false)
164
+
165
+ const failNextPushes = (
166
+ count: number,
167
+ error?: (batch: ReadonlyArray<LiveStoreEvent.AnyEncodedGlobal>) => Effect.Effect<never, InvalidPushError>,
168
+ ) =>
169
+ Effect.gen(function* () {
170
+ yield* SubscriptionRef.set(failCounterRef, count)
171
+ yield* SubscriptionRef.set(failEffectRef, error)
172
+ })
173
+
174
+ return {
175
+ syncEventSequenceNumberRef,
176
+ syncPullQueue,
177
+ pushedEvents: Mailbox.toStream(pushedEventsQueue),
178
+ connect,
179
+ disconnect,
180
+ makeSyncBackend,
181
+ advance,
182
+ failNextPushes,
183
+ }
184
+ }).pipe(Effect.withSpanScoped('MockSyncBackend'))
@@ -1,6 +1,5 @@
1
1
  import { EventSequenceNumber } from '../../schema/mod.ts'
2
2
  import { replacesFacts } from './facts.ts'
3
- import { graphologyDag } from './graphology_.ts'
4
3
  import type { HistoryDag } from './history-dag-common.ts'
5
4
  import { emptyHistoryDag } from './history-dag-common.ts'
6
5
 
@@ -17,7 +16,7 @@ export const compactEvents = (inputDag: HistoryDag): { dag: HistoryDag; compacte
17
16
  const dag = inputDag.copy()
18
17
  const compactedEventCount = 0
19
18
 
20
- const orderedEventSequenceNumberStrs = graphologyDag.topologicalSort(dag).reverse()
19
+ const orderedEventSequenceNumberStrs = dag.topologicalNodeIds().reverse()
21
20
 
22
21
  // drop root
23
22
  orderedEventSequenceNumberStrs.pop()
@@ -116,7 +115,7 @@ const findSubDagsInHistory = (
116
115
  const subDags: HistoryDag[] = []
117
116
  const allOutsideDependencies: string[][] = []
118
117
 
119
- for (const eventNumStr of graphologyDag.topologicalSort(inputDag)) {
118
+ for (const eventNumStr of inputDag.topologicalNodeIds()) {
120
119
  if (eventNumStr === upToExclEventSequenceNumberStr) {
121
120
  break
122
121
  }
@@ -189,8 +188,8 @@ const dagReplacesDag = (dagA: HistoryDag, dagB: HistoryDag): boolean => {
189
188
  }
190
189
 
191
190
  // TODO write tests that covers deterministic order when DAGs have branches
192
- const nodeEntriesA = graphologyDag.topologicalSort(dagA).map((nodeId) => dagA.getNodeAttributes(nodeId))
193
- const nodeEntriesB = graphologyDag.topologicalSort(dagB).map((nodeId) => dagB.getNodeAttributes(nodeId))
191
+ const nodeEntriesA = dagA.topologicalNodeIds().map((nodeId) => dagA.getNodeAttributes(nodeId))
192
+ const nodeEntriesB = dagB.topologicalNodeIds().map((nodeId) => dagB.getNodeAttributes(nodeId))
194
193
 
195
194
  for (let i = 0; i < nodeEntriesA.length; i++) {
196
195
  const nodeA = nodeEntriesA[i]!
@@ -1,5 +1,4 @@
1
1
  import { notYetImplemented } from '@livestore/utils'
2
-
3
2
  import type {
4
3
  EventDefFactInput,
5
4
  EventDefFacts,
@@ -8,7 +7,6 @@ import type {
8
7
  FactsCallback,
9
8
  } from '../../schema/EventDef.ts'
10
9
  import type * as EventSequenceNumber from '../../schema/EventSequenceNumber.ts'
11
- import { graphologyDag } from './graphology_.ts'
12
10
  import { EMPTY_FACT_VALUE, type HistoryDag, type HistoryDagNode } from './history-dag-common.ts'
13
11
 
14
12
  export const factsSnapshotForEvents = (
@@ -34,7 +32,7 @@ export const factsSnapshotForDag = (
34
32
  ): EventDefFactsSnapshot => {
35
33
  const facts = new Map<string, any>()
36
34
 
37
- const orderedEventSequenceNumberStrs = graphologyDag.topologicalSort(dag)
35
+ const orderedEventSequenceNumberStrs = dag.topologicalNodeIds()
38
36
 
39
37
  for (let i = 0; i < orderedEventSequenceNumberStrs.length; i++) {
40
38
  const event = dag.getNodeAttributes(orderedEventSequenceNumberStrs[i]!)
@@ -1,30 +1,11 @@
1
+ import { shouldNeverHappen } from '@livestore/utils'
2
+ import { Graph } from '@livestore/utils/effect'
1
3
  import type { EventDefFactsGroup } from '../../schema/EventDef.ts'
2
4
  import * as EventSequenceNumber from '../../schema/EventSequenceNumber.ts'
3
- import { graphology } from './graphology_.ts'
4
5
 
5
6
  export const connectionTypeOptions = ['parent', 'facts'] as const
6
7
  export type ConnectionType = (typeof connectionTypeOptions)[number]
7
8
 
8
- /**
9
- * Eventlog represented as a multi-DAG including edges for
10
- * - total-order (parent) relationships
11
- * - dependency (requires/reads facts) relationships
12
- */
13
- export type HistoryDag = graphology.IGraph<HistoryDagNode, { type: ConnectionType }>
14
-
15
- export const emptyHistoryDag = (): HistoryDag =>
16
- new graphology.Graph({
17
- allowSelfLoops: false,
18
- multi: true,
19
- type: 'directed',
20
- })
21
-
22
- // TODO consider making `ROOT_ID` parent to itself
23
- export const rootParentNum = EventSequenceNumber.make({
24
- global: EventSequenceNumber.ROOT.global - 1,
25
- client: EventSequenceNumber.clientDefault,
26
- })
27
-
28
9
  export type HistoryDagNode = {
29
10
  seqNum: EventSequenceNumber.EventSequenceNumber
30
11
  parentSeqNum: EventSequenceNumber.EventSequenceNumber
@@ -37,6 +18,276 @@ export type HistoryDagNode = {
37
18
  sessionId: string | undefined
38
19
  }
39
20
 
21
+ type HistoryDagEdgeAttributes = { type: ConnectionType }
22
+
23
+ type HistoryDagEdgeEntry = {
24
+ edge: Graph.EdgeIndex
25
+ source: string
26
+ target: string
27
+ attributes: HistoryDagEdgeAttributes
28
+ }
29
+
30
+ type HistoryDagOptions = {
31
+ allowSelfLoops: boolean
32
+ }
33
+
34
+ const defaultOptions: HistoryDagOptions = {
35
+ allowSelfLoops: false,
36
+ }
37
+
38
+ const cloneFactsGroup = (factsGroup: EventDefFactsGroup): EventDefFactsGroup => ({
39
+ depRead: new Map(factsGroup.depRead),
40
+ depRequire: new Map(factsGroup.depRequire),
41
+ modifySet: new Map(factsGroup.modifySet),
42
+ modifyUnset: new Map(factsGroup.modifyUnset),
43
+ })
44
+
45
+ const cloneHistoryDagNode = (node: HistoryDagNode): HistoryDagNode => ({
46
+ ...node,
47
+ // Copy the event sequence numbers to avoid accidental aliasing
48
+ parentSeqNum: { ...node.parentSeqNum },
49
+ seqNum: { ...node.seqNum },
50
+ // Facts are represented via maps which should not be shared across DAG copies
51
+ factsGroup: cloneFactsGroup(node.factsGroup),
52
+ })
53
+
54
+ /**
55
+ * Mutable DAG wrapper that retains the previous string-based node ids API
56
+ * while delegating storage and algorithms to Effect's graph module.
57
+ */
58
+ export class HistoryDag {
59
+ private readonly options: HistoryDagOptions
60
+ private readonly idToIndex: Map<string, Graph.NodeIndex>
61
+ private readonly indexToId: Map<Graph.NodeIndex, string>
62
+ private readonly graph: Graph.MutableDirectedGraph<HistoryDagNode, HistoryDagEdgeAttributes>
63
+
64
+ private constructor({
65
+ graph,
66
+ idToIndex,
67
+ indexToId,
68
+ options,
69
+ }: {
70
+ graph: Graph.MutableDirectedGraph<HistoryDagNode, HistoryDagEdgeAttributes>
71
+ idToIndex?: Map<string, Graph.NodeIndex>
72
+ indexToId?: Map<Graph.NodeIndex, string>
73
+ options?: Partial<HistoryDagOptions>
74
+ }) {
75
+ this.graph = graph
76
+ this.options = { ...defaultOptions, ...options }
77
+ this.idToIndex = idToIndex ? new Map(idToIndex) : new Map()
78
+ this.indexToId = indexToId ? new Map(indexToId) : new Map()
79
+ }
80
+
81
+ static create(options?: Partial<HistoryDagOptions>): HistoryDag {
82
+ const graph = Graph.beginMutation(Graph.directed<HistoryDagNode, HistoryDagEdgeAttributes>())
83
+ return options ? new HistoryDag({ graph, options }) : new HistoryDag({ graph })
84
+ }
85
+
86
+ copy(): HistoryDag {
87
+ const clone = HistoryDag.create(this.options)
88
+
89
+ for (const [id, index] of this.idToIndex) {
90
+ const node = this.graph.nodes.get(index) ?? shouldNeverHappen(`HistoryDag.copy missing node for ${id}`)
91
+ clone.addNode(id, cloneHistoryDagNode(node))
92
+ }
93
+
94
+ for (const edge of this.graph.edges.values()) {
95
+ const sourceId = this.indexToId.get(edge.source) ?? shouldNeverHappen('HistoryDag.copy missing source id')
96
+ const targetId = this.indexToId.get(edge.target) ?? shouldNeverHappen('HistoryDag.copy missing target id')
97
+ clone.addEdge(sourceId, targetId, { ...edge.data })
98
+ }
99
+
100
+ return clone
101
+ }
102
+
103
+ topologicalNodeIds(): Array<string> {
104
+ const walker = Graph.topo(this.graph)
105
+ const indices = Array.from(Graph.indices(walker))
106
+ return indices.map((index) => this.indexToId.get(index) ?? shouldNeverHappen(`Missing node id for index ${index}`))
107
+ }
108
+
109
+ addNode(id: string, attributes: HistoryDagNode): void {
110
+ if (this.idToIndex.has(id)) {
111
+ shouldNeverHappen(`HistoryDag node ${id} already exists`)
112
+ }
113
+
114
+ const nodeIndex = Graph.addNode(this.graph, attributes)
115
+ this.idToIndex.set(id, nodeIndex)
116
+ this.indexToId.set(nodeIndex, id)
117
+ }
118
+
119
+ hasNode(id: string): boolean {
120
+ return this.idToIndex.has(id)
121
+ }
122
+
123
+ getNodeAttributes(id: string): HistoryDagNode {
124
+ const index = this.idToIndex.get(id)
125
+ if (index === undefined) {
126
+ return shouldNeverHappen(`HistoryDag node ${id} not found`)
127
+ }
128
+
129
+ const node = this.graph.nodes.get(index)
130
+ return node ?? shouldNeverHappen(`HistoryDag node data missing for ${id}`)
131
+ }
132
+
133
+ nodes(): IterableIterator<string> {
134
+ return this.idToIndex.keys()
135
+ }
136
+
137
+ nodeEntries(): IterableIterator<{ key: string; attributes: HistoryDagNode }> {
138
+ return function* (this: HistoryDag) {
139
+ for (const [id, index] of this.idToIndex) {
140
+ const attributes = this.graph.nodes.get(index) ?? shouldNeverHappen(`HistoryDag node data missing for ${id}`)
141
+ yield { key: id, attributes }
142
+ }
143
+ }.call(this)
144
+ }
145
+
146
+ addEdge(sourceId: string, targetId: string, attributes: HistoryDagEdgeAttributes): Graph.EdgeIndex {
147
+ if (this.options.allowSelfLoops === false && sourceId === targetId) {
148
+ return shouldNeverHappen('HistoryDag self-loops are disabled')
149
+ }
150
+
151
+ const sourceIndex = this.idToIndex.get(sourceId)
152
+ const targetIndex = this.idToIndex.get(targetId)
153
+
154
+ if (sourceIndex === undefined || targetIndex === undefined) {
155
+ return shouldNeverHappen(`HistoryDag edge references unknown nodes: ${sourceId} -> ${targetId}`)
156
+ }
157
+
158
+ return Graph.addEdge(this.graph, sourceIndex, targetIndex, attributes)
159
+ }
160
+
161
+ edges(sourceId: string, targetId: string): Array<Graph.EdgeIndex> {
162
+ const sourceIndex = this.idToIndex.get(sourceId)
163
+ const targetIndex = this.idToIndex.get(targetId)
164
+
165
+ if (sourceIndex === undefined || targetIndex === undefined) {
166
+ return []
167
+ }
168
+
169
+ const adjacency = this.graph.adjacency.get(sourceIndex)
170
+ if (adjacency === undefined) {
171
+ return []
172
+ }
173
+
174
+ return adjacency.filter((edgeIndex) => {
175
+ const edge = this.graph.edges.get(edgeIndex)
176
+ return edge !== undefined && edge.target === targetIndex
177
+ })
178
+ }
179
+
180
+ inEdges(id: string): Array<Graph.EdgeIndex> {
181
+ const index = this.idToIndex.get(id)
182
+ if (index === undefined) {
183
+ return []
184
+ }
185
+ const incoming = this.graph.reverseAdjacency.get(index)
186
+ return incoming ? [...incoming] : []
187
+ }
188
+
189
+ outboundEdgeEntries(id: string): Array<HistoryDagEdgeEntry> {
190
+ const index = this.idToIndex.get(id)
191
+ if (index === undefined) {
192
+ return []
193
+ }
194
+
195
+ const adjacency = this.graph.adjacency.get(index)
196
+ if (adjacency === undefined) {
197
+ return []
198
+ }
199
+
200
+ return adjacency
201
+ .map((edgeIndex) => this.edgeEntry(edgeIndex))
202
+ .filter((entry): entry is HistoryDagEdgeEntry => entry !== undefined)
203
+ }
204
+
205
+ inboundEdgeEntries(id: string): Array<HistoryDagEdgeEntry> {
206
+ const index = this.idToIndex.get(id)
207
+ if (index === undefined) {
208
+ return []
209
+ }
210
+
211
+ const adjacency = this.graph.reverseAdjacency.get(index)
212
+ if (adjacency === undefined) {
213
+ return []
214
+ }
215
+
216
+ return adjacency
217
+ .map((edgeIndex) => this.edgeEntry(edgeIndex))
218
+ .filter((entry): entry is HistoryDagEdgeEntry => entry !== undefined)
219
+ }
220
+
221
+ getEdgeAttributes(edgeIndex: Graph.EdgeIndex): HistoryDagEdgeAttributes {
222
+ const edge = this.graph.edges.get(edgeIndex)
223
+ return edge?.data ?? shouldNeverHappen(`HistoryDag edge ${edgeIndex} not found`)
224
+ }
225
+
226
+ getEdgeAttribute<TKey extends keyof HistoryDagEdgeAttributes>(
227
+ edgeIndex: Graph.EdgeIndex,
228
+ key: TKey,
229
+ ): HistoryDagEdgeAttributes[TKey] {
230
+ const attributes = this.getEdgeAttributes(edgeIndex)
231
+ return attributes[key]
232
+ }
233
+
234
+ source(edgeIndex: Graph.EdgeIndex): string {
235
+ const edge = this.graph.edges.get(edgeIndex)
236
+ const sourceId = edge !== undefined ? this.indexToId.get(edge.source) : undefined
237
+ return sourceId ?? shouldNeverHappen(`HistoryDag edge ${edgeIndex} missing source`)
238
+ }
239
+
240
+ target(edgeIndex: Graph.EdgeIndex): string {
241
+ const edge = this.graph.edges.get(edgeIndex)
242
+ const targetId = edge !== undefined ? this.indexToId.get(edge.target) : undefined
243
+ return targetId ?? shouldNeverHappen(`HistoryDag edge ${edgeIndex} missing target`)
244
+ }
245
+
246
+ dropNode(id: string): void {
247
+ const index = this.idToIndex.get(id)
248
+ if (index === undefined) {
249
+ return
250
+ }
251
+
252
+ Graph.removeNode(this.graph, index)
253
+ this.idToIndex.delete(id)
254
+ this.indexToId.delete(index)
255
+ }
256
+
257
+ get size(): number {
258
+ return this.idToIndex.size
259
+ }
260
+
261
+ private edgeEntry(edgeIndex: Graph.EdgeIndex): HistoryDagEdgeEntry | undefined {
262
+ const edge = this.graph.edges.get(edgeIndex)
263
+ if (edge === undefined) {
264
+ return undefined
265
+ }
266
+
267
+ const source = this.indexToId.get(edge.source)
268
+ const target = this.indexToId.get(edge.target)
269
+
270
+ if (source === undefined || target === undefined) {
271
+ return undefined
272
+ }
273
+
274
+ return {
275
+ edge: edgeIndex,
276
+ source,
277
+ target,
278
+ attributes: edge.data,
279
+ }
280
+ }
281
+ }
282
+
283
+ export const emptyHistoryDag = (): HistoryDag => HistoryDag.create({ allowSelfLoops: false })
284
+
285
+ // TODO consider making `ROOT_ID` parent to itself
286
+ export const rootParentNum = EventSequenceNumber.make({
287
+ global: EventSequenceNumber.ROOT.global - 1,
288
+ client: EventSequenceNumber.clientDefault,
289
+ })
290
+
40
291
  export const rootEventNode: HistoryDagNode = {
41
292
  seqNum: EventSequenceNumber.ROOT,
42
293
  parentSeqNum: rootParentNum,
@@ -18,7 +18,9 @@ export const historyDagFromNodes = (dagNodes: HistoryDagNode[], options?: { skip
18
18
 
19
19
  const dag = emptyHistoryDag()
20
20
 
21
- dagNodes.forEach((node) => dag.addNode(EventSequenceNumber.toString(node.seqNum), node))
21
+ dagNodes.forEach((node) => {
22
+ dag.addNode(EventSequenceNumber.toString(node.seqNum), node)
23
+ })
22
24
 
23
25
  dagNodes.forEach((node) => {
24
26
  if (EventSequenceNumber.toString(node.parentSeqNum) !== EventSequenceNumber.toString(rootParentNum)) {
@@ -0,0 +1,22 @@
1
+ import { Effect, KeyValueStore, Option } from '@livestore/utils/effect'
2
+ import { UnexpectedError } from '../errors.ts'
3
+
4
+ export const makeBackendIdHelper = Effect.gen(function* () {
5
+ const kv = yield* KeyValueStore.KeyValueStore
6
+
7
+ const backendIdKey = `backendId`
8
+ const backendIdRef = { current: yield* kv.get(backendIdKey).pipe(UnexpectedError.mapToUnexpectedError) }
9
+
10
+ const setBackendId = (backendId: string) =>
11
+ Effect.gen(function* () {
12
+ if (backendIdRef.current._tag === 'None' || backendIdRef.current.value !== backendId) {
13
+ backendIdRef.current = Option.some(backendId)
14
+ yield* kv.set(backendIdKey, backendId)
15
+ }
16
+ }).pipe(UnexpectedError.mapToUnexpectedError)
17
+
18
+ return {
19
+ lazySet: setBackendId,
20
+ get: () => backendIdRef.current,
21
+ }
22
+ })