@wovin/core 0.1.35 → 0.2.0
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 +25 -6
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts +4 -5
- 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} +6 -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-KXMTKPF4.min.js → chunk-3JZMOEOD.js} +8 -8
- package/dist/chunk-3JZMOEOD.js.map +1 -0
- package/dist/chunk-3WZVG277.js +434 -0
- package/dist/chunk-3WZVG277.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-CPSDKFBG.js +147 -0
- package/dist/chunk-CPSDKFBG.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/{chunk-H3VQJP56.min.js → chunk-J2FDHGOZ.js} +9 -9
- package/dist/chunk-J2FDHGOZ.js.map +1 -0
- package/dist/chunk-L5EEEGE6.js +1862 -0
- package/dist/chunk-L5EEEGE6.js.map +1 -0
- package/dist/{chunk-BRC7LSM6.min.js → chunk-PD3C7XUM.js} +5 -5
- package/dist/chunk-PD3C7XUM.js.map +1 -0
- package/dist/chunk-QZXKQCAY.js +1026 -0
- package/dist/chunk-QZXKQCAY.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} +73 -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 +85 -21
- 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 +1 -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} +51 -32
- 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 +56 -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 +507 -0
- package/src/applog/datom-types.ts +148 -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 +277 -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 +131 -0
- package/src/query/liveFilterAndMap.test.ts +102 -0
- package/src/query/matchers.ts +8 -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 +250 -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,209 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import { createBLAKE3 } from 'hash-wasm'
|
|
3
|
+
import { pick } from 'lodash-es'
|
|
4
|
+
import { CID } from 'multiformats'
|
|
5
|
+
import { arraysContainSameElements } from '../applog/applog-utils.ts'
|
|
6
|
+
import { areApplogsEqual } from '../applog/applog-utils.ts'
|
|
7
|
+
import { type Applog, ApplogForInsert, CidString } from '../applog/datom-types.ts'
|
|
8
|
+
import type { SubscribableArray, ArrayEvent } from '../query/subscribable.ts'
|
|
9
|
+
import { isArrayInitEvent } from '../query/subscribable.ts'
|
|
10
|
+
import { areCidsEqual } from '../ipfs/ipfs-utils.ts'
|
|
11
|
+
import { prettifyThreadName } from '../utils/debug-name.ts'
|
|
12
|
+
import { arrayIfSingle, ArrayOrSingle } from '../types/typescript-utils.ts'
|
|
13
|
+
|
|
14
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO, { prefix: '[thread]' })
|
|
15
|
+
|
|
16
|
+
export type ThreadEvent = ArrayEvent<Applog>
|
|
17
|
+
/** @deprecated Use isArrayInitEvent from @wovin/core/query */
|
|
18
|
+
export const isInitEvent = isArrayInitEvent
|
|
19
|
+
|
|
20
|
+
export type ApplogsOrThread = Thread | readonly Applog[]
|
|
21
|
+
|
|
22
|
+
// const blakeHasher = await createBLAKE3()
|
|
23
|
+
|
|
24
|
+
export abstract class Thread implements SubscribableArray<Applog> {
|
|
25
|
+
readonly filters: readonly string[]
|
|
26
|
+
readonly parents: Thread[] | readonly Thread[] | null
|
|
27
|
+
protected _derivedSubscribers: ((event: ThreadEvent) => void)[] = []
|
|
28
|
+
protected _subscribers: ((event: ThreadEvent) => void)[] = []
|
|
29
|
+
/** Monotonic counter incremented on every mutation. Used by memoizedFn to invalidate caches. */
|
|
30
|
+
_version = 0
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
readonly name: string, /* = null */
|
|
34
|
+
parents: ArrayOrSingle<Thread> | readonly Thread[] | null,
|
|
35
|
+
filters: readonly string[],
|
|
36
|
+
protected _applogs: Applog[] = [],
|
|
37
|
+
) {
|
|
38
|
+
this.parents = parents === null ? null : arrayIfSingle(parents) as readonly Thread[]
|
|
39
|
+
this.filters = filters // ? uniq([...parents?.map(p => p.filters), filters])
|
|
40
|
+
if (this.parents?.length === 0) {
|
|
41
|
+
WARN(`[Thread] empty parents array`, name) // just to see where it happens, is actually mostly fine
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get readOnly() {
|
|
46
|
+
if (this.parents.length !== 1) return true // ? multi-parent writable stream? - we don't have a use-case for this yet, but could this be a thing?
|
|
47
|
+
return this.parents[0].readOnly
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public insert(appLogsToInsert: ArrayOrSingle<ApplogForInsert>) {
|
|
51
|
+
if (this.readOnly) throw ERROR(`[Thread] insert() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
52
|
+
if (!this.parents) throw ERROR(`[Thread] insert() called on non-writable thread without parents:`, this.nameAndSizeUntracked)
|
|
53
|
+
if (this.parents?.length !== 1) throw ERROR(`[Thread] insert() called on thread with multiple parents:`, this.nameAndSizeUntracked)
|
|
54
|
+
return this.parents[0].insert(appLogsToInsert)
|
|
55
|
+
}
|
|
56
|
+
public insertRaw(appLogsToInsert: readonly Applog[]) {
|
|
57
|
+
if (this.readOnly) throw ERROR(`[Thread] insertRaw() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
58
|
+
if (!this.parents) throw ERROR(`[Thread] insertRaw() called on non-writable thread without parents:`, this.nameAndSizeUntracked)
|
|
59
|
+
if (this.parents?.length !== 1) throw ERROR(`[Thread] insertRaw() called on thread with multiple parents:`, this.nameAndSizeUntracked)
|
|
60
|
+
return this.parents[0].insertRaw(appLogsToInsert)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
subscribe(callback: (event: ThreadEvent) => void, type?: 'derived' | 'reaction') {
|
|
64
|
+
const list = type === 'derived' ? this._derivedSubscribers : this._subscribers
|
|
65
|
+
list.push(callback)
|
|
66
|
+
return () => {
|
|
67
|
+
const idx = list.indexOf(callback)
|
|
68
|
+
if (idx >= 0) list.splice(idx, 1)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected notifySubscribers(event: ThreadEvent) {
|
|
73
|
+
this._version++
|
|
74
|
+
DEBUG(`[thread: ${this.name}] notifying`, this._derivedSubscribers.length, 'derived +', this._subscribers.length, 'subscribers of', { ...event, subs: this._subscribers })
|
|
75
|
+
const derived = [...this._derivedSubscribers] // snapshot — safe if a subscriber unsubs during iteration
|
|
76
|
+
for (const subscriber of derived) {
|
|
77
|
+
subscriber(event)
|
|
78
|
+
}
|
|
79
|
+
const subs = [...this._subscribers]
|
|
80
|
+
for (const subscriber of subs) {
|
|
81
|
+
subscriber(event)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── SubscribableArray<Applog> ──
|
|
86
|
+
get items(): readonly Applog[] { return this._applogs }
|
|
87
|
+
|
|
88
|
+
dispose() {
|
|
89
|
+
this._derivedSubscribers.length = 0
|
|
90
|
+
this._subscribers.length = 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get applogs(): readonly Applog[] /* (i) only type hint, not actually immutable */ {
|
|
94
|
+
// VERBOSE.isDisabled || trace()
|
|
95
|
+
return this._applogs
|
|
96
|
+
}
|
|
97
|
+
get applogsCids(): readonly CidString[] {
|
|
98
|
+
return this._applogs.map(l => l.cid)
|
|
99
|
+
}
|
|
100
|
+
get applogsCidSet(): ReadonlySet<CidString> {
|
|
101
|
+
return new Set(this._applogs.map(l => l.cid))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public map<R>(fn: (applog: Applog) => R) {
|
|
105
|
+
// if (!this.applogs.map) throw ERROR(`thread.applogs is not an array?!`, this.applogs)
|
|
106
|
+
return this.applogs.map(fn)
|
|
107
|
+
}
|
|
108
|
+
public get findLast() {
|
|
109
|
+
return this.applogs.findLast.bind(this.applogs)
|
|
110
|
+
}
|
|
111
|
+
public get findFirst() {
|
|
112
|
+
return this.applogs.find.bind(this.applogs)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get firstLog() {
|
|
116
|
+
return this.applogs[0]
|
|
117
|
+
}
|
|
118
|
+
get latestLog() {
|
|
119
|
+
return this.applogs[this.applogs.length - 1]
|
|
120
|
+
}
|
|
121
|
+
public hasApplog(applog: Applog, byRef: boolean) {
|
|
122
|
+
if (byRef) {
|
|
123
|
+
return this.applogs.includes(applog)
|
|
124
|
+
} else {
|
|
125
|
+
if (!applog.cid) throw ERROR(`[hasApplogs] applog without CID:`, applog) // trying to make this be always the case
|
|
126
|
+
return this.hasApplogCid(applog.cid)
|
|
127
|
+
// const keySet = Object.keys(applog) / / HACK: sanity check to catch bugs
|
|
128
|
+
// return !!this.applogs.find(log => {
|
|
129
|
+
// if (!arraysContainSameElements(keySet, Object.keys(log))) {
|
|
130
|
+
// /* throw */ ERROR(`[hasApplog] field set mismatch:`, { applog, log }) / / HACK: properly handle this
|
|
131
|
+
// return comparer.structural(pick(log, ['en', 'at', 'vl', 'ag', 'ts']), pick(applog, ['en', 'at', 'vl', 'ag', 'ts']))
|
|
132
|
+
// }
|
|
133
|
+
// return areApplogsEqual(log, applog)
|
|
134
|
+
// })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
public hasApplogCid(cid: CID | CidString) {
|
|
138
|
+
return this.applogsCidSet.has(cid.toString()) // O(1) via Set vs O(n) via includes
|
|
139
|
+
}
|
|
140
|
+
get applogsByCid() {
|
|
141
|
+
return new Map(this.applogs.map(log => [log.cid, log]))
|
|
142
|
+
}
|
|
143
|
+
public getApplog(cid: CID | CidString) {
|
|
144
|
+
return this.applogsByCid.get(cid.toString())
|
|
145
|
+
// .find(function findApplogInThread(log) {
|
|
146
|
+
// return areCidsEqual(log.cid, cid)
|
|
147
|
+
// })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public hasApplogWithDiffTs(applog: ApplogForInsert) {
|
|
151
|
+
// HACK this is basically as inefficient as it gets
|
|
152
|
+
return this.applogs.find(existing => (
|
|
153
|
+
existing.en === applog.en
|
|
154
|
+
&& existing.at === applog.at
|
|
155
|
+
&& existing.vl === applog.vl
|
|
156
|
+
&& existing.ag === applog.ag
|
|
157
|
+
))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// get stateHash() {
|
|
161
|
+
// blakeHasher.init()
|
|
162
|
+
// for (const log of this.applogs) {
|
|
163
|
+
// blakeHasher.update(log.cid)
|
|
164
|
+
// }
|
|
165
|
+
// return blakeHasher.digest()
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
get isEmpty() {
|
|
169
|
+
return this.size === 0
|
|
170
|
+
}
|
|
171
|
+
get size() {
|
|
172
|
+
return this.applogs.length
|
|
173
|
+
}
|
|
174
|
+
get length() {
|
|
175
|
+
return this.applogs.length
|
|
176
|
+
}
|
|
177
|
+
get untrackedSize() {
|
|
178
|
+
return this.size
|
|
179
|
+
}
|
|
180
|
+
get nameAndSizeUntracked() {
|
|
181
|
+
return `${this.name} (${this.size})`
|
|
182
|
+
}
|
|
183
|
+
get prettyName() {
|
|
184
|
+
return prettifyThreadName(this.name)
|
|
185
|
+
}
|
|
186
|
+
get hasParents() {
|
|
187
|
+
return !!this.parents?.length
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const getLogsFromThread = (logsOrThread: ApplogsOrThread) => logsOrThread instanceof Thread ? logsOrThread.applogs : logsOrThread
|
|
192
|
+
|
|
193
|
+
export class StaticThread extends Thread {
|
|
194
|
+
static fromArray(applogs: Applog[], name?: string) {
|
|
195
|
+
return new StaticThread(name || 'static', null, [], applogs)
|
|
196
|
+
}
|
|
197
|
+
constructor(
|
|
198
|
+
name: string, /* = null */
|
|
199
|
+
parents: ArrayOrSingle<Thread> | readonly Thread[] | null,
|
|
200
|
+
filters: readonly string[],
|
|
201
|
+
_applogs: Applog[],
|
|
202
|
+
) {
|
|
203
|
+
super(name, parents, filters, _applogs)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get readOnly() {
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import stringify from 'safe-stable-stringify'
|
|
3
|
+
import { finalizeApplogForInsert } from '../applog/applog-helpers.ts'
|
|
4
|
+
import { dateNowIso, matchPartStatic } from '../applog/applog-utils.ts'
|
|
5
|
+
import { Applog, ApplogForInsert, ApplogValue, CidString, DatalogQueryPattern, ValueOrMatcher } from '../applog/datom-types.ts'
|
|
6
|
+
import { createDebugName } from '../utils/debug-name.ts'
|
|
7
|
+
import { memoizedFn } from '../query/memoized.ts'
|
|
8
|
+
import { SubscribableImpl } from '../query/subscribable.ts'
|
|
9
|
+
import type { Subscribable } from '../query/subscribable.ts'
|
|
10
|
+
import { isInitEvent, Thread, ThreadEvent } from './basic.ts'
|
|
11
|
+
import { type DeltaContext, type DeltaEvent, MappedThread, type ThreadDerivation } from './mapped.ts'
|
|
12
|
+
|
|
13
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
14
|
+
|
|
15
|
+
export const rollingFilter = memoizedFn('rollingFilter', function rollingFilter(
|
|
16
|
+
thread: Thread,
|
|
17
|
+
pattern: DatalogQueryPattern,
|
|
18
|
+
opts: { name?: string; extraFilterName?: string } = {},
|
|
19
|
+
) {
|
|
20
|
+
const filter = makeFilter(pattern)
|
|
21
|
+
|
|
22
|
+
const derivation: ThreadDerivation = {
|
|
23
|
+
compute: (parents) => {
|
|
24
|
+
if (parents.length !== 1) {
|
|
25
|
+
throw ERROR(`rollingFilter requires exactly one parent`, { parents: parents.length })
|
|
26
|
+
}
|
|
27
|
+
return filter(parents[0].applogs)
|
|
28
|
+
},
|
|
29
|
+
mapDelta: (delta, { state }) => {
|
|
30
|
+
const mappedDelta: DeltaEvent = {
|
|
31
|
+
added: filter(delta.added),
|
|
32
|
+
removed: delta.removed?.filter(log => state.includes(log)),
|
|
33
|
+
}
|
|
34
|
+
VERBOSE(
|
|
35
|
+
`rollingFilter{${thread.nameAndSizeUntracked} | ${opts.name ? ` '${opts.name}'}` : ''} parentUpdate`,
|
|
36
|
+
pattern,
|
|
37
|
+
delta,
|
|
38
|
+
'=>',
|
|
39
|
+
mappedDelta,
|
|
40
|
+
)
|
|
41
|
+
return mappedDelta
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
return new MappedThread(
|
|
45
|
+
`${thread.name} | ${opts.name || `rollingFilter{${stringify(pattern)}}`}`,
|
|
46
|
+
thread,
|
|
47
|
+
[...thread.filters, ...(opts.extraFilterName ? [opts.extraFilterName] : [])],
|
|
48
|
+
derivation,
|
|
49
|
+
)
|
|
50
|
+
}, {
|
|
51
|
+
argsDebugName: (thread, pattern, opts) => createDebugName({ caller: 'rollingFilter', thread, pattern, args: opts }),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export const rollingMapper = memoizedFn('rollingMapper', function rollingMapper(
|
|
55
|
+
thread: Thread,
|
|
56
|
+
derivation: ThreadDerivation,
|
|
57
|
+
opts: { name?: string; extraFilterName?: string } = {},
|
|
58
|
+
) {
|
|
59
|
+
return new MappedThread(
|
|
60
|
+
`${thread.name} | ${opts.name || `rollingMapper`}`,
|
|
61
|
+
thread,
|
|
62
|
+
[...thread.filters, ...(opts.extraFilterName ? [opts.extraFilterName] as const : [] as const)],
|
|
63
|
+
derivation,
|
|
64
|
+
)
|
|
65
|
+
}, {
|
|
66
|
+
argsDebugName: (thread, _derivation, opts) => createDebugName({ caller: 'rollingMapper', thread, args: opts }),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export const rollingAcc = memoizedFn(
|
|
70
|
+
'rollingAcc',
|
|
71
|
+
function rollingAcc<ACC>(
|
|
72
|
+
thread: Thread,
|
|
73
|
+
acc: ACC,
|
|
74
|
+
eventMapper: (event: ThreadEvent, acc: ACC) => void,
|
|
75
|
+
opts: { name?: string } = {},
|
|
76
|
+
): Subscribable<ACC> {
|
|
77
|
+
eventMapper({ init: thread.applogs }, acc) // Do initial mapping
|
|
78
|
+
|
|
79
|
+
const result = new SubscribableImpl<ACC>(acc, () =>
|
|
80
|
+
thread.subscribe(event => {
|
|
81
|
+
eventMapper(event, acc)
|
|
82
|
+
result._set(acc) // notify — same reference but contents mutated
|
|
83
|
+
}, 'derived'),
|
|
84
|
+
{ equals: false }, // accumulator is mutated in-place
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
},
|
|
89
|
+
{ argsDebugName: (thread, _acc, _mapper, opts) => `rollingAcc{${thread.nameAndSizeUntracked}${opts?.name ? ` | ${opts?.name}` : ''}}` },
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
export const getUntrackedPattern = function getUntrackedPattern(
|
|
93
|
+
pattern: DatalogQueryPattern,
|
|
94
|
+
) {
|
|
95
|
+
if (!Object.entries(pattern).length) {
|
|
96
|
+
throw new Error(`Pattern is empty`)
|
|
97
|
+
}
|
|
98
|
+
return pattern
|
|
99
|
+
}
|
|
100
|
+
export function makeFilter(
|
|
101
|
+
pattern: DatalogQueryPattern,
|
|
102
|
+
) {
|
|
103
|
+
return function madeFilter(logs: readonly Applog[]) {
|
|
104
|
+
return logs.filter(function madeFilterSingleLog(applog) {
|
|
105
|
+
for (const field of Object.keys(pattern)) {
|
|
106
|
+
let patternValue = pattern[field] // (i) not using .entries bc. https://gists.cwidanage.com/2018/06/how-to-iterate-over-object-entries-in.html
|
|
107
|
+
if (patternValue === undefined) continue // undefined in pattern means "no constraint/query on this field"
|
|
108
|
+
const applogValue = applog[field.startsWith('!') ? field.slice(1) : field]
|
|
109
|
+
const patternValT: ValueOrMatcher<ApplogValue> = patternValue
|
|
110
|
+
if (!matchPartStatic(field as keyof Applog, patternValT, applogValue)) {
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* // ! think twice before using
|
|
121
|
+
*/
|
|
122
|
+
export const getUntrackedFilterResults = function getUntrackedFilterResults(
|
|
123
|
+
thread: Thread,
|
|
124
|
+
pattern: DatalogQueryPattern,
|
|
125
|
+
opts: { name?: string } = {},
|
|
126
|
+
) {
|
|
127
|
+
const untrackedPattern = getUntrackedPattern(pattern)
|
|
128
|
+
const filter = makeFilter(untrackedPattern)
|
|
129
|
+
return filter(thread.applogs)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
///////////////////////////
|
|
133
|
+
// FILTERED THREAD TYPES //
|
|
134
|
+
///////////////////////////
|
|
135
|
+
export type ThreadWithFilter<T extends string> = Thread & {
|
|
136
|
+
filters: readonly T[]
|
|
137
|
+
}
|
|
138
|
+
export function hasFilter<T extends string>(thread: Thread, filter: T): thread is ThreadWithFilter<T> {
|
|
139
|
+
return thread.filters.includes(filter)
|
|
140
|
+
}
|
|
141
|
+
export function assertRaw(thread: Thread) /* : thread is ThreadWithoutFilters */ {
|
|
142
|
+
if (thread.filters.length) {
|
|
143
|
+
WARN(`[assertRaw] but '${thread.nameAndSizeUntracked}' has filters:`, thread.filters)
|
|
144
|
+
}
|
|
145
|
+
return thread as ThreadWithoutFilters
|
|
146
|
+
}
|
|
147
|
+
export function assertOnlyCurrent(thread: Thread) /* : thread is ThreadOnlyCurrent */ {
|
|
148
|
+
if (
|
|
149
|
+
!hasFilter(thread, 'lastWriteWins') ||
|
|
150
|
+
!hasFilter(thread, 'withoutDeleted')
|
|
151
|
+
) throw ERROR(`should be filtered thread, but is:`, thread.filters)
|
|
152
|
+
return thread
|
|
153
|
+
}
|
|
154
|
+
export type ThreadOnlyCurrent = ThreadWithFilter<'lastWriteWins'>
|
|
155
|
+
export type ThreadNoDeleted = ThreadWithFilter<'withoutDeleted'>
|
|
156
|
+
export type ThreadOnlyCurrentNoDeleted = ThreadWithFilter<'lastWriteWins' | 'withoutDeleted'>
|
|
157
|
+
export type ThreadWithoutFilters = Thread & { filters: [] }
|
|
158
|
+
|
|
159
|
+
/** Re-export for convenience */
|
|
160
|
+
export const asReadOnly = MappedThread.asReadOnly
|
|
161
|
+
|
|
162
|
+
//////////////////
|
|
163
|
+
// MISC HELPERS //
|
|
164
|
+
//////////////////
|
|
165
|
+
|
|
166
|
+
export type ApplogMapper = (log: Applog, sourceThread: Thread) => ApplogForInsert
|
|
167
|
+
export const simpleApplogMapper = function simpleApplogMapper(
|
|
168
|
+
thread: Thread,
|
|
169
|
+
logMapper: ApplogMapper,
|
|
170
|
+
opts: { name?: string; extraFilterName?: string } = {},
|
|
171
|
+
) {
|
|
172
|
+
const mappedTo = new Map<CidString, Applog>() // source log CID to result log
|
|
173
|
+
const mapLogs = (applogs: readonly Applog[], thread: Thread) => {
|
|
174
|
+
const ts = dateNowIso()
|
|
175
|
+
return applogs.map(log => {
|
|
176
|
+
const mapped = logMapper(log, thread)
|
|
177
|
+
let mapTo: Applog
|
|
178
|
+
if (mapped === log) {
|
|
179
|
+
mapTo = log
|
|
180
|
+
} else {
|
|
181
|
+
if ((mapped as Applog).cid === log.cid) {
|
|
182
|
+
delete (mapped as Applog).cid // for convenience of not needing to remove it in the mapper
|
|
183
|
+
}
|
|
184
|
+
mapTo = finalizeApplogForInsert(mapped, {
|
|
185
|
+
ts,
|
|
186
|
+
threadForPv: null, // ? should not be inferred, right?
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
mappedTo.set(log.cid, mapTo)
|
|
190
|
+
return mapTo
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const derivation: ThreadDerivation = {
|
|
195
|
+
compute(parents) {
|
|
196
|
+
if (parents.length !== 1) {
|
|
197
|
+
throw ERROR(`simpleApplogMapper requires exactly one parent`, { parents: parents.length })
|
|
198
|
+
}
|
|
199
|
+
const [parent] = parents
|
|
200
|
+
mappedTo.clear()
|
|
201
|
+
return mapLogs(parent.applogs, parent)
|
|
202
|
+
},
|
|
203
|
+
mapDelta: (delta, { state }) => {
|
|
204
|
+
const mappedDelta: DeltaEvent = {
|
|
205
|
+
added: mapLogs(delta.added, thread),
|
|
206
|
+
removed: delta.removed?.map(removedSourceLog => {
|
|
207
|
+
const mappedLog = mappedTo.get(removedSourceLog.cid)
|
|
208
|
+
if (!mappedLog) {
|
|
209
|
+
throw ERROR(`[simpleApplogMapper] Parent remove event for Applog that we don't know about`, { removedSourceLog })
|
|
210
|
+
}
|
|
211
|
+
mappedTo.delete(removedSourceLog.cid)
|
|
212
|
+
return mappedLog
|
|
213
|
+
}).filter(log => state.includes(log)),
|
|
214
|
+
}
|
|
215
|
+
VERBOSE(
|
|
216
|
+
`simpleApplogMapper{${thread.nameAndSizeUntracked} | ${opts?.name ? ` '${opts?.name}'}` : ''} parentUpdate`,
|
|
217
|
+
delta,
|
|
218
|
+
'=>',
|
|
219
|
+
mappedDelta,
|
|
220
|
+
)
|
|
221
|
+
return mappedDelta
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
const mappedThread = rollingMapper(thread, derivation, opts)
|
|
225
|
+
VERBOSE.isDisabled || VERBOSE(`simpleApplogMapper<${thread.nameAndSizeUntracked}> initial mapped to`, mappedThread.applogs)
|
|
226
|
+
return mappedThread
|
|
227
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Logger } from 'besonders-logger'
|
|
2
|
+
import { Applog, ApplogValue, EntityID } from '../applog/datom-types.ts'
|
|
3
|
+
import { memoizedFn } from '../query/memoized.ts'
|
|
4
|
+
import { SubscribableImpl, type Subscribable } from '../query/subscribable.ts'
|
|
5
|
+
import { isInitEvent, Thread } from './basic.ts'
|
|
6
|
+
import { rollingFilter } from './filters.ts'
|
|
7
|
+
|
|
8
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Live index of applogs grouped by entity ID. Memoized per thread — one subscription
|
|
12
|
+
* per thread regardless of how many consumers call this. Updated incrementally on
|
|
13
|
+
* each parent event (O(event-size)). Lookup is O(1) via `map.get(en)`.
|
|
14
|
+
*
|
|
15
|
+
* Subscriber order: registers a 'derived' subscription on `thread` at first call. By
|
|
16
|
+
* the FIFO subscriber-order convention (`thread/basic.ts:notifySubscribers`),
|
|
17
|
+
* consumers that subscribe to `thread` AFTER calling `applogsByEntity(thread)` will
|
|
18
|
+
* see an up-to-date index when their handler runs.
|
|
19
|
+
*/
|
|
20
|
+
export const applogsByEntity = memoizedFn(
|
|
21
|
+
'applogsByEntity',
|
|
22
|
+
function applogsByEntity(thread: Thread): ReadonlyMap<EntityID, readonly Applog[]> {
|
|
23
|
+
const map = new Map<EntityID, Applog[]>()
|
|
24
|
+
|
|
25
|
+
const add = (log: Applog) => {
|
|
26
|
+
let arr = map.get(log.en)
|
|
27
|
+
if (!arr) {
|
|
28
|
+
arr = []
|
|
29
|
+
map.set(log.en, arr)
|
|
30
|
+
}
|
|
31
|
+
arr.push(log)
|
|
32
|
+
}
|
|
33
|
+
const remove = (log: Applog) => {
|
|
34
|
+
const arr = map.get(log.en)
|
|
35
|
+
if (!arr) return
|
|
36
|
+
const idx = arr.indexOf(log)
|
|
37
|
+
if (idx >= 0) arr.splice(idx, 1)
|
|
38
|
+
if (arr.length === 0) map.delete(log.en)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const log of thread.applogs) add(log)
|
|
42
|
+
|
|
43
|
+
thread.subscribe(event => {
|
|
44
|
+
if (isInitEvent(event)) {
|
|
45
|
+
map.clear()
|
|
46
|
+
for (const log of event.init) add(log)
|
|
47
|
+
} else {
|
|
48
|
+
for (const log of event.added) add(log)
|
|
49
|
+
if (event.removed) for (const log of event.removed) remove(log)
|
|
50
|
+
}
|
|
51
|
+
}, 'derived')
|
|
52
|
+
|
|
53
|
+
return map
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Live index of applogs grouped by value for a given attribute. Value-based counterpart
|
|
59
|
+
* to `applogsByEntity` (which groups by entity ID).
|
|
60
|
+
*
|
|
61
|
+
* Uses `rollingFilter` internally for the attribute filter (memoized, shared with any
|
|
62
|
+
* other code filtering by the same attribute).
|
|
63
|
+
*
|
|
64
|
+
* Returns a `Subscribable` with lazy upstream activation:
|
|
65
|
+
* - Snapshot use: read `.value` — zero subscription overhead
|
|
66
|
+
* - Reactive use: `.subscribe()` activates the chain for incremental updates
|
|
67
|
+
*/
|
|
68
|
+
export const applogsByAttrValue = memoizedFn(
|
|
69
|
+
'applogsByAttrValue',
|
|
70
|
+
function applogsByAttrValue(
|
|
71
|
+
thread: Thread,
|
|
72
|
+
attr: string,
|
|
73
|
+
): Subscribable<ReadonlyMap<ApplogValue, readonly Applog[]>> {
|
|
74
|
+
const map = new Map<ApplogValue, Applog[]>()
|
|
75
|
+
|
|
76
|
+
const add = (log: Applog) => {
|
|
77
|
+
let arr = map.get(log.vl)
|
|
78
|
+
if (!arr) {
|
|
79
|
+
arr = []
|
|
80
|
+
map.set(log.vl, arr)
|
|
81
|
+
}
|
|
82
|
+
arr.push(log)
|
|
83
|
+
}
|
|
84
|
+
const remove = (log: Applog) => {
|
|
85
|
+
const arr = map.get(log.vl)
|
|
86
|
+
if (!arr) return
|
|
87
|
+
const idx = arr.indexOf(log)
|
|
88
|
+
if (idx >= 0) arr.splice(idx, 1)
|
|
89
|
+
if (arr.length === 0) map.delete(log.vl)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const filtered = rollingFilter(thread, { at: attr })
|
|
93
|
+
|
|
94
|
+
// Initial build from current filtered state
|
|
95
|
+
for (const log of filtered.applogs) add(log)
|
|
96
|
+
|
|
97
|
+
const result = new SubscribableImpl<ReadonlyMap<ApplogValue, readonly Applog[]>>(
|
|
98
|
+
map,
|
|
99
|
+
() => filtered.subscribe(event => {
|
|
100
|
+
if (isInitEvent(event)) {
|
|
101
|
+
map.clear()
|
|
102
|
+
for (const log of event.init) add(log)
|
|
103
|
+
} else {
|
|
104
|
+
if (event.removed) for (const log of event.removed) remove(log)
|
|
105
|
+
for (const log of event.added) add(log)
|
|
106
|
+
}
|
|
107
|
+
result._set(map)
|
|
108
|
+
}, 'derived'),
|
|
109
|
+
{ equals: false },
|
|
110
|
+
)
|
|
111
|
+
return result
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// ── entityLinkIndex ─────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export interface LinkEntry {
|
|
118
|
+
/** The link entity's own ID */
|
|
119
|
+
linkId: EntityID
|
|
120
|
+
/** Value of attrA on this link entity */
|
|
121
|
+
aValue: EntityID
|
|
122
|
+
/** Value of attrB on this link entity */
|
|
123
|
+
bValue: EntityID
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface EntityLinkIndexValue {
|
|
127
|
+
/** Lookup by attrA value → link entries (each has linkId + bValue) */
|
|
128
|
+
byA: ReadonlyMap<EntityID, readonly LinkEntry[]>
|
|
129
|
+
/** Lookup by attrB value → link entries (each has linkId + aValue) */
|
|
130
|
+
byB: ReadonlyMap<EntityID, readonly LinkEntry[]>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Live bidirectional index for entity-link relations: a common wovin pattern where
|
|
135
|
+
* a **link entity** connects two targets via two attributes.
|
|
136
|
+
*
|
|
137
|
+
* Example: note3 relations use a relation entity with `relation/block` (→ child block)
|
|
138
|
+
* and `relation/childOf` (→ parent block).
|
|
139
|
+
*
|
|
140
|
+
* Returns a `Subscribable` with lazy upstream activation:
|
|
141
|
+
* - Snapshot use: read `.value` — zero subscription overhead
|
|
142
|
+
* - Reactive use: `.subscribe()` activates the chain for incremental updates
|
|
143
|
+
*
|
|
144
|
+
* Link entity IDs are included in entries so consumers can look up full link state
|
|
145
|
+
* via `applogsByEntity`.
|
|
146
|
+
*/
|
|
147
|
+
export const entityLinkIndex = memoizedFn(
|
|
148
|
+
'entityLinkIndex',
|
|
149
|
+
function entityLinkIndex(
|
|
150
|
+
thread: Thread,
|
|
151
|
+
attrA: string,
|
|
152
|
+
attrB: string,
|
|
153
|
+
): Subscribable<EntityLinkIndexValue> {
|
|
154
|
+
const byA = new Map<EntityID, LinkEntry[]>()
|
|
155
|
+
const byB = new Map<EntityID, LinkEntry[]>()
|
|
156
|
+
|
|
157
|
+
// Internal state: track partial link entities (only one attr seen so far)
|
|
158
|
+
const aByLink = new Map<EntityID, EntityID>() // linkId → aValue
|
|
159
|
+
const bByLink = new Map<EntityID, EntityID>() // linkId → bValue
|
|
160
|
+
|
|
161
|
+
function addLink(linkId: EntityID, aValue: EntityID, bValue: EntityID) {
|
|
162
|
+
const entry: LinkEntry = { linkId, aValue, bValue }
|
|
163
|
+
let arrA = byA.get(aValue)
|
|
164
|
+
if (!arrA) { arrA = []; byA.set(aValue, arrA) }
|
|
165
|
+
arrA.push(entry)
|
|
166
|
+
let arrB = byB.get(bValue)
|
|
167
|
+
if (!arrB) { arrB = []; byB.set(bValue, arrB) }
|
|
168
|
+
arrB.push(entry)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function removeLink(linkId: EntityID, aValue: EntityID, bValue: EntityID) {
|
|
172
|
+
const arrA = byA.get(aValue)
|
|
173
|
+
if (arrA) {
|
|
174
|
+
const idx = arrA.findIndex(e => e.linkId === linkId)
|
|
175
|
+
if (idx >= 0) arrA.splice(idx, 1)
|
|
176
|
+
if (arrA.length === 0) byA.delete(aValue)
|
|
177
|
+
}
|
|
178
|
+
const arrB = byB.get(bValue)
|
|
179
|
+
if (arrB) {
|
|
180
|
+
const idx = arrB.findIndex(e => e.linkId === linkId)
|
|
181
|
+
if (idx >= 0) arrB.splice(idx, 1)
|
|
182
|
+
if (arrB.length === 0) byB.delete(bValue)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function processLog(log: Applog) {
|
|
187
|
+
if (log.at === attrA) {
|
|
188
|
+
aByLink.set(log.en, log.vl as EntityID)
|
|
189
|
+
const bVal = bByLink.get(log.en)
|
|
190
|
+
if (bVal !== undefined) {
|
|
191
|
+
addLink(log.en, log.vl as EntityID, bVal)
|
|
192
|
+
}
|
|
193
|
+
} else if (log.at === attrB) {
|
|
194
|
+
bByLink.set(log.en, log.vl as EntityID)
|
|
195
|
+
const aVal = aByLink.get(log.en)
|
|
196
|
+
if (aVal !== undefined) {
|
|
197
|
+
addLink(log.en, aVal, log.vl as EntityID)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function removeLog(log: Applog) {
|
|
203
|
+
if (log.at === attrA) {
|
|
204
|
+
const aVal = aByLink.get(log.en)
|
|
205
|
+
const bVal = bByLink.get(log.en)
|
|
206
|
+
if (aVal !== undefined && bVal !== undefined) {
|
|
207
|
+
removeLink(log.en, aVal, bVal)
|
|
208
|
+
}
|
|
209
|
+
aByLink.delete(log.en)
|
|
210
|
+
} else if (log.at === attrB) {
|
|
211
|
+
const aVal = aByLink.get(log.en)
|
|
212
|
+
const bVal = bByLink.get(log.en)
|
|
213
|
+
if (aVal !== undefined && bVal !== undefined) {
|
|
214
|
+
removeLink(log.en, aVal, bVal)
|
|
215
|
+
}
|
|
216
|
+
bByLink.delete(log.en)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildFull(applogs: readonly Applog[]) {
|
|
221
|
+
byA.clear()
|
|
222
|
+
byB.clear()
|
|
223
|
+
aByLink.clear()
|
|
224
|
+
bByLink.clear()
|
|
225
|
+
for (const log of applogs) processLog(log)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const filtered = rollingFilter(thread, { at: [attrA, attrB] })
|
|
229
|
+
|
|
230
|
+
// Initial build
|
|
231
|
+
buildFull(filtered.applogs)
|
|
232
|
+
|
|
233
|
+
const value: EntityLinkIndexValue = { byA, byB }
|
|
234
|
+
const result = new SubscribableImpl<EntityLinkIndexValue>(
|
|
235
|
+
value,
|
|
236
|
+
() => filtered.subscribe(event => {
|
|
237
|
+
if (isInitEvent(event)) {
|
|
238
|
+
buildFull(event.init)
|
|
239
|
+
} else {
|
|
240
|
+
// Process removes before adds (LWW updates appear as remove+add)
|
|
241
|
+
if (event.removed) for (const log of event.removed) removeLog(log)
|
|
242
|
+
for (const log of event.added) processLog(log)
|
|
243
|
+
}
|
|
244
|
+
result._set(value)
|
|
245
|
+
}, 'derived'),
|
|
246
|
+
{ equals: false },
|
|
247
|
+
)
|
|
248
|
+
return result
|
|
249
|
+
},
|
|
250
|
+
)
|