@wovin/core 0.1.36 → 0.2.2
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/README.md +0 -12
- package/dist/applog/applog-helpers.d.ts +12 -12
- package/dist/applog/applog-helpers.d.ts.map +1 -1
- package/dist/applog/applog-utils.d.ts +40 -6
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts +67 -12
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.d.ts +3 -3
- package/dist/applog.d.ts.map +1 -1
- package/dist/{applog.min.js → applog.js} +12 -7
- package/dist/blockstore.d.ts +1 -1
- package/dist/blockstore.d.ts.map +1 -1
- package/dist/{blockstore.min.js → blockstore.js} +1 -3
- package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
- package/dist/chunk-22WDFLXO.js +138 -0
- package/dist/chunk-22WDFLXO.js.map +1 -0
- package/dist/chunk-3SUFNJEZ.js +1026 -0
- package/dist/chunk-3SUFNJEZ.js.map +1 -0
- package/dist/chunk-6ALNRM3J.js +435 -0
- package/dist/chunk-6ALNRM3J.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/{chunk-KXMTKPF4.min.js → chunk-BLF5MAWU.js} +8 -8
- package/dist/chunk-BLF5MAWU.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/{chunk-H3VQJP56.min.js → chunk-HUIQ54TT.js} +9 -9
- package/dist/chunk-HUIQ54TT.js.map +1 -0
- package/dist/{chunk-BRC7LSM6.min.js → chunk-OC6Z6CQW.js} +5 -5
- package/dist/chunk-OC6Z6CQW.js.map +1 -0
- package/dist/chunk-SHUHRHOT.js +1923 -0
- package/dist/chunk-SHUHRHOT.js.map +1 -0
- package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/{index.min.js → index.js} +81 -46
- package/dist/ipfs/car.d.ts +11 -11
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs/ipfs-utils.d.ts +2 -2
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
- package/dist/ipfs.d.ts +3 -3
- package/dist/ipfs.d.ts.map +1 -1
- package/dist/{ipfs.min.js → ipfs.js} +7 -10
- package/dist/ipns.d.ts +1 -1
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +3 -3
- package/dist/pubsub/pub-pull.d.ts.map +1 -1
- package/dist/pubsub/pubsub-types.d.ts +3 -3
- package/dist/pubsub/pubsub-types.d.ts.map +1 -1
- package/dist/pubsub/snap-push.d.ts +4 -4
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub/ucan.d.ts +1 -1
- package/dist/pubsub/ucan.d.ts.map +1 -1
- package/dist/pubsub.d.ts +4 -4
- package/dist/pubsub.d.ts.map +1 -1
- package/dist/{pubsub.min.js → pubsub.js} +7 -10
- package/dist/query/attr-helpers.d.ts +5 -0
- package/dist/query/attr-helpers.d.ts.map +1 -0
- package/dist/query/basic.d.ts +87 -23
- package/dist/query/basic.d.ts.map +1 -1
- package/dist/query/divergences.d.ts +5 -5
- package/dist/query/divergences.d.ts.map +1 -1
- package/dist/query/entity-collection.d.ts +19 -0
- package/dist/query/entity-collection.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +12 -1
- package/dist/query/matchers.d.ts.map +1 -1
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/situations.d.ts +2 -1
- package/dist/query/situations.d.ts.map +1 -1
- package/dist/query/subscribable.d.ts +111 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +54 -14
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query.d.ts +9 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/{query.min.js → query.js} +55 -34
- package/dist/retrieve/index.d.ts +1 -1
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/update-thread.d.ts +3 -3
- package/dist/retrieve/update-thread.d.ts.map +1 -1
- package/dist/retrieve.d.ts +1 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +14 -0
- package/dist/thread/basic.d.ts +15 -19
- package/dist/thread/basic.d.ts.map +1 -1
- package/dist/thread/filters.d.ts +8 -10
- package/dist/thread/filters.d.ts.map +1 -1
- package/dist/thread/indexes.d.ts +57 -0
- package/dist/thread/indexes.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +40 -11
- package/dist/thread/mapped.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +5 -5
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/writeable.d.ts +2 -2
- package/dist/thread/writeable.d.ts.map +1 -1
- package/dist/thread.d.ts +6 -5
- package/dist/thread.d.ts.map +1 -1
- package/dist/{thread.min.js → thread.js} +9 -6
- package/dist/types/typescript-utils.d.ts +6 -5
- package/dist/types/typescript-utils.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{types.min.js → types.js} +3 -4
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/package.json +32 -23
- package/src/applog/applog-helpers.ts +155 -0
- package/src/applog/applog-utils.test.ts +108 -0
- package/src/applog/applog-utils.ts +551 -0
- package/src/applog/datom-types.ts +167 -0
- package/src/applog/object-values.test.ts +106 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +278 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/attr-helpers.ts +5 -0
- package/src/query/basic.ts +1245 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/entity-collection.ts +132 -0
- package/src/query/liveFilterAndMap.test.ts +102 -0
- package/src/query/matchers.ts +30 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +538 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +234 -0
- package/src/query/types.ts +155 -0
- package/src/query/withoutDeleted.test.ts +204 -0
- package/src/query.ts +9 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +182 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +227 -0
- package/src/thread/indexes.ts +256 -0
- package/src/thread/joinThreads.test.ts +304 -0
- package/src/thread/mapped.ts +226 -0
- package/src/thread/utils.ts +144 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +6 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
- package/dist/chunk-2Y2PYHGR.min.js +0 -65
- package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
- package/dist/chunk-5MMGBK2U.min.js +0 -1
- package/dist/chunk-7IDQIMQO.min.js +0 -1
- package/dist/chunk-BRC7LSM6.min.js.map +0 -1
- package/dist/chunk-COXXILXC.min.js +0 -512
- package/dist/chunk-COXXILXC.min.js.map +0 -1
- package/dist/chunk-GDX2OO7L.min.js +0 -9080
- package/dist/chunk-GDX2OO7L.min.js.map +0 -1
- package/dist/chunk-H3VQJP56.min.js.map +0 -1
- package/dist/chunk-HYMC7W6S.min.js +0 -1549
- package/dist/chunk-HYMC7W6S.min.js.map +0 -1
- package/dist/chunk-KEHU7HGZ.min.js +0 -5216
- package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
- package/dist/chunk-KXMTKPF4.min.js.map +0 -1
- package/dist/chunk-PHITDXZT.min.js +0 -36
- package/dist/chunk-QO2KMGDN.min.js +0 -3771
- package/dist/chunk-QO2KMGDN.min.js.map +0 -1
- package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
- package/dist/chunk-WXLCBTHX.min.js +0 -1606
- package/dist/chunk-WXLCBTHX.min.js.map +0 -1
- package/dist/ipns.min.js +0 -6419
- package/dist/ipns.min.js.map +0 -1
- package/dist/mobx/mobx-utils.d.ts +0 -82
- package/dist/mobx/mobx-utils.d.ts.map +0 -1
- package/dist/mobx.d.ts +0 -2
- package/dist/mobx.d.ts.map +0 -1
- package/dist/mobx.min.js +0 -141
- package/dist/retrieve.min.js +0 -17
- package/dist/types.min.js.map +0 -1
- package/dist/utils.min.js +0 -10
- package/dist/utils.min.js.map +0 -1
- /package/dist/{applog.min.js.map → applog.js.map} +0 -0
- /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
- /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
- /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
- /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
- /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
- /package/dist/{mobx.min.js.map → query.js.map} +0 -0
- /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
- /package/dist/{query.min.js.map → thread.js.map} +0 -0
- /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
- /package/dist/{thread.min.js.map → utils.js.map} +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for joinThreads — the multi-parent thread merge.
|
|
3
|
+
*
|
|
4
|
+
* Covers: initial merge, incremental adds, deduplication, setParents,
|
|
5
|
+
* triggerRemap, and subscriber notifications.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { finalizeApplogForInsert, joinThreads } from '../applog/applog-helpers.ts'
|
|
9
|
+
import { sortApplogsByTs } from '../applog/applog-utils.ts'
|
|
10
|
+
import type { Applog, ApplogForInsert, CidString } from '../applog/datom-types.ts'
|
|
11
|
+
import { isInitEvent, type ThreadEvent } from './basic.ts'
|
|
12
|
+
import { ThreadInMemory } from './writeable.ts'
|
|
13
|
+
|
|
14
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
let tsCounter = 0
|
|
17
|
+
|
|
18
|
+
/** Create a valid Applog with proper CID (can be used with insertRaw) */
|
|
19
|
+
function makeLog(overrides: Partial<Applog> & Pick<ApplogForInsert, 'en' | 'at' | 'vl'>): Applog {
|
|
20
|
+
tsCounter++
|
|
21
|
+
const base = {
|
|
22
|
+
ts: overrides.ts ?? new Date(1700000000000 + tsCounter * 1000).toISOString(),
|
|
23
|
+
pv: null,
|
|
24
|
+
ag: 'testAgent',
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
// Use finalizeApplogForInsert to get a real CID
|
|
28
|
+
return finalizeApplogForInsert(base as ApplogForInsert, {})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeLogs(...specs: Array<Partial<Applog> & Pick<ApplogForInsert, 'en' | 'at' | 'vl'>>): Applog[] {
|
|
32
|
+
return sortApplogsByTs(specs.map(s => makeLog(s)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function threadFrom(logs: Applog[], name = 'test'): ThreadInMemory {
|
|
36
|
+
return ThreadInMemory.fromArray(logs, name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
tsCounter = 0
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// ═════════════════════════════════════════════════════════════════
|
|
44
|
+
// Initial merge (compute)
|
|
45
|
+
// ═════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
describe('joinThreads — initial merge', () => {
|
|
48
|
+
it('merges applogs from two threads', () => {
|
|
49
|
+
const logsA = makeLogs(
|
|
50
|
+
{ en: 'e1', at: 'name', vl: 'Alice' },
|
|
51
|
+
)
|
|
52
|
+
const logsB = makeLogs(
|
|
53
|
+
{ en: 'e2', at: 'name', vl: 'Bob' },
|
|
54
|
+
)
|
|
55
|
+
const joined = joinThreads([threadFrom(logsA, 'A'), threadFrom(logsB, 'B')])
|
|
56
|
+
|
|
57
|
+
expect(joined.size).toBe(2)
|
|
58
|
+
expect(joined.applogs.map(l => l.vl)).toContain('Alice')
|
|
59
|
+
expect(joined.applogs.map(l => l.vl)).toContain('Bob')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('deduplicates identical applogs across threads', () => {
|
|
63
|
+
const shared = makeLog({ en: 'e1', at: 'name', vl: 'Alice' })
|
|
64
|
+
const a = threadFrom([shared], 'A')
|
|
65
|
+
const b = threadFrom([shared], 'B')
|
|
66
|
+
const joined = joinThreads([a, b])
|
|
67
|
+
|
|
68
|
+
expect(joined.size).toBe(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('sorts merged result by timestamp', () => {
|
|
72
|
+
const early = makeLog({ en: 'e1', at: 'x', vl: 1, ts: '2020-01-01T00:00:00Z' })
|
|
73
|
+
const late = makeLog({ en: 'e2', at: 'x', vl: 2, ts: '2025-01-01T00:00:00Z' })
|
|
74
|
+
// Give them to threads in reverse order
|
|
75
|
+
const joined = joinThreads([threadFrom([late], 'A'), threadFrom([early], 'B')])
|
|
76
|
+
|
|
77
|
+
expect(joined.applogs[0]).toBe(early)
|
|
78
|
+
expect(joined.applogs[1]).toBe(late)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('handles empty threads', () => {
|
|
82
|
+
const logsA = makeLogs({ en: 'e1', at: 'name', vl: 'Alice' })
|
|
83
|
+
const joined = joinThreads([threadFrom(logsA, 'A'), threadFrom([], 'B')])
|
|
84
|
+
|
|
85
|
+
expect(joined.size).toBe(1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles joining a single thread', () => {
|
|
89
|
+
const logsA = makeLogs({ en: 'e1', at: 'name', vl: 'Alice' })
|
|
90
|
+
const joined = joinThreads([threadFrom(logsA, 'A')])
|
|
91
|
+
|
|
92
|
+
expect(joined.size).toBe(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('merges three threads', () => {
|
|
96
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
97
|
+
const b = threadFrom(makeLogs({ en: 'e2', at: 'x', vl: 2 }), 'B')
|
|
98
|
+
const c = threadFrom(makeLogs({ en: 'e3', at: 'x', vl: 3 }), 'C')
|
|
99
|
+
const joined = joinThreads([a, b, c])
|
|
100
|
+
|
|
101
|
+
expect(joined.size).toBe(3)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ═════════════════════════════════════════════════════════════════
|
|
106
|
+
// Incremental updates (mapDelta — added)
|
|
107
|
+
// ═════════════════════════════════════════════════════════════════
|
|
108
|
+
|
|
109
|
+
describe('joinThreads — incremental adds', () => {
|
|
110
|
+
it('propagates new applog from one parent', () => {
|
|
111
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
112
|
+
const b = threadFrom([], 'B')
|
|
113
|
+
const joined = joinThreads([a, b])
|
|
114
|
+
|
|
115
|
+
expect(joined.size).toBe(1)
|
|
116
|
+
|
|
117
|
+
// Insert into B
|
|
118
|
+
const newLog = makeLog({ en: 'e2', at: 'x', vl: 2 })
|
|
119
|
+
b.insertRaw([newLog])
|
|
120
|
+
|
|
121
|
+
expect(joined.size).toBe(2)
|
|
122
|
+
expect(joined.applogs).toContain(newLog)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('deduplicates when same log is added to second parent', () => {
|
|
126
|
+
const shared = makeLog({ en: 'e1', at: 'x', vl: 1 })
|
|
127
|
+
const a = threadFrom([shared], 'A')
|
|
128
|
+
const b = threadFrom([], 'B')
|
|
129
|
+
const joined = joinThreads([a, b])
|
|
130
|
+
|
|
131
|
+
expect(joined.size).toBe(1)
|
|
132
|
+
|
|
133
|
+
// Insert the same log into B — should not duplicate
|
|
134
|
+
b.insertRaw([shared])
|
|
135
|
+
|
|
136
|
+
expect(joined.size).toBe(1)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('notifies subscribers on add', () => {
|
|
140
|
+
const a = threadFrom([], 'A')
|
|
141
|
+
const b = threadFrom([], 'B')
|
|
142
|
+
const joined = joinThreads([a, b])
|
|
143
|
+
|
|
144
|
+
const events: ThreadEvent[] = []
|
|
145
|
+
joined.subscribe(e => events.push(e))
|
|
146
|
+
|
|
147
|
+
const newLog = makeLog({ en: 'e1', at: 'x', vl: 1 })
|
|
148
|
+
a.insertRaw([newLog])
|
|
149
|
+
|
|
150
|
+
expect(events).toHaveLength(1)
|
|
151
|
+
expect(isInitEvent(events[0])).toBe(false)
|
|
152
|
+
if (!isInitEvent(events[0])) {
|
|
153
|
+
expect(events[0].added).toContain(newLog)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('inserts maintain sort order', () => {
|
|
158
|
+
const early = makeLog({ en: 'e1', at: 'x', vl: 1, ts: '2020-01-01T00:00:00Z' })
|
|
159
|
+
const a = threadFrom([early], 'A')
|
|
160
|
+
const b = threadFrom([], 'B')
|
|
161
|
+
const joined = joinThreads([a, b])
|
|
162
|
+
|
|
163
|
+
const late = makeLog({ en: 'e2', at: 'x', vl: 2, ts: '2025-01-01T00:00:00Z' })
|
|
164
|
+
b.insertRaw([late])
|
|
165
|
+
|
|
166
|
+
expect(joined.applogs[0]).toBe(early)
|
|
167
|
+
expect(joined.applogs[1]).toBe(late)
|
|
168
|
+
|
|
169
|
+
// Now insert something in between
|
|
170
|
+
const mid = makeLog({ en: 'e3', at: 'x', vl: 3, ts: '2022-06-01T00:00:00Z' })
|
|
171
|
+
a.insertRaw([mid])
|
|
172
|
+
|
|
173
|
+
expect(joined.applogs[0]).toBe(early)
|
|
174
|
+
expect(joined.applogs[1]).toBe(mid)
|
|
175
|
+
expect(joined.applogs[2]).toBe(late)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ═════════════════════════════════════════════════════════════════
|
|
180
|
+
// setParents — swap input threads without new result reference
|
|
181
|
+
// ═════════════════════════════════════════════════════════════════
|
|
182
|
+
|
|
183
|
+
describe('joinThreads — setParents', () => {
|
|
184
|
+
it('replaces parents and recomputes', () => {
|
|
185
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
186
|
+
const b = threadFrom(makeLogs({ en: 'e2', at: 'x', vl: 2 }), 'B')
|
|
187
|
+
const joined = joinThreads([a, b])
|
|
188
|
+
|
|
189
|
+
expect(joined.size).toBe(2)
|
|
190
|
+
|
|
191
|
+
// Swap to a completely different set of parents
|
|
192
|
+
const c = threadFrom(makeLogs({ en: 'e3', at: 'x', vl: 3 }), 'C')
|
|
193
|
+
joined.setParents([c])
|
|
194
|
+
|
|
195
|
+
expect(joined.size).toBe(1)
|
|
196
|
+
expect(joined.applogs[0].vl).toBe(3)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('notifies subscribers with init event on setParents', () => {
|
|
200
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
201
|
+
const joined = joinThreads([a])
|
|
202
|
+
|
|
203
|
+
const events: ThreadEvent[] = []
|
|
204
|
+
joined.subscribe(e => events.push(e))
|
|
205
|
+
|
|
206
|
+
const b = threadFrom(makeLogs({ en: 'e2', at: 'x', vl: 2 }), 'B')
|
|
207
|
+
joined.setParents([b])
|
|
208
|
+
|
|
209
|
+
expect(events).toHaveLength(1)
|
|
210
|
+
expect(isInitEvent(events[0])).toBe(true)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('subscribes to new parents after setParents', () => {
|
|
214
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
215
|
+
const joined = joinThreads([a])
|
|
216
|
+
|
|
217
|
+
const b = threadFrom([], 'B')
|
|
218
|
+
joined.setParents([b])
|
|
219
|
+
|
|
220
|
+
expect(joined.size).toBe(0)
|
|
221
|
+
|
|
222
|
+
// Insert into new parent — should propagate
|
|
223
|
+
const newLog = makeLog({ en: 'e2', at: 'x', vl: 2 })
|
|
224
|
+
b.insertRaw([newLog])
|
|
225
|
+
|
|
226
|
+
expect(joined.size).toBe(1)
|
|
227
|
+
expect(joined.applogs).toContain(newLog)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('unsubscribes from old parents after setParents', () => {
|
|
231
|
+
const a = threadFrom([], 'A')
|
|
232
|
+
const joined = joinThreads([a])
|
|
233
|
+
|
|
234
|
+
const events: ThreadEvent[] = []
|
|
235
|
+
joined.subscribe(e => events.push(e))
|
|
236
|
+
|
|
237
|
+
const b = threadFrom([], 'B')
|
|
238
|
+
joined.setParents([b])
|
|
239
|
+
|
|
240
|
+
// Clear the init event from setParents
|
|
241
|
+
events.length = 0
|
|
242
|
+
|
|
243
|
+
// Insert into OLD parent — should NOT propagate
|
|
244
|
+
a.insertRaw([makeLog({ en: 'e1', at: 'x', vl: 1 })])
|
|
245
|
+
|
|
246
|
+
expect(events).toHaveLength(0)
|
|
247
|
+
expect(joined.size).toBe(0)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// ═════════════════════════════════════════════════════════════════
|
|
252
|
+
// triggerRemap — full recompute
|
|
253
|
+
// ═════════════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
describe('joinThreads — triggerRemap', () => {
|
|
256
|
+
it('recomputes from current parents', () => {
|
|
257
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
258
|
+
const b = threadFrom(makeLogs({ en: 'e2', at: 'x', vl: 2 }), 'B')
|
|
259
|
+
const joined = joinThreads([a, b])
|
|
260
|
+
|
|
261
|
+
expect(joined.size).toBe(2)
|
|
262
|
+
|
|
263
|
+
joined.triggerRemap()
|
|
264
|
+
|
|
265
|
+
expect(joined.size).toBe(2)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('emits init event to subscribers', () => {
|
|
269
|
+
const a = threadFrom(makeLogs({ en: 'e1', at: 'x', vl: 1 }), 'A')
|
|
270
|
+
const joined = joinThreads([a])
|
|
271
|
+
|
|
272
|
+
const events: ThreadEvent[] = []
|
|
273
|
+
joined.subscribe(e => events.push(e))
|
|
274
|
+
|
|
275
|
+
joined.triggerRemap()
|
|
276
|
+
|
|
277
|
+
expect(events).toHaveLength(1)
|
|
278
|
+
expect(isInitEvent(events[0])).toBe(true)
|
|
279
|
+
if (isInitEvent(events[0])) {
|
|
280
|
+
expect(events[0].init).toHaveLength(1)
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// ═════════════════════════════════════════════════════════════════
|
|
286
|
+
// dispose
|
|
287
|
+
// ═════════════════════════════════════════════════════════════════
|
|
288
|
+
|
|
289
|
+
describe('joinThreads — dispose', () => {
|
|
290
|
+
it('stops receiving parent updates after dispose', () => {
|
|
291
|
+
const a = threadFrom([], 'A')
|
|
292
|
+
const joined = joinThreads([a])
|
|
293
|
+
|
|
294
|
+
const events: ThreadEvent[] = []
|
|
295
|
+
joined.subscribe(e => events.push(e))
|
|
296
|
+
|
|
297
|
+
joined.dispose()
|
|
298
|
+
|
|
299
|
+
a.insertRaw([makeLog({ en: 'e1', at: 'x', vl: 1 })])
|
|
300
|
+
|
|
301
|
+
// No events should have been received
|
|
302
|
+
expect(events).toHaveLength(0)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import { DebouncedFunc, pull, sortedIndexBy } from 'lodash-es'
|
|
3
|
+
import { Applog, ApplogForInsert } from '../applog/datom-types.ts'
|
|
4
|
+
import { arrayIfSingle } from '../types/typescript-utils.ts'
|
|
5
|
+
import { isInitEvent, Thread, ThreadEvent } from './basic.ts'
|
|
6
|
+
|
|
7
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
8
|
+
|
|
9
|
+
// ── ThreadDerivation ────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Delta event — only incremental changes (no init) */
|
|
12
|
+
export type DeltaEvent = { added: readonly Applog[], removed: readonly Applog[] | null }
|
|
13
|
+
|
|
14
|
+
/** Context passed to mapDelta — explicit deps, no `this` needed */
|
|
15
|
+
export interface DeltaContext {
|
|
16
|
+
/** The parent thread that emitted the delta */
|
|
17
|
+
source: Thread
|
|
18
|
+
/** All current parent threads */
|
|
19
|
+
parents: readonly Thread[]
|
|
20
|
+
/** The mapped thread's output before this delta is applied */
|
|
21
|
+
state: readonly Applog[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derivation for a MappedThread — separates full recomputation from incremental updates.
|
|
26
|
+
*
|
|
27
|
+
* - `compute` produces full state from parents (construction, reset, parent reinit)
|
|
28
|
+
* - `mapDelta` transforms a parent's incremental delta (optional — falls back to compute)
|
|
29
|
+
*/
|
|
30
|
+
export interface ThreadDerivation {
|
|
31
|
+
/** Compute full state from parents. Called at construction, triggerRemap, and parent reinit. */
|
|
32
|
+
compute(parents: readonly Thread[]): Applog[]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform an incremental delta from a parent.
|
|
36
|
+
* Return null to fall back to full recompute via compute().
|
|
37
|
+
* Optional — if omitted, every parent change triggers compute().
|
|
38
|
+
*/
|
|
39
|
+
mapDelta?: (delta: DeltaEvent, ctx: DeltaContext) => DeltaEvent | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Write mapper (unchanged) ───────────────────────────────────
|
|
43
|
+
|
|
44
|
+
// export type ApplogWriteMapper<T extends Applog[] | ApplogForInsert[]> = (this: MappedThread, applogs: T) => T | null // FIXME: the TS generics don't actually work how I want them to
|
|
45
|
+
export type ApplogWriteMapper = (this: MappedThread, applogs: Applog[] | ApplogForInsert[]) => Applog[] | ApplogForInsert[] | null
|
|
46
|
+
|
|
47
|
+
// ── MappedThread ───────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export class MappedThread extends Thread {
|
|
50
|
+
static mapWrites(parent: Thread, name: string, mapper: ApplogWriteMapper) {
|
|
51
|
+
return new MappedThread(
|
|
52
|
+
`${name}<${parent.nameAndSizeUntracked}>`,
|
|
53
|
+
parent,
|
|
54
|
+
parent.filters,
|
|
55
|
+
null, // no derivation — share parent array
|
|
56
|
+
mapper,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
static asReadOnly(parent: Thread) {
|
|
60
|
+
if (parent.readOnly) return parent // already read-only, no need to wrap
|
|
61
|
+
return new MappedThread(
|
|
62
|
+
`readOnly(${parent.name})`,
|
|
63
|
+
parent,
|
|
64
|
+
parent.filters,
|
|
65
|
+
null, // no derivation — share parent array
|
|
66
|
+
null,
|
|
67
|
+
true, // readOnly
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
private _parentSubscriptions: Map<Thread, (() => void)> = null // mapped to unsubscribe function
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
readonly name: string,
|
|
74
|
+
parents: Thread | readonly Thread[],
|
|
75
|
+
filters: readonly string[],
|
|
76
|
+
private _derivation: ThreadDerivation | null,
|
|
77
|
+
private _writeMapper: ApplogWriteMapper = null,
|
|
78
|
+
private _readOnly?: boolean,
|
|
79
|
+
) {
|
|
80
|
+
const parentArr = arrayIfSingle(parents) as readonly Thread[]
|
|
81
|
+
|
|
82
|
+
if (!_derivation && parentArr.length > 1) {
|
|
83
|
+
throw new Error(`MappedThread without derivation must have exactly one parent`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const initialLogs = _derivation
|
|
87
|
+
? _derivation.compute(parentArr)
|
|
88
|
+
: (parentArr[0] as any)._applogs as Applog[] // no derivation = share parent array
|
|
89
|
+
|
|
90
|
+
super(
|
|
91
|
+
name,
|
|
92
|
+
parents,
|
|
93
|
+
filters,
|
|
94
|
+
_derivation ? [...initialLogs] : initialLogs, // clone if derived, share if not
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if (_derivation) {
|
|
98
|
+
this.subscribeToParents()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public insert(appLogsToInsert: ApplogForInsert[]) {
|
|
103
|
+
if (this.readOnly) throw ERROR(`[MappedThread] insert() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
104
|
+
const mapped = this._writeMapper ? this._writeMapper(appLogsToInsert) : appLogsToInsert
|
|
105
|
+
if (this._writeMapper && !mapped) return
|
|
106
|
+
return this.parents.forEach(parent => parent.insert(mapped))
|
|
107
|
+
}
|
|
108
|
+
public insertRaw(appLogsToInsert: Applog[]) {
|
|
109
|
+
if (this.readOnly) throw ERROR(`[MappedThread] insertRaw() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
110
|
+
const mapped = this._writeMapper ? this._writeMapper(appLogsToInsert) : appLogsToInsert
|
|
111
|
+
if (this._writeMapper && !mapped) return
|
|
112
|
+
return this.parents.forEach(parent => parent.insertRaw(mapped as Applog[]))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private subscribeToParents() {
|
|
116
|
+
this._parentSubscriptions = new Map()
|
|
117
|
+
if (!this.parents.length) {
|
|
118
|
+
WARN(`MappedThread has no parents`, this)
|
|
119
|
+
}
|
|
120
|
+
VERBOSE(`[MappedThread: ${this.name}] subscribing to parents:`, this.parents.map(p => p.name))
|
|
121
|
+
|
|
122
|
+
for (const p of this.parents) {
|
|
123
|
+
VERBOSE(`[MappedThread: ${this.name}] sub to parent`, p.nameAndSizeUntracked)
|
|
124
|
+
const sub = this.onParentUpdate.bind(this, p)
|
|
125
|
+
const unsubscribe = p.subscribe(sub, 'derived')
|
|
126
|
+
this._parentSubscriptions.set(p, unsubscribe)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Tear down parent subscriptions and clear internal state */
|
|
131
|
+
dispose() {
|
|
132
|
+
if (this._parentSubscriptions) {
|
|
133
|
+
for (const [, unsubscribe] of this._parentSubscriptions) {
|
|
134
|
+
unsubscribe()
|
|
135
|
+
}
|
|
136
|
+
this._parentSubscriptions = null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Swap parents at runtime — re-subscribes and recomputes applogs, notifying downstream */
|
|
141
|
+
setParents(newParents: readonly Thread[]) {
|
|
142
|
+
this.dispose()
|
|
143
|
+
;(this as { parents: readonly Thread[] }).parents = newParents
|
|
144
|
+
this.subscribeToParents()
|
|
145
|
+
this.triggerRemap()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
subscribe(callback: (event: ThreadEvent) => void, type?: 'derived' | 'reaction') {
|
|
149
|
+
if (this._derivation && !this._parentSubscriptions) {
|
|
150
|
+
this.subscribeToParents()
|
|
151
|
+
}
|
|
152
|
+
return super.subscribe(callback, type)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Recompute full state from parents via compute() */
|
|
156
|
+
triggerRemap() {
|
|
157
|
+
if (!this._derivation) throw ERROR(`triggerRemap on a thread without derivation`, this.nameAndSizeUntracked)
|
|
158
|
+
DEBUG(`MappedThread{${this.nameAndSizeUntracked}} triggerRemap`)
|
|
159
|
+
const newLogs = this._derivation.compute(this.parents as readonly Thread[])
|
|
160
|
+
this._applogs.length = 0
|
|
161
|
+
this._applogs.push(...newLogs)
|
|
162
|
+
this.notifySubscribers({ init: newLogs })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected onParentUpdate(sourceThread: Thread, event: ThreadEvent) {
|
|
166
|
+
if (!this._derivation) return // no derivation = shared array, parent handles it
|
|
167
|
+
|
|
168
|
+
VERBOSE(`MappedThread{${this.nameAndSizeUntracked}} parentUpdate`, event)
|
|
169
|
+
|
|
170
|
+
let result: ThreadEvent
|
|
171
|
+
|
|
172
|
+
if (isInitEvent(event)) {
|
|
173
|
+
// Parent reinit → full recompute (ignore payload, read current parent state)
|
|
174
|
+
const newLogs = this._derivation.compute(this.parents as readonly Thread[])
|
|
175
|
+
result = { init: newLogs }
|
|
176
|
+
} else if (this._derivation.mapDelta) {
|
|
177
|
+
const ctx: DeltaContext = {
|
|
178
|
+
source: sourceThread,
|
|
179
|
+
parents: this.parents as readonly Thread[],
|
|
180
|
+
state: this._applogs,
|
|
181
|
+
}
|
|
182
|
+
const mapped = this._derivation.mapDelta(event, ctx)
|
|
183
|
+
if (mapped === null) {
|
|
184
|
+
// mapDelta can't handle it → fall back to full recompute
|
|
185
|
+
const newLogs = this._derivation.compute(this.parents as readonly Thread[])
|
|
186
|
+
result = { init: newLogs }
|
|
187
|
+
} else {
|
|
188
|
+
result = mapped
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// No mapDelta → always full recompute
|
|
192
|
+
const newLogs = this._derivation.compute(this.parents as readonly Thread[])
|
|
193
|
+
result = { init: newLogs }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Apply result to _applogs
|
|
197
|
+
if (isInitEvent(result)) {
|
|
198
|
+
this._applogs.length = 0
|
|
199
|
+
this._applogs.push(...result.init)
|
|
200
|
+
} else {
|
|
201
|
+
VERBOSE(`MappedThread{${this.nameAndSizeUntracked}} parentUpdate => mapped`, result)
|
|
202
|
+
for (const log of result.added) {
|
|
203
|
+
// insert at right location to maintain sort order
|
|
204
|
+
this._applogs.splice(sortedIndexBy(this._applogs, log, 'ts'), 0, log)
|
|
205
|
+
}
|
|
206
|
+
if (result.removed) {
|
|
207
|
+
for (const toRemove of result.removed) {
|
|
208
|
+
const idx = this._applogs.indexOf(toRemove)
|
|
209
|
+
if (idx >= 0) {
|
|
210
|
+
this._applogs.splice(idx, 1)
|
|
211
|
+
} else {
|
|
212
|
+
throw ERROR(`MappedThread{${this.name}} toRemove: log not found`, toRemove, {
|
|
213
|
+
thread: this,
|
|
214
|
+
event,
|
|
215
|
+
result,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.notifySubscribers(result)
|
|
222
|
+
}
|
|
223
|
+
get readOnly() {
|
|
224
|
+
return this._readOnly ?? false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import { curry, debounce, partial, pull, uniq, uniqBy, uniqWith } from 'lodash-es'
|
|
3
|
+
|
|
4
|
+
import { CID } from 'multiformats'
|
|
5
|
+
import { ensureTsPvAndFinalizeApplogs, joinThreads } from '../applog/applog-helpers.ts'
|
|
6
|
+
import { compareApplogsByEnAt, compareApplogsByTs, objEqualByKeys } from '../applog/applog-utils.ts'
|
|
7
|
+
import { Applog, ApplogForInsert, CidString, EntityID } from '../applog/datom-types.ts'
|
|
8
|
+
import { Thread } from './basic.ts'
|
|
9
|
+
import { MappedThread } from './mapped.ts'
|
|
10
|
+
import { ThreadInMemory, WriteableThread } from './writeable.ts'
|
|
11
|
+
|
|
12
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
13
|
+
|
|
14
|
+
export function entityCount(thread: Thread) {
|
|
15
|
+
return allEntityIDs(thread).size // at some point: index size?
|
|
16
|
+
}
|
|
17
|
+
export function allEntityIDs(thread: Thread) {
|
|
18
|
+
return accumulateLogsToSet(thread, log => log.en)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function accumulateLogsToSet<ACC>(
|
|
22
|
+
threadOrLogs: Thread | Applog[],
|
|
23
|
+
callback: (log: Applog, acc: Set<ACC>) => ACC | undefined,
|
|
24
|
+
) {
|
|
25
|
+
const logs = threadOrLogs instanceof Thread ? threadOrLogs.applogs : threadOrLogs
|
|
26
|
+
const set = new Set<ACC>()
|
|
27
|
+
for (const log of logs) {
|
|
28
|
+
set.add(callback(log, set))
|
|
29
|
+
}
|
|
30
|
+
return set
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function debounceWrites(
|
|
34
|
+
thread: Thread,
|
|
35
|
+
wait = 700,
|
|
36
|
+
removeDuplicatesWith = compareApplogsByEnAt,
|
|
37
|
+
) {
|
|
38
|
+
if (thread.readOnly) throw ERROR(`[debounceWrites] but readOnly thread`, thread.name)
|
|
39
|
+
let insertQueue: Array<Applog | ApplogForInsert> = []
|
|
40
|
+
|
|
41
|
+
const debouncedCommit = debounce(() => {
|
|
42
|
+
WARN(`Debounce tail`, { thread, mappedThread, insertQueue })
|
|
43
|
+
const toInsert = ensureTsPvAndFinalizeApplogs(
|
|
44
|
+
// ? uniq, sure - but which one is used? (update: seems the first one, so reverse)
|
|
45
|
+
uniqWith(insertQueue.reverse(), removeDuplicatesWith),
|
|
46
|
+
thread,
|
|
47
|
+
)
|
|
48
|
+
thread.insertRaw(toInsert)
|
|
49
|
+
insertQueue.splice(0, insertQueue.length) // clear queue
|
|
50
|
+
}, wait)
|
|
51
|
+
const handleInsert = (applogs: Applog[] | ApplogForInsert[]) => {
|
|
52
|
+
DEBUG(`Debounce input:`, applogs, { thread, mappedThread, insertQueue })
|
|
53
|
+
insertQueue.push(...applogs)
|
|
54
|
+
debouncedCommit()
|
|
55
|
+
return null // don't insert anything
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const mappedThread = MappedThread.mapWrites(
|
|
59
|
+
thread,
|
|
60
|
+
`Debounce(${wait})`,
|
|
61
|
+
handleInsert,
|
|
62
|
+
)
|
|
63
|
+
return mappedThread
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param deduplicateHoldItemsWith called with (heldLog, realLog), if it returns true, the held log is skipped
|
|
68
|
+
* @param onFirstWrite called with held logs about to be inserted, if it returns an array, those logs will be inserted instead
|
|
69
|
+
*/
|
|
70
|
+
export function holdTillFirstWrite(
|
|
71
|
+
thread: Thread,
|
|
72
|
+
applogsToHold: ApplogForInsert[],
|
|
73
|
+
opts: {
|
|
74
|
+
deduplicateHoldItemsWith?: typeof compareApplogsByEnAt
|
|
75
|
+
onFirstWrite: (heldLogsToInsert: Applog[]) => Applog[] | undefined
|
|
76
|
+
},
|
|
77
|
+
) {
|
|
78
|
+
DEBUG(`[holdTillFirstWrite] holding logs:`, { applogsToHold })
|
|
79
|
+
if (thread.readOnly) throw ERROR(`[holdTillFirstWrite] but readOnly thread`, thread.nameAndSizeUntracked)
|
|
80
|
+
const heldLogs = ensureTsPvAndFinalizeApplogs([...applogsToHold], thread)
|
|
81
|
+
const heldThread = ThreadInMemory.fromArray(heldLogs)
|
|
82
|
+
|
|
83
|
+
let hasInserted = false
|
|
84
|
+
const handleInsert = (realApplogs: Applog[] | ApplogForInsert[]) => {
|
|
85
|
+
if (hasInserted) return realApplogs
|
|
86
|
+
hasInserted = true
|
|
87
|
+
|
|
88
|
+
let toInsert = [...heldLogs]
|
|
89
|
+
// heldLogs.splice(0, heldLogs.length) // ? joinThread could take care of this
|
|
90
|
+
if (opts.deduplicateHoldItemsWith) {
|
|
91
|
+
toInsert = toInsert.filter(heldLog =>
|
|
92
|
+
// some duplicate? so don't insert
|
|
93
|
+
!realApplogs.some(realLog => opts.deduplicateHoldItemsWith(heldLog, realLog))
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (opts.onFirstWrite) {
|
|
97
|
+
const callbackResult = opts.onFirstWrite(toInsert)
|
|
98
|
+
if (callbackResult !== undefined) {
|
|
99
|
+
toInsert = callbackResult
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
DEBUG(`[holdTillFirstWrite] adding hold logs:`, { applogsToHold, heldLogs, toInsert, realApplogs })
|
|
103
|
+
return [...toInsert, ...realApplogs]
|
|
104
|
+
}
|
|
105
|
+
const joinedThread = joinThreads([thread, heldThread])
|
|
106
|
+
return new MappedThread(
|
|
107
|
+
`HoldTillFirstWrite[${applogsToHold.length}]<${thread.nameAndSizeUntracked}>`,
|
|
108
|
+
joinedThread,
|
|
109
|
+
thread.filters,
|
|
110
|
+
null, // no derivation — shares joinedThread's array
|
|
111
|
+
(applogs) => {
|
|
112
|
+
const logsToInsert = handleInsert(applogs)
|
|
113
|
+
thread.insert(logsToInsert)
|
|
114
|
+
return null
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getAgents(thread: Thread) {
|
|
120
|
+
return uniq(thread.map(l => l.ag))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns applogs NOT contained in the exclusion set.
|
|
125
|
+
* Uses CID-based comparison (consistent with removeDuplicateAppLogs).
|
|
126
|
+
*/
|
|
127
|
+
export const excludeApplogsContainedIn = (
|
|
128
|
+
applogs: readonly Applog[],
|
|
129
|
+
exclude: Thread | readonly Applog[] | Set<CID | CidString>,
|
|
130
|
+
): Applog[] => {
|
|
131
|
+
const excludeCids: Set<string> = exclude instanceof Set
|
|
132
|
+
? new Set([...exclude].map(c => c.toString()))
|
|
133
|
+
: exclude instanceof Thread
|
|
134
|
+
? new Set(exclude.applogsCids)
|
|
135
|
+
: new Set(exclude.map(a => a.cid))
|
|
136
|
+
|
|
137
|
+
return applogs.filter(applog => {
|
|
138
|
+
if (!applog.cid) {
|
|
139
|
+
ERROR(`applog with missing CID`, applog)
|
|
140
|
+
throw new Error(`applog with missing CID`)
|
|
141
|
+
}
|
|
142
|
+
return !excludeCids.has(applog.cid)
|
|
143
|
+
})
|
|
144
|
+
}
|