@livestore/common 0.0.58-dev.1 → 0.0.58-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 (142) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +48 -6
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +16 -1
  5. package/dist/adapter-types.js.map +1 -1
  6. package/dist/bounded-collections.js.map +1 -1
  7. package/dist/derived-mutations.d.ts +5 -5
  8. package/dist/derived-mutations.d.ts.map +1 -1
  9. package/dist/derived-mutations.js +4 -2
  10. package/dist/derived-mutations.js.map +1 -1
  11. package/dist/derived-mutations.test.js +1 -0
  12. package/dist/derived-mutations.test.js.map +1 -1
  13. package/dist/devtools/devtools-bridge.d.ts +1 -1
  14. package/dist/devtools/devtools-bridge.d.ts.map +1 -1
  15. package/dist/devtools/devtools-messages.d.ts +91 -13
  16. package/dist/devtools/devtools-messages.d.ts.map +1 -1
  17. package/dist/devtools/devtools-messages.js +13 -4
  18. package/dist/devtools/devtools-messages.js.map +1 -1
  19. package/dist/devtools/index.d.ts +1 -0
  20. package/dist/devtools/index.d.ts.map +1 -1
  21. package/dist/devtools/index.js +2 -0
  22. package/dist/devtools/index.js.map +1 -1
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  26. package/dist/rehydrate-from-mutationlog.js +11 -5
  27. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  28. package/dist/schema/index.d.ts +2 -2
  29. package/dist/schema/index.d.ts.map +1 -1
  30. package/dist/schema/index.js +1 -1
  31. package/dist/schema/index.js.map +1 -1
  32. package/dist/schema/mutations.d.ts +137 -17
  33. package/dist/schema/mutations.d.ts.map +1 -1
  34. package/dist/schema/mutations.js +42 -16
  35. package/dist/schema/mutations.js.map +1 -1
  36. package/dist/schema/schema-helpers.js +1 -1
  37. package/dist/schema/schema-helpers.js.map +1 -1
  38. package/dist/schema/system-tables.d.ts +119 -6
  39. package/dist/schema/system-tables.d.ts.map +1 -1
  40. package/dist/schema/system-tables.js +22 -9
  41. package/dist/schema/system-tables.js.map +1 -1
  42. package/dist/schema/table-def.d.ts +3 -3
  43. package/dist/schema/table-def.d.ts.map +1 -1
  44. package/dist/schema/table-def.js +1 -1
  45. package/dist/schema/table-def.js.map +1 -1
  46. package/dist/schema-management/migrations.d.ts +1 -1
  47. package/dist/schema-management/migrations.d.ts.map +1 -1
  48. package/dist/schema-management/migrations.js +2 -2
  49. package/dist/schema-management/migrations.js.map +1 -1
  50. package/dist/sql-queries/sql-queries.d.ts +10 -3
  51. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  52. package/dist/sql-queries/sql-queries.js +8 -7
  53. package/dist/sql-queries/sql-queries.js.map +1 -1
  54. package/dist/sql-queries/sql-query-builder.d.ts +1 -1
  55. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  56. package/dist/sql-queries/types.d.ts +2 -2
  57. package/dist/sql-queries/types.d.ts.map +1 -1
  58. package/dist/sync/next/compact-events.d.ts +15 -0
  59. package/dist/sync/next/compact-events.d.ts.map +1 -0
  60. package/dist/sync/next/compact-events.js +176 -0
  61. package/dist/sync/next/compact-events.js.map +1 -0
  62. package/dist/sync/next/facts.d.ts +37 -0
  63. package/dist/sync/next/facts.d.ts.map +1 -0
  64. package/dist/sync/next/facts.js +156 -0
  65. package/dist/sync/next/facts.js.map +1 -0
  66. package/dist/sync/next/graphology.d.ts +8 -0
  67. package/dist/sync/next/graphology.d.ts.map +1 -0
  68. package/dist/sync/next/graphology.js +36 -0
  69. package/dist/sync/next/graphology.js.map +1 -0
  70. package/dist/sync/next/graphology_.d.ts +3 -0
  71. package/dist/sync/next/graphology_.d.ts.map +1 -0
  72. package/dist/sync/next/graphology_.js +3 -0
  73. package/dist/sync/next/graphology_.js.map +1 -0
  74. package/dist/sync/next/history-dag.d.ts +30 -0
  75. package/dist/sync/next/history-dag.d.ts.map +1 -0
  76. package/dist/sync/next/history-dag.js +69 -0
  77. package/dist/sync/next/history-dag.js.map +1 -0
  78. package/dist/sync/next/mod.d.ts +5 -0
  79. package/dist/sync/next/mod.d.ts.map +1 -0
  80. package/dist/sync/next/mod.js +5 -0
  81. package/dist/sync/next/mod.js.map +1 -0
  82. package/dist/sync/next/rebase-events.d.ts +27 -0
  83. package/dist/sync/next/rebase-events.d.ts.map +1 -0
  84. package/dist/sync/next/rebase-events.js +41 -0
  85. package/dist/sync/next/rebase-events.js.map +1 -0
  86. package/dist/sync/next/test/compact-events.calculator.test.d.ts +2 -0
  87. package/dist/sync/next/test/compact-events.calculator.test.d.ts.map +1 -0
  88. package/dist/sync/next/test/compact-events.calculator.test.js +101 -0
  89. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -0
  90. package/dist/sync/next/test/compact-events.test.d.ts +2 -0
  91. package/dist/sync/next/test/compact-events.test.d.ts.map +1 -0
  92. package/dist/sync/next/test/compact-events.test.js +201 -0
  93. package/dist/sync/next/test/compact-events.test.js.map +1 -0
  94. package/dist/sync/next/test/mod.d.ts +2 -0
  95. package/dist/sync/next/test/mod.d.ts.map +1 -0
  96. package/dist/sync/next/test/mod.js +2 -0
  97. package/dist/sync/next/test/mod.js.map +1 -0
  98. package/dist/sync/next/test/mutation-fixtures.d.ts +73 -0
  99. package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -0
  100. package/dist/sync/next/test/mutation-fixtures.js +161 -0
  101. package/dist/sync/next/test/mutation-fixtures.js.map +1 -0
  102. package/dist/sync/sync.d.ts +19 -6
  103. package/dist/sync/sync.d.ts.map +1 -1
  104. package/dist/sync/sync.js.map +1 -1
  105. package/dist/version.d.ts +8 -0
  106. package/dist/version.d.ts.map +1 -1
  107. package/dist/version.js +9 -0
  108. package/dist/version.js.map +1 -1
  109. package/package.json +21 -4
  110. package/src/adapter-types.ts +46 -7
  111. package/src/bounded-collections.ts +1 -1
  112. package/src/derived-mutations.test.ts +2 -1
  113. package/src/derived-mutations.ts +10 -10
  114. package/src/devtools/devtools-bridge.ts +1 -1
  115. package/src/devtools/devtools-messages.ts +12 -2
  116. package/src/devtools/index.ts +2 -0
  117. package/src/index.ts +6 -0
  118. package/src/rehydrate-from-mutationlog.ts +16 -7
  119. package/src/schema/index.ts +4 -3
  120. package/src/schema/mutations.ts +175 -30
  121. package/src/schema/schema-helpers.ts +1 -1
  122. package/src/schema/system-tables.ts +30 -9
  123. package/src/schema/table-def.ts +3 -3
  124. package/src/schema-management/migrations.ts +2 -2
  125. package/src/sql-queries/sql-queries.ts +21 -10
  126. package/src/sql-queries/sql-query-builder.ts +1 -1
  127. package/src/sql-queries/types.ts +2 -2
  128. package/src/sync/next/ambient.d.ts +3 -0
  129. package/src/sync/next/compact-events.ts +218 -0
  130. package/src/sync/next/facts.ts +229 -0
  131. package/src/sync/next/graphology.ts +49 -0
  132. package/src/sync/next/graphology_.ts +2 -0
  133. package/src/sync/next/history-dag.ts +109 -0
  134. package/src/sync/next/mod.ts +4 -0
  135. package/src/sync/next/rebase-events.ts +97 -0
  136. package/src/sync/next/test/compact-events.calculator.test.ts +121 -0
  137. package/src/sync/next/test/compact-events.test.ts +232 -0
  138. package/src/sync/next/test/mod.ts +1 -0
  139. package/src/sync/next/test/mutation-fixtures.ts +230 -0
  140. package/src/sync/sync.ts +30 -6
  141. package/src/version.ts +10 -0
  142. package/tsconfig.json +1 -1
@@ -0,0 +1,218 @@
1
+ import { replacesFacts } from './facts.js'
2
+ import { graphologyDag } from './graphology_.js'
3
+ import type { HistoryDag } from './history-dag.js'
4
+ import { emptyHistoryDag, eventIdToString } from './history-dag.js'
5
+
6
+ /**
7
+ * Idea:
8
+ * - iterate over all events from leaves to root
9
+ * - for each event
10
+ * - gradually make sub dags by following the event's fact dependencies
11
+ * - for each sub dag check and remove sub dags further up in the history dag that are a subset of the current sub dag
12
+ *
13
+ * TODO: try to implement this function on top of SQLite
14
+ */
15
+ export const compactEvents = (inputDag: HistoryDag): { dag: HistoryDag; compactedEventCount: number } => {
16
+ const dag = inputDag.copy()
17
+ const compactedEventCount = 0
18
+
19
+ const orderedEventIdStrs = graphologyDag.topologicalSort(dag).reverse()
20
+
21
+ // drop root
22
+ orderedEventIdStrs.pop()
23
+
24
+ for (const eventIdStr of orderedEventIdStrs) {
25
+ if (dag.hasNode(eventIdStr) === false) {
26
+ continue
27
+ }
28
+
29
+ const subDagsForEvent = Array.from(makeSubDagsForEvent(dag, eventIdStr))
30
+ for (const subDag of subDagsForEvent) {
31
+ let shouldRetry = true
32
+ while (shouldRetry) {
33
+ const subDagsInHistory = findSubDagsInHistory(dag, subDag, eventIdStr)
34
+
35
+ // console.debug(
36
+ // 'subDagsInHistory',
37
+ // eventIdStr,
38
+ // 'target',
39
+ // subDag.nodes(),
40
+ // 'found',
41
+ // ...subDagsInHistory.subDags.map((_) => _.nodes()),
42
+ // )
43
+
44
+ for (const subDagInHistory of subDagsInHistory.subDags) {
45
+ if (dagDependsOnDag(subDag, subDagInHistory, dag) === false) {
46
+ dropFromDag(dag, subDagInHistory)
47
+ }
48
+ }
49
+
50
+ // Sometimes some sub dags are ommitted because they depended on other sub dags in same batch.
51
+ // We can retry to also remove those.
52
+ // Implementation: retry if outsideDependencies overlap with deleted sub dags
53
+ if (
54
+ subDagsInHistory.allOutsideDependencies.some((outsideDependencies) =>
55
+ outsideDependencies.every((dep) => subDagsInHistory.subDags.some((subDag) => subDag.hasNode(dep))),
56
+ ) === false
57
+ ) {
58
+ shouldRetry = false
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ return { dag, compactedEventCount }
65
+ }
66
+
67
+ function* makeSubDagsForEvent(inputDag: HistoryDag, eventIdStr: string): Generator<HistoryDag> {
68
+ /** Map from eventIdStr to array of eventIdStrs that are dependencies */
69
+ let nextIterationEls: Map<string, string[]> = new Map([[eventIdStr, []]])
70
+ let previousDag: HistoryDag | undefined
71
+
72
+ while (nextIterationEls.size > 0) {
73
+ // start with a copy of the last sub dag to build on top of
74
+ const subDag = previousDag?.copy() ?? emptyHistoryDag()
75
+
76
+ const currentIterationEls = new Map(nextIterationEls)
77
+ nextIterationEls = new Map()
78
+
79
+ for (const [currentEventIdStr, edgeTargetIdStrs] of currentIterationEls) {
80
+ const node = inputDag.getNodeAttributes(currentEventIdStr)
81
+ if (subDag.hasNode(currentEventIdStr) === false) {
82
+ subDag.addNode(currentEventIdStr, { ...node })
83
+ }
84
+ for (const edgeTargetIdStr of edgeTargetIdStrs) {
85
+ subDag.addEdge(currentEventIdStr, edgeTargetIdStr, { type: 'facts' })
86
+ }
87
+
88
+ for (const depEdge of inputDag.outboundEdgeEntries(currentEventIdStr)) {
89
+ if (depEdge.attributes.type === 'facts') {
90
+ const depEventIdStr = depEdge.target
91
+ nextIterationEls.set(depEventIdStr, [...(nextIterationEls.get(depEventIdStr) ?? []), currentEventIdStr])
92
+ }
93
+ }
94
+ }
95
+
96
+ previousDag = subDag
97
+
98
+ // console.debug('subDag yield', subDag.nodes())
99
+ yield subDag
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Iterates over all events from root to `upToExclEventIdStr`
105
+ * and collects all valid sub dags that are replaced by `targetSubDag`.
106
+ */
107
+ const findSubDagsInHistory = (
108
+ inputDag: HistoryDag,
109
+ targetSubDag: HistoryDag,
110
+ upToExclEventIdStr: string,
111
+ ): { subDags: HistoryDag[]; allOutsideDependencies: string[][] } => {
112
+ const subDags: HistoryDag[] = []
113
+ const allOutsideDependencies: string[][] = []
114
+
115
+ for (const eventIdStr of graphologyDag.topologicalSort(inputDag)) {
116
+ if (eventIdStr === upToExclEventIdStr) {
117
+ break
118
+ }
119
+
120
+ for (const subDag of makeSubDagsForEvent(inputDag, eventIdStr)) {
121
+ // console.debug('findSubDagsInHistory', 'target', targetSubDag.nodes(), 'subDag', subDag.nodes())
122
+ if (subDag.size < targetSubDag.size) {
123
+ continue
124
+ }
125
+
126
+ const outsideDependencies = outsideDependenciesForDag(subDag, inputDag)
127
+ if (outsideDependencies.length > 0) {
128
+ allOutsideDependencies.push(outsideDependencies)
129
+ }
130
+
131
+ if (outsideDependencies.length === 0 && dagReplacesDag(subDag, targetSubDag)) {
132
+ subDags.push(subDag)
133
+ } else {
134
+ break
135
+ }
136
+ }
137
+ }
138
+
139
+ return { subDags, allOutsideDependencies }
140
+ }
141
+
142
+ const dropFromDag = (dag: HistoryDag, subDag: HistoryDag) => {
143
+ for (const nodeIdStr of subDag.nodes()) {
144
+ removeEvent(dag, nodeIdStr)
145
+ }
146
+ }
147
+
148
+ /** Returns outside dependencies of `subDag` (but inside `inputDag`) */
149
+ const outsideDependenciesForDag = (subDag: HistoryDag, inputDag: HistoryDag) => {
150
+ const outsideDependencies = []
151
+ for (const nodeIdStr of subDag.nodes()) {
152
+ for (const edgeEntry of inputDag.outboundEdgeEntries(nodeIdStr)) {
153
+ if (edgeEntry.attributes.type === 'facts') {
154
+ const depEventIdStr = edgeEntry.target
155
+ if (subDag.hasNode(depEventIdStr) === false) {
156
+ outsideDependencies.push(depEventIdStr)
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ return outsideDependencies
163
+ }
164
+
165
+ /** Checks whether dagA depends on dagB */
166
+ const dagDependsOnDag = (dagA: HistoryDag, dagB: HistoryDag, inputDag: HistoryDag): boolean => {
167
+ for (const nodeAIdStr of dagA.nodes()) {
168
+ for (const edgeEntryA of inputDag.inboundEdgeEntries(nodeAIdStr)) {
169
+ if (edgeEntryA.attributes.type === 'facts') {
170
+ const depNodeIdStr = edgeEntryA.target
171
+ if (dagB.hasNode(depNodeIdStr)) {
172
+ return true
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ return false
179
+ }
180
+
181
+ /** Checks if dagA replaces dagB */
182
+ const dagReplacesDag = (dagA: HistoryDag, dagB: HistoryDag): boolean => {
183
+ if (dagA.size !== dagB.size) {
184
+ return false
185
+ }
186
+
187
+ // TODO write tests that covers deterministic order when DAGs have branches
188
+ const nodeEntriesA = graphologyDag.topologicalSort(dagA).map((nodeId) => dagA.getNodeAttributes(nodeId))
189
+ const nodeEntriesB = graphologyDag.topologicalSort(dagB).map((nodeId) => dagB.getNodeAttributes(nodeId))
190
+
191
+ for (let i = 0; i < nodeEntriesA.length; i++) {
192
+ const nodeA = nodeEntriesA[i]!
193
+ const nodeB = nodeEntriesB[i]!
194
+
195
+ if (replacesFacts(nodeA.factsGroup, nodeB.factsGroup) === false) {
196
+ return false
197
+ }
198
+ }
199
+
200
+ return true
201
+ }
202
+
203
+ const removeEvent = (dag: HistoryDag, eventIdStr: string) => {
204
+ // console.debug('removing event', eventIdStr)
205
+ const event = dag.getNodeAttributes(eventIdStr)
206
+ const parentIdStr = eventIdToString(event.parentId)
207
+ const childEdges = dag.outboundEdgeEntries(eventIdStr)
208
+
209
+ for (const childEdge of childEdges) {
210
+ if (childEdge.attributes.type === 'parent') {
211
+ const childEvent = dag.getNodeAttributes(childEdge.target)
212
+ childEvent.parentId = { ...event.parentId }
213
+ dag.addEdge(parentIdStr, eventIdToString(childEvent.id), { type: 'parent' })
214
+ }
215
+ }
216
+
217
+ dag.dropNode(eventIdStr)
218
+ }
@@ -0,0 +1,229 @@
1
+ import { notYetImplemented } from '@livestore/utils'
2
+
3
+ import type { EventId } from '../../adapter-types.js'
4
+ import type {
5
+ FactsCallback,
6
+ MutationEventFactInput,
7
+ MutationEventFacts,
8
+ MutationEventFactsGroup,
9
+ MutationEventFactsSnapshot,
10
+ } from '../../schema/mutations.js'
11
+ import { graphologyDag } from './graphology_.js'
12
+ import { EMPTY_FACT_VALUE, type HistoryDag, type HistoryDagNode } from './history-dag.js'
13
+
14
+ export const factsSnapshotForEvents = (events: HistoryDagNode[], endEventId: EventId): MutationEventFactsSnapshot => {
15
+ const facts = new Map<string, any>()
16
+
17
+ for (const event of events) {
18
+ if (compareEventIds(event.id, endEventId) > 0) {
19
+ return facts
20
+ }
21
+
22
+ applyFactGroup(event.factsGroup, facts)
23
+ }
24
+
25
+ return facts
26
+ }
27
+
28
+ export const factsSnapshotForDag = (dag: HistoryDag, endEventId: EventId | undefined): MutationEventFactsSnapshot => {
29
+ const facts = new Map<string, any>()
30
+
31
+ const orderedEventIdStrs = graphologyDag.topologicalSort(dag)
32
+
33
+ for (let i = 0; i < orderedEventIdStrs.length; i++) {
34
+ const event = dag.getNodeAttributes(orderedEventIdStrs[i]!)
35
+ if (endEventId !== undefined && compareEventIds(event.id, endEventId) > 0) {
36
+ return facts
37
+ }
38
+
39
+ applyFactGroup(event.factsGroup, facts)
40
+ }
41
+
42
+ return facts
43
+ }
44
+
45
+ export type FactValidationResult =
46
+ | {
47
+ success: true
48
+ }
49
+ | {
50
+ success: false
51
+ /** Index of the item that caused the validation to fail */
52
+ index: number
53
+ requiredFacts: MutationEventFacts
54
+ mismatch: {
55
+ existing: MutationEventFacts
56
+ required: MutationEventFacts
57
+ }
58
+ currentSnapshot: MutationEventFacts
59
+ }
60
+
61
+ export const validateFacts = ({
62
+ factGroups,
63
+ initialSnapshot,
64
+ }: {
65
+ factGroups: MutationEventFactsGroup[]
66
+ initialSnapshot: MutationEventFactsSnapshot
67
+ }): FactValidationResult => {
68
+ const currentSnapshot = new Map(initialSnapshot)
69
+
70
+ for (const [index, factGroup] of factGroups.entries()) {
71
+ if (isSubSetMapByValue(factGroup.depRequire, currentSnapshot) === false) {
72
+ const existing = new Map()
73
+ const required = new Map()
74
+
75
+ for (const [key, value] of factGroup.depRequire) {
76
+ if (currentSnapshot.get(key) !== value) {
77
+ existing.set(key, currentSnapshot.get(key))
78
+ required.set(key, value)
79
+ }
80
+ }
81
+
82
+ return {
83
+ success: false,
84
+ index,
85
+ requiredFacts: factGroup.depRequire,
86
+ currentSnapshot,
87
+ mismatch: { existing, required },
88
+ }
89
+ }
90
+
91
+ applyFactGroup(factGroup, currentSnapshot)
92
+ }
93
+
94
+ return {
95
+ success: true,
96
+ }
97
+ }
98
+
99
+ export const applyFactGroups = (factGroups: MutationEventFactsGroup[], snapshot: MutationEventFactsSnapshot) => {
100
+ for (const factGroup of factGroups) {
101
+ applyFactGroup(factGroup, snapshot)
102
+ }
103
+ }
104
+
105
+ export const applyFactGroup = (factGroup: MutationEventFactsGroup, snapshot: MutationEventFactsSnapshot) => {
106
+ for (const [key, value] of factGroup.modifySet) {
107
+ snapshot.set(key, value)
108
+ }
109
+
110
+ for (const [key, _value] of factGroup.modifyUnset) {
111
+ snapshot.delete(key)
112
+ }
113
+ }
114
+
115
+ /** Check if setA is a subset of setB */
116
+ const isSubSetMapByValue = (setA: MutationEventFacts, setB: MutationEventFacts) => {
117
+ for (const [key, value] of setA) {
118
+ if (setB.get(key) !== value) {
119
+ return false
120
+ }
121
+ }
122
+ return true
123
+ }
124
+
125
+ /** Check if setA is a subset of setB */
126
+ const isSubSetMapByKey = (setA: MutationEventFacts, setB: MutationEventFacts) => {
127
+ for (const [key, _value] of setA) {
128
+ if (!setB.has(key)) {
129
+ return false
130
+ }
131
+ }
132
+ return true
133
+ }
134
+
135
+ /** Check if groupA depends on groupB */
136
+ export const dependsOn = (groupA: MutationEventFactsGroup, groupB: MutationEventFactsGroup): boolean =>
137
+ factsIntersect(groupA.depRead, groupB.modifySet) ||
138
+ factsIntersect(groupA.depRead, groupB.modifyUnset) ||
139
+ factsIntersect(groupA.depRequire, groupB.modifySet) ||
140
+ factsIntersect(groupA.depRequire, groupB.modifyUnset)
141
+
142
+ export const replacesFacts = (groupA: MutationEventFactsGroup, groupB: MutationEventFactsGroup): boolean => {
143
+ const replaces = (a: MutationEventFacts, b: MutationEventFacts) => a.size > 0 && b.size > 0 && isSameMapByKey(a, b)
144
+
145
+ const noFactsOrSame = (a: MutationEventFacts, b: MutationEventFacts) =>
146
+ a.size === 0 || b.size === 0 || isSameMapByKey(a, b)
147
+
148
+ return (
149
+ (replaces(groupA.modifySet, groupB.modifySet) && noFactsOrSame(groupA.modifyUnset, groupB.modifyUnset)) ||
150
+ (replaces(groupA.modifySet, groupB.modifyUnset) && noFactsOrSame(groupA.modifyUnset, groupB.modifySet)) ||
151
+ (replaces(groupA.modifyUnset, groupB.modifySet) && noFactsOrSame(groupA.modifySet, groupB.modifyUnset)) ||
152
+ (replaces(groupA.modifyUnset, groupB.modifyUnset) && noFactsOrSame(groupA.modifySet, groupB.modifySet))
153
+ )
154
+ }
155
+
156
+ export const isSameMapByKey = (set: MutationEventFacts, otherSet: MutationEventFacts) =>
157
+ set.size === otherSet.size && isSubSetMapByKey(set, otherSet)
158
+
159
+ export const factsToString = (facts: MutationEventFacts) => {
160
+ return Array.from(facts)
161
+ .map(([key, value]) => (value === EMPTY_FACT_VALUE ? key : `${key}=${value}`))
162
+ .join(', ')
163
+ }
164
+
165
+ export const factsIntersect = (setA: MutationEventFacts, setB: MutationEventFacts): boolean => {
166
+ for (const [key, _value] of setA) {
167
+ if (setB.has(key)) {
168
+ return true
169
+ }
170
+ }
171
+ return false
172
+ }
173
+
174
+ export const getFactsGroupForMutationArgs = ({
175
+ factsCallback,
176
+ args,
177
+ currentFacts,
178
+ }: {
179
+ factsCallback: FactsCallback<any> | undefined
180
+ args: any
181
+ currentFacts: MutationEventFactsSnapshot
182
+ }): MutationEventFactsGroup => {
183
+ const depRead: MutationEventFactsSnapshot = new Map<string, any>()
184
+ const factsSnapshotProxy = new Proxy(currentFacts, {
185
+ get: (target, prop) => {
186
+ if (prop === 'has') {
187
+ return (key: string) => {
188
+ depRead.set(key, EMPTY_FACT_VALUE)
189
+ return target.has(key)
190
+ }
191
+ } else if (prop === 'get') {
192
+ return (key: string) => {
193
+ depRead.set(key, EMPTY_FACT_VALUE)
194
+ return target.get(key)
195
+ }
196
+ }
197
+
198
+ notYetImplemented(`getFactsGroupForMutationArgs: ${prop.toString()} is not yet implemented`)
199
+ },
200
+ })
201
+
202
+ const factsRes = factsCallback?.(args, factsSnapshotProxy)
203
+ const iterableToMap = (iterable: Iterable<MutationEventFactInput>) => {
204
+ const map = new Map()
205
+ for (const item of iterable) {
206
+ if (typeof item === 'string') {
207
+ map.set(item, EMPTY_FACT_VALUE)
208
+ } else {
209
+ map.set(item[0], item[1])
210
+ }
211
+ }
212
+ return map
213
+ }
214
+ const facts = {
215
+ modifySet: factsRes?.modify.set ? iterableToMap(factsRes.modify.set) : new Map(),
216
+ modifyUnset: factsRes?.modify.unset ? iterableToMap(factsRes.modify.unset) : new Map(),
217
+ depRequire: factsRes?.require ? iterableToMap(factsRes.require) : new Map(),
218
+ depRead,
219
+ }
220
+
221
+ return facts
222
+ }
223
+
224
+ export const compareEventIds = (a: EventId, b: EventId) => {
225
+ if (a.global !== b.global) {
226
+ return a.global - b.global
227
+ }
228
+ return a.local - b.local
229
+ }
@@ -0,0 +1,49 @@
1
+ // TODO re-enable when `graphology` supports ESM `exports` and `.js` import/export syntax
2
+ // import {} from 'graphology'
3
+ import * as graphology_ from 'graphology'
4
+ import type * as graphologyTypes from 'graphology-types'
5
+
6
+ export const graphology = graphology_ as any
7
+
8
+ export declare class IGraph<
9
+ NodeAttributes extends graphologyTypes.Attributes = graphologyTypes.Attributes,
10
+ EdgeAttributes extends graphologyTypes.Attributes = graphologyTypes.Attributes,
11
+ GraphAttributes extends graphologyTypes.Attributes = graphologyTypes.Attributes,
12
+ > extends graphologyTypes.AbstractGraph<NodeAttributes, EdgeAttributes, GraphAttributes> {
13
+ constructor(options?: graphologyTypes.GraphOptions)
14
+ }
15
+
16
+ export const DirectedGraph = class DirectedGraph extends graphology.DirectedGraph {
17
+ constructor(options?: graphologyTypes.GraphOptions) {
18
+ super(options)
19
+ }
20
+ } as typeof IGraph
21
+
22
+ export const Graph = class Graph extends graphology.Graph {
23
+ constructor(options?: graphologyTypes.GraphOptions) {
24
+ super(options)
25
+ }
26
+ } as typeof IGraph
27
+
28
+ // export const graphology = graphology_ as graphologyTypes
29
+
30
+ /*
31
+
32
+ Example usage:
33
+
34
+ const dag = new graphology.DirectedGraph({ allowSelfLoops: false })
35
+
36
+ nodes.forEach((node) => dag.addNode(node.id, { width: node.data.label.length * 100, height: 40 }))
37
+ edges.forEach((edge) => {
38
+ // TODO do this filtering earlier
39
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) return
40
+
41
+ dag.addEdge(edge.source, edge.target)
42
+ })
43
+
44
+ graphologyLayout.random.assign(dag) // needed for initial `x`, `y` values
45
+ const sensibleSettings = forceAtlas2.inferSettings(dag)
46
+ // forceAtlas2.assign(dag, { iterations: 100, settings: { adjustSizes: true, } })
47
+ forceAtlas2.assign(dag, { iterations: 100, settings: sensibleSettings })
48
+
49
+ */
@@ -0,0 +1,2 @@
1
+ export * as graphology from './graphology.js'
2
+ export { default as graphologyDag } from 'graphology-dag'
@@ -0,0 +1,109 @@
1
+ import { type EventId, ROOT_ID } from '../../adapter-types.js'
2
+ import type { MutationEventFactsGroup } from '../../schema/mutations.js'
3
+ import { factsToString, validateFacts } from './facts.js'
4
+ import { graphology } from './graphology_.js'
5
+
6
+ export const connectionTypeOptions = ['parent', 'facts'] as const
7
+ export type ConnectionType = (typeof connectionTypeOptions)[number]
8
+
9
+ /**
10
+ * Eventlog represented as a multi-DAG including edges for
11
+ * - total-order (parent) relationships
12
+ * - dependency (requires/reads facts) relationships
13
+ */
14
+ export type HistoryDag = graphology.IGraph<HistoryDagNode, { type: ConnectionType }>
15
+
16
+ export const emptyHistoryDag = (): HistoryDag =>
17
+ new graphology.Graph({
18
+ allowSelfLoops: false,
19
+ multi: true,
20
+ type: 'directed',
21
+ })
22
+
23
+ // TODO consider making `ROOT_ID` parent to itself
24
+ const rootParentId = { global: ROOT_ID.global - 1, local: 0 } satisfies EventId
25
+
26
+ export type HistoryDagNode = {
27
+ id: EventId
28
+ parentId: EventId
29
+ mutation: string
30
+ args: any
31
+ /** Facts are being used for conflict detection and history compaction */
32
+ factsGroup: MutationEventFactsGroup
33
+ meta?: any
34
+ }
35
+
36
+ export const rootEventNode: HistoryDagNode = {
37
+ id: ROOT_ID,
38
+ parentId: rootParentId,
39
+ // unused below
40
+ mutation: '__Root__',
41
+ args: {},
42
+ factsGroup: { modifySet: new Map(), modifyUnset: new Map(), depRequire: new Map(), depRead: new Map() },
43
+ }
44
+
45
+ export const EMPTY_FACT_VALUE = Symbol('EMPTY_FACT_VALUE')
46
+
47
+ export const eventIdToString = (eventId: EventId) =>
48
+ eventId.local === 0 ? eventId.global.toString() : `${eventId.global}.${eventId.local}`
49
+
50
+ export const historyDagFromNodes = (dagNodes: HistoryDagNode[], options?: { skipFactsCheck: boolean }) => {
51
+ if (options?.skipFactsCheck !== true) {
52
+ const validationResult = validateFacts({
53
+ factGroups: dagNodes.map((node) => node.factsGroup),
54
+ initialSnapshot: new Map<string, any>(),
55
+ })
56
+
57
+ if (validationResult.success === false) {
58
+ throw new Error(
59
+ `Mutation ${dagNodes[validationResult.index]!.mutation} requires facts that have not been set yet.\nRequires: ${factsToString(validationResult.requiredFacts)}\nFacts Snapshot: ${factsToString(validationResult.currentSnapshot)}`,
60
+ )
61
+ }
62
+ }
63
+
64
+ const dag = emptyHistoryDag()
65
+
66
+ dagNodes.forEach((node) => dag.addNode(eventIdToString(node.id), node))
67
+
68
+ dagNodes.forEach((node) => {
69
+ if (eventIdToString(node.parentId) !== eventIdToString(rootParentId)) {
70
+ dag.addEdge(eventIdToString(node.parentId), eventIdToString(node.id), { type: 'parent' })
71
+ }
72
+ })
73
+
74
+ dagNodes.forEach((node) => {
75
+ const factKeys = [...node.factsGroup.depRequire.keys(), ...node.factsGroup.depRead.keys()]
76
+ for (const factKey of factKeys) {
77
+ // Find the first ancestor node with a matching fact key (via modifySet or modifyUnset) by traversing the graph backwards via the parent edges
78
+ const depNode = (() => {
79
+ let currentIdStr = eventIdToString(node.id)
80
+
81
+ while (currentIdStr !== eventIdToString(rootParentId)) {
82
+ const parentEdge = dag.inEdges(currentIdStr).find((e) => dag.getEdgeAttribute(e, 'type') === 'parent')
83
+ if (!parentEdge) return null
84
+
85
+ const parentIdStr = dag.source(parentEdge)
86
+ const parentNode = dag.getNodeAttributes(parentIdStr)
87
+
88
+ if (parentNode.factsGroup.modifySet.has(factKey) || parentNode.factsGroup.modifyUnset.has(factKey)) {
89
+ return parentNode
90
+ }
91
+
92
+ currentIdStr = parentIdStr
93
+ }
94
+
95
+ return null
96
+ })()
97
+
98
+ if (depNode) {
99
+ const depNodeIdStr = eventIdToString(depNode.id)
100
+ const nodeIdStr = eventIdToString(node.id)
101
+ if (dag.edges(depNodeIdStr, nodeIdStr).filter((e) => dag.getEdgeAttributes(e).type === 'facts').length === 0) {
102
+ dag.addEdge(depNodeIdStr, nodeIdStr, { type: 'facts' })
103
+ }
104
+ }
105
+ }
106
+ })
107
+
108
+ return dag
109
+ }
@@ -0,0 +1,4 @@
1
+ export * from './history-dag.js'
2
+ export * from './facts.js'
3
+ export * from './compact-events.js'
4
+ export * from './rebase-events.js'