@livestore/common 0.0.58-dev.6 → 0.0.58-dev.7

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