@livestore/common 0.4.0-dev.1 → 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 (253) 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 +6 -2
  86. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/column-def.js +122 -185
  88. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  89. package/dist/schema/state/sqlite/column-def.test.js +116 -73
  90. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  91. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  92. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  93. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  94. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  95. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  96. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  97. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  98. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  99. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  100. package/dist/schema/state/sqlite/mod.js +1 -1
  101. package/dist/schema/state/sqlite/mod.js.map +1 -1
  102. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  103. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  104. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  105. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  106. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  108. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  109. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  110. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  111. package/dist/schema/state/sqlite/system-tables.js +2 -0
  112. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  113. package/dist/schema/state/sqlite/table-def.d.ts +4 -4
  114. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  115. package/dist/schema/state/sqlite/table-def.js +2 -2
  116. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  117. package/dist/schema/state/sqlite/table-def.test.js +51 -2
  118. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  119. package/dist/schema/unknown-events.d.ts +47 -0
  120. package/dist/schema/unknown-events.d.ts.map +1 -0
  121. package/dist/schema/unknown-events.js +69 -0
  122. package/dist/schema/unknown-events.js.map +1 -0
  123. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  124. package/dist/sql-queries/sql-query-builder.js +2 -1
  125. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  126. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  127. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  128. package/dist/sync/ClientSessionSyncProcessor.js +35 -33
  129. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  130. package/dist/sync/errors.d.ts +61 -0
  131. package/dist/sync/errors.d.ts.map +1 -0
  132. package/dist/sync/errors.js +36 -0
  133. package/dist/sync/errors.js.map +1 -0
  134. package/dist/sync/index.d.ts +3 -0
  135. package/dist/sync/index.d.ts.map +1 -1
  136. package/dist/sync/index.js +3 -0
  137. package/dist/sync/index.js.map +1 -1
  138. package/dist/sync/mock-sync-backend.d.ts +23 -0
  139. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  140. package/dist/sync/mock-sync-backend.js +114 -0
  141. package/dist/sync/mock-sync-backend.js.map +1 -0
  142. package/dist/sync/next/compact-events.d.ts.map +1 -1
  143. package/dist/sync/next/compact-events.js +4 -5
  144. package/dist/sync/next/compact-events.js.map +1 -1
  145. package/dist/sync/next/facts.d.ts.map +1 -1
  146. package/dist/sync/next/facts.js +1 -2
  147. package/dist/sync/next/facts.js.map +1 -1
  148. package/dist/sync/next/history-dag-common.d.ts +50 -11
  149. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  150. package/dist/sync/next/history-dag-common.js +193 -4
  151. package/dist/sync/next/history-dag-common.js.map +1 -1
  152. package/dist/sync/next/history-dag.d.ts.map +1 -1
  153. package/dist/sync/next/history-dag.js +3 -1
  154. package/dist/sync/next/history-dag.js.map +1 -1
  155. package/dist/sync/sync-backend-kv.d.ts +7 -0
  156. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  157. package/dist/sync/sync-backend-kv.js +18 -0
  158. package/dist/sync/sync-backend-kv.js.map +1 -0
  159. package/dist/sync/sync-backend.d.ts +105 -0
  160. package/dist/sync/sync-backend.d.ts.map +1 -0
  161. package/dist/sync/sync-backend.js +61 -0
  162. package/dist/sync/sync-backend.js.map +1 -0
  163. package/dist/sync/sync.d.ts +6 -84
  164. package/dist/sync/sync.d.ts.map +1 -1
  165. package/dist/sync/sync.js +2 -27
  166. package/dist/sync/sync.js.map +1 -1
  167. package/dist/sync/transport-chunking.d.ts +36 -0
  168. package/dist/sync/transport-chunking.d.ts.map +1 -0
  169. package/dist/sync/transport-chunking.js +56 -0
  170. package/dist/sync/transport-chunking.js.map +1 -0
  171. package/dist/sync/validate-push-payload.d.ts +1 -1
  172. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  173. package/dist/sync/validate-push-payload.js +6 -6
  174. package/dist/sync/validate-push-payload.js.map +1 -1
  175. package/dist/testing/event-factory.d.ts +68 -0
  176. package/dist/testing/event-factory.d.ts.map +1 -0
  177. package/dist/testing/event-factory.js +80 -0
  178. package/dist/testing/event-factory.js.map +1 -0
  179. package/dist/testing/mod.d.ts +2 -0
  180. package/dist/testing/mod.d.ts.map +1 -0
  181. package/dist/testing/mod.js +2 -0
  182. package/dist/testing/mod.js.map +1 -0
  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 +7 -8
  188. package/src/ClientSessionLeaderThreadProxy.ts +7 -2
  189. package/src/adapter-types.ts +13 -3
  190. package/src/devtools/devtools-messages-common.ts +1 -8
  191. package/src/errors.ts +33 -4
  192. package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
  193. package/src/leader-thread/eventlog.ts +10 -6
  194. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  195. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  196. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  197. package/src/leader-thread/materialize-event.ts +34 -9
  198. package/src/leader-thread/recreate-db.ts +11 -3
  199. package/src/leader-thread/shutdown-channel.ts +16 -2
  200. package/src/leader-thread/types.ts +7 -5
  201. package/src/materializer-helper.ts +22 -5
  202. package/src/rematerialize-from-eventlog.ts +33 -23
  203. package/src/schema/EventDef.ts +3 -0
  204. package/src/schema/LiveStoreEvent.ts +1 -2
  205. package/src/schema/mod.ts +2 -0
  206. package/src/schema/schema.ts +37 -1
  207. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  208. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  209. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  210. package/src/schema/state/sqlite/column-def.test.ts +150 -93
  211. package/src/schema/state/sqlite/column-def.ts +128 -203
  212. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  213. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  214. package/src/schema/state/sqlite/mod.ts +1 -0
  215. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  216. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  217. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  218. package/src/schema/state/sqlite/system-tables.ts +2 -0
  219. package/src/schema/state/sqlite/table-def.test.ts +64 -2
  220. package/src/schema/state/sqlite/table-def.ts +9 -8
  221. package/src/schema/unknown-events.ts +131 -0
  222. package/src/sql-queries/sql-query-builder.ts +2 -1
  223. package/src/sync/ClientSessionSyncProcessor.ts +55 -49
  224. package/src/sync/errors.ts +38 -0
  225. package/src/sync/index.ts +3 -0
  226. package/src/sync/mock-sync-backend.ts +184 -0
  227. package/src/sync/next/compact-events.ts +4 -5
  228. package/src/sync/next/facts.ts +1 -3
  229. package/src/sync/next/history-dag-common.ts +272 -21
  230. package/src/sync/next/history-dag.ts +3 -1
  231. package/src/sync/sync-backend-kv.ts +22 -0
  232. package/src/sync/sync-backend.ts +185 -0
  233. package/src/sync/sync.ts +6 -89
  234. package/src/sync/transport-chunking.ts +90 -0
  235. package/src/sync/validate-push-payload.ts +6 -7
  236. package/src/testing/event-factory.ts +133 -0
  237. package/src/testing/mod.ts +1 -0
  238. package/src/version.ts +2 -2
  239. package/dist/schema-management/migrations.test.d.ts +0 -2
  240. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  241. package/dist/schema-management/migrations.test.js +0 -52
  242. package/dist/schema-management/migrations.test.js.map +0 -1
  243. package/dist/sync/next/graphology.d.ts +0 -8
  244. package/dist/sync/next/graphology.d.ts.map +0 -1
  245. package/dist/sync/next/graphology.js +0 -30
  246. package/dist/sync/next/graphology.js.map +0 -1
  247. package/dist/sync/next/graphology_.d.ts +0 -3
  248. package/dist/sync/next/graphology_.d.ts.map +0 -1
  249. package/dist/sync/next/graphology_.js +0 -3
  250. package/dist/sync/next/graphology_.js.map +0 -1
  251. package/src/sync/next/ambient.d.ts +0 -3
  252. package/src/sync/next/graphology.ts +0 -41
  253. package/src/sync/next/graphology_.ts +0 -2
@@ -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
+ })
@@ -0,0 +1,185 @@
1
+ import {
2
+ type Cause,
3
+ type Effect,
4
+ type HttpClient,
5
+ type KeyValueStore,
6
+ Option,
7
+ Schema,
8
+ type Scope,
9
+ type Stream,
10
+ type SubscriptionRef,
11
+ } from '@livestore/utils/effect'
12
+ import type { UnexpectedError } from '../adapter-types.ts'
13
+ import type * as LiveStoreEvent from '../schema/LiveStoreEvent.ts'
14
+ import type { EventSequenceNumber } from '../schema/mod.ts'
15
+ import type { InvalidPullError, InvalidPushError, IsOfflineError } from './errors.ts'
16
+
17
+ export * from './sync-backend-kv.ts'
18
+
19
+ /**
20
+ * Those arguments can be used to implement multi-tenancy etc and are passed in from the store.
21
+ */
22
+ export type MakeBackendArgs = {
23
+ storeId: string
24
+ clientId: string
25
+ payload: Schema.JsonValue | undefined
26
+ }
27
+
28
+ // TODO rename to `SyncProviderClientConstructor`
29
+ export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
30
+ args: MakeBackendArgs,
31
+ ) => Effect.Effect<
32
+ SyncBackend<TSyncMetadata>,
33
+ UnexpectedError,
34
+ Scope.Scope | HttpClient.HttpClient | KeyValueStore.KeyValueStore
35
+ >
36
+
37
+ // TODO add more runtime sync metadata/metrics
38
+ // - latency histogram
39
+ // - number of events pushed/pulled
40
+ // - dynamic sync backend data;
41
+ // - data center location (e.g. colo on CF workers)
42
+
43
+ // TODO rename to `SyncProviderClient`
44
+ export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
45
+ /**
46
+ * Can be implemented to prepare a connection to the sync backend to speed up the first pull/push.
47
+ */
48
+ connect: Effect.Effect<void, IsOfflineError | UnexpectedError, Scope.Scope>
49
+ pull: (
50
+ cursor: Option.Option<{
51
+ eventSequenceNumber: EventSequenceNumber.GlobalEventSequenceNumber
52
+ /** Metadata is needed by some sync backends */
53
+ metadata: Option.Option<TSyncMetadata>
54
+ }>,
55
+ options?: {
56
+ /**
57
+ * If true, the sync backend will return a stream of events that have been pushed after the cursor.
58
+ *
59
+ * @default false
60
+ */
61
+ live?: boolean
62
+ },
63
+ ) => Stream.Stream<PullResItem<TSyncMetadata>, IsOfflineError | InvalidPullError>
64
+ // TODO support transactions (i.e. group of mutation events which need to be applied together)
65
+ push: (
66
+ /**
67
+ * Constraints for batch:
68
+ * - Number of events: 1-100
69
+ * - sequence numbers must be in ascending order
70
+ * */
71
+ batch: ReadonlyArray<LiveStoreEvent.AnyEncodedGlobal>,
72
+ ) => Effect.Effect<void, IsOfflineError | InvalidPushError>
73
+ ping: Effect.Effect<void, IsOfflineError | UnexpectedError | Cause.TimeoutException>
74
+ // TODO also expose latency information additionally to whether the backend is connected
75
+ isConnected: SubscriptionRef.SubscriptionRef<boolean>
76
+ /**
77
+ * Metadata describing the sync backend. (Currently only used by devtools.)
78
+ */
79
+ metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
80
+ /** Information about the sync backend capabilities. */
81
+ supports: {
82
+ /**
83
+ * Whether the sync backend supports the `hasMore` field in the pull response.
84
+ */
85
+ pullPageInfoKnown: boolean
86
+ /**
87
+ * Whether the sync backend supports the `live` option for the pull method and thus
88
+ * long-lived, reactive pull streams.
89
+ */
90
+ pullLive: boolean
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Connectivity metadata emitted by sync backends.
96
+ */
97
+ export const NetworkStatus = Schema.Struct({
98
+ /** True when the upstream sync backend is reachable and responding to health checks. */
99
+ isConnected: Schema.Boolean,
100
+ /** Unix epoch timestamp (ms) of the latest connectivity state transition. */
101
+ timestampMs: Schema.Number,
102
+ /** Devtools specific metadata describing simulator overrides. */
103
+ devtools: Schema.Struct({
104
+ /** Indicates whether the devtools latch forced the client into an offline state. */
105
+ latchClosed: Schema.Boolean,
106
+ }),
107
+ }).annotations({ title: 'NetworkStatus' })
108
+
109
+ export type NetworkStatus = typeof NetworkStatus.Type
110
+
111
+ /**
112
+ * Runtime type guard for SyncBackend objects.
113
+ * Performs lightweight structural checks on the object shape.
114
+ */
115
+ export const isSyncBackend = (value: unknown): value is SyncBackend<any> => {
116
+ if (typeof value !== 'object' || value === null) return false
117
+
118
+ const v: any = value
119
+ const hasCoreFns =
120
+ typeof v.connect === 'function' &&
121
+ typeof v.pull === 'function' &&
122
+ typeof v.push === 'function' &&
123
+ typeof v.ping === 'function'
124
+
125
+ const hasSupports =
126
+ typeof v.supports === 'object' &&
127
+ v.supports !== null &&
128
+ typeof v.supports.pullPageInfoKnown === 'boolean' &&
129
+ typeof v.supports.pullLive === 'boolean'
130
+
131
+ const hasMetadata =
132
+ typeof v.metadata === 'object' &&
133
+ v.metadata !== null &&
134
+ typeof v.metadata.name === 'string' &&
135
+ typeof v.metadata.description === 'string'
136
+
137
+ const hasIsConnected = typeof v.isConnected === 'object' && v.isConnected !== null
138
+
139
+ return hasCoreFns && hasSupports && hasMetadata && hasIsConnected
140
+ }
141
+
142
+ export const PullResPageInfo = Schema.Union(
143
+ Schema.TaggedStruct('MoreUnknown', {}),
144
+ Schema.TaggedStruct('MoreKnown', {
145
+ remaining: Schema.Number,
146
+ }),
147
+ Schema.TaggedStruct('NoMore', {}),
148
+ )
149
+
150
+ export type PullResPageInfo = typeof PullResPageInfo.Type
151
+
152
+ export const pageInfoNoMore: PullResPageInfo = { _tag: 'NoMore' } as const
153
+ export const pageInfoMoreUnknown: PullResPageInfo = { _tag: 'MoreUnknown' } as const
154
+ export const pageInfoMoreKnown = (remaining: number): PullResPageInfo => ({ _tag: 'MoreKnown', remaining })
155
+
156
+ export const pullResItemEmpty = <TSyncMetadata = Schema.JsonValue>(): PullResItem<TSyncMetadata> => ({
157
+ batch: [],
158
+ pageInfo: pageInfoNoMore,
159
+ })
160
+
161
+ export interface PullResItem<TSyncMetadata = Schema.JsonValue> {
162
+ batch: ReadonlyArray<{
163
+ eventEncoded: LiveStoreEvent.AnyEncodedGlobal
164
+ metadata: Option.Option<TSyncMetadata>
165
+ }>
166
+ pageInfo: PullResPageInfo
167
+ }
168
+
169
+ export const of = <TSyncMetadata = Schema.JsonValue>(obj: SyncBackend<TSyncMetadata>) => obj
170
+
171
+ /**
172
+ * Useful to continue pulling from the last event in the batch.
173
+ */
174
+ export const cursorFromPullResItem = <TSyncMetadata = Schema.JsonValue>(
175
+ item: PullResItem<TSyncMetadata>,
176
+ ): Option.Option<{
177
+ eventSequenceNumber: EventSequenceNumber.GlobalEventSequenceNumber
178
+ metadata: Option.Option<TSyncMetadata>
179
+ }> => {
180
+ const lastEvent = item.batch.at(-1)
181
+ if (!lastEvent) {
182
+ return Option.none()
183
+ }
184
+ return Option.some({ eventSequenceNumber: lastEvent.eventEncoded.seqNum, metadata: lastEvent.metadata })
185
+ }
package/src/sync/sync.ts CHANGED
@@ -1,19 +1,8 @@
1
- import type { Effect, HttpClient, Option, Scope, Stream, SubscriptionRef } from '@livestore/utils/effect'
2
- import { Schema } from '@livestore/utils/effect'
1
+ export * from './errors.ts'
2
+ export * as SyncBackend from './sync-backend.ts'
3
3
 
4
- import type { UnexpectedError } from '../adapter-types.ts'
5
4
  import type { InitialSyncOptions } from '../leader-thread/types.ts'
6
- import * as EventSequenceNumber from '../schema/EventSequenceNumber.ts'
7
- import type * as LiveStoreEvent from '../schema/LiveStoreEvent.ts'
8
-
9
- /**
10
- * Those arguments can be used to implement multi-tenancy etc and are passed in from the store.
11
- */
12
- export type MakeBackendArgs = {
13
- storeId: string
14
- clientId: string
15
- payload: Schema.JsonValue | undefined
16
- }
5
+ import type { SyncBackendConstructor } from './sync-backend.ts'
17
6
 
18
7
  export type SyncOptions = {
19
8
  backend?: SyncBackendConstructor<any>
@@ -29,81 +18,9 @@ export type SyncOptions = {
29
18
  * @default 'ignore'
30
19
  * */
31
20
  onSyncError?: 'shutdown' | 'ignore'
32
- }
33
-
34
- // TODO rename to `SyncProviderClientConstructor`
35
- export type SyncBackendConstructor<TSyncMetadata = Schema.JsonValue> = (
36
- args: MakeBackendArgs,
37
- ) => Effect.Effect<SyncBackend<TSyncMetadata>, UnexpectedError, Scope.Scope | HttpClient.HttpClient>
38
-
39
- // TODO add more runtime sync metadata
40
- // - latency histogram
41
- // - number of events pushed/pulled
42
- // - dynamic sync backend data;
43
- // - data center location (e.g. colo on CF workers)
44
-
45
- // TODO rename to `SyncProviderClient`
46
- export type SyncBackend<TSyncMetadata = Schema.JsonValue> = {
47
21
  /**
48
- * Can be implemented to prepare a connection to the sync backend to speed up the first pull/push.
22
+ * Whether the sync backend should reactively pull new events from the sync backend
23
+ * @default true
49
24
  */
50
- connect: Effect.Effect<void, IsOfflineError | UnexpectedError, HttpClient.HttpClient | Scope.Scope>
51
- pull: (
52
- args: Option.Option<{
53
- cursor: EventSequenceNumber.EventSequenceNumber
54
- metadata: Option.Option<TSyncMetadata>
55
- }>,
56
- ) => Stream.Stream<
57
- {
58
- batch: ReadonlyArray<{
59
- eventEncoded: LiveStoreEvent.AnyEncodedGlobal
60
- metadata: Option.Option<TSyncMetadata>
61
- }>
62
- remaining: number
63
- },
64
- IsOfflineError | InvalidPullError,
65
- HttpClient.HttpClient
66
- >
67
- // TODO support transactions (i.e. group of mutation events which need to be applied together)
68
- push: (
69
- /**
70
- * Constraints for batch:
71
- * - Number of events: 1-100
72
- * - sequence numbers must be in ascending order
73
- * */
74
- batch: ReadonlyArray<LiveStoreEvent.AnyEncodedGlobal>,
75
- ) => Effect.Effect<void, IsOfflineError | InvalidPushError, HttpClient.HttpClient>
76
- isConnected: SubscriptionRef.SubscriptionRef<boolean>
77
- /**
78
- * Metadata describing the sync backend. (Currently only used by devtools.)
79
- */
80
- metadata: { name: string; description: string } & Record<string, Schema.JsonValue>
25
+ livePull?: boolean
81
26
  }
82
-
83
- export class IsOfflineError extends Schema.TaggedError<IsOfflineError>()('IsOfflineError', {}) {}
84
-
85
- // TODO gt rid of this error in favour of SyncError
86
- export class InvalidPushError extends Schema.TaggedError<InvalidPushError>()('InvalidPushError', {
87
- reason: Schema.Union(
88
- Schema.TaggedStruct('Unexpected', {
89
- message: Schema.String,
90
- }),
91
- Schema.TaggedStruct('ServerAhead', {
92
- minimumExpectedNum: Schema.Number,
93
- providedNum: Schema.Number,
94
- }),
95
- ),
96
- }) {}
97
-
98
- // TODO gt rid of this error in favour of SyncError
99
- export class InvalidPullError extends Schema.TaggedError<InvalidPullError>()('InvalidPullError', {
100
- message: Schema.String,
101
- }) {}
102
-
103
- // TODO gt rid of this error in favour of SyncError
104
- export class LeaderAheadError extends Schema.TaggedError<LeaderAheadError>()('LeaderAheadError', {
105
- minimumExpectedNum: EventSequenceNumber.EventSequenceNumber,
106
- providedNum: EventSequenceNumber.EventSequenceNumber,
107
- /** Generation number the client session should use for subsequent pushes */
108
- // nextGeneration: Schema.Number,
109
- }) {}