@livestore/common 0.0.58-dev.5 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapter-types.d.ts +29 -3
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +6 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/derived-mutations.d.ts +12 -12
- package/dist/derived-mutations.d.ts.map +1 -1
- package/dist/derived-mutations.test.js +1 -0
- package/dist/derived-mutations.test.js.map +1 -1
- package/dist/devtools/devtools-bridge.d.ts +1 -1
- package/dist/devtools/devtools-bridge.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.d.ts +99 -21
- package/dist/devtools/devtools-messages.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.js +13 -4
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -0
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/devtools/index.js +2 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +11 -5
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/mutations.d.ts +137 -18
- package/dist/schema/mutations.d.ts.map +1 -1
- package/dist/schema/mutations.js +41 -16
- package/dist/schema/mutations.js.map +1 -1
- package/dist/schema/system-tables.d.ts +130 -8
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +23 -8
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema/table-def.d.ts +1 -1
- package/dist/schema-management/migrations.js +1 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sync/next/compact-events.d.ts +15 -0
- package/dist/sync/next/compact-events.d.ts.map +1 -0
- package/dist/sync/next/compact-events.js +176 -0
- package/dist/sync/next/compact-events.js.map +1 -0
- package/dist/sync/next/facts.d.ts +37 -0
- package/dist/sync/next/facts.d.ts.map +1 -0
- package/dist/sync/next/facts.js +155 -0
- package/dist/sync/next/facts.js.map +1 -0
- package/dist/sync/next/graphology.d.ts +8 -0
- package/dist/sync/next/graphology.d.ts.map +1 -0
- package/dist/sync/next/graphology.js +36 -0
- package/dist/sync/next/graphology.js.map +1 -0
- package/dist/sync/next/graphology_.d.ts +3 -0
- package/dist/sync/next/graphology_.d.ts.map +1 -0
- package/dist/sync/next/graphology_.js +3 -0
- package/dist/sync/next/graphology_.js.map +1 -0
- package/dist/sync/next/history-dag.d.ts +30 -0
- package/dist/sync/next/history-dag.d.ts.map +1 -0
- package/dist/sync/next/history-dag.js +69 -0
- package/dist/sync/next/history-dag.js.map +1 -0
- package/dist/sync/next/mod.d.ts +5 -0
- package/dist/sync/next/mod.d.ts.map +1 -0
- package/dist/sync/next/mod.js +5 -0
- package/dist/sync/next/mod.js.map +1 -0
- package/dist/sync/next/mutation-fixtures.d.ts +73 -0
- package/dist/sync/next/mutation-fixtures.d.ts.map +1 -0
- package/dist/sync/next/mutation-fixtures.js +160 -0
- package/dist/sync/next/mutation-fixtures.js.map +1 -0
- package/dist/sync/next/rebase-events.d.ts +27 -0
- package/dist/sync/next/rebase-events.d.ts.map +1 -0
- package/dist/sync/next/rebase-events.js +41 -0
- package/dist/sync/next/rebase-events.js.map +1 -0
- package/dist/sync/next/test/compact-events.calculator.test.d.ts +2 -0
- package/dist/sync/next/test/compact-events.calculator.test.d.ts.map +1 -0
- package/dist/sync/next/test/compact-events.calculator.test.js +101 -0
- package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -0
- package/dist/sync/next/test/compact-events.test.d.ts +2 -0
- package/dist/sync/next/test/compact-events.test.d.ts.map +1 -0
- package/dist/sync/next/test/compact-events.test.js +201 -0
- package/dist/sync/next/test/compact-events.test.js.map +1 -0
- package/dist/sync/next/test/mod.d.ts +2 -0
- package/dist/sync/next/test/mod.d.ts.map +1 -0
- package/dist/sync/next/test/mod.js +2 -0
- package/dist/sync/next/test/mod.js.map +1 -0
- package/dist/sync/next/test/mutation-fixtures.d.ts +73 -0
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +1 -0
- package/dist/sync/next/test/mutation-fixtures.js +161 -0
- package/dist/sync/next/test/mutation-fixtures.js.map +1 -0
- package/dist/sync/sync.d.ts +19 -6
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/package.json +21 -4
- package/src/adapter-types.ts +27 -4
- package/src/derived-mutations.test.ts +2 -1
- package/src/derived-mutations.ts +5 -5
- package/src/devtools/devtools-bridge.ts +1 -1
- package/src/devtools/devtools-messages.ts +12 -2
- package/src/devtools/index.ts +2 -0
- package/src/index.ts +6 -0
- package/src/rehydrate-from-mutationlog.ts +16 -7
- package/src/schema/mutations.ts +171 -30
- package/src/schema/system-tables.ts +31 -8
- package/src/schema-management/migrations.ts +1 -1
- package/src/sync/next/ambient.d.ts +3 -0
- package/src/sync/next/compact-events.ts +219 -0
- package/src/sync/next/facts.ts +228 -0
- package/src/sync/next/graphology.ts +49 -0
- package/src/sync/next/graphology_.ts +2 -0
- package/src/sync/next/history-dag.ts +109 -0
- package/src/sync/next/mod.ts +4 -0
- package/src/sync/next/rebase-events.ts +97 -0
- package/src/sync/next/test/compact-events.calculator.test.ts +121 -0
- package/src/sync/next/test/compact-events.test.ts +232 -0
- package/src/sync/next/test/mod.ts +1 -0
- package/src/sync/next/test/mutation-fixtures.ts +230 -0
- package/src/sync/sync.ts +30 -6
@@ -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,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
|
+
}
|