@wovin/core 0.0.0-ciao-mobx-955482e8
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/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/applog/applog-helpers.d.ts +47 -0
- package/dist/applog/applog-helpers.d.ts.map +1 -0
- package/dist/applog/applog-utils.d.ts +57 -0
- package/dist/applog/applog-utils.d.ts.map +1 -0
- package/dist/applog/datom-types.d.ts +128 -0
- package/dist/applog/datom-types.d.ts.map +1 -0
- package/dist/applog.d.ts +4 -0
- package/dist/applog.d.ts.map +1 -0
- package/dist/applog.js +101 -0
- package/dist/applog.js.map +1 -0
- package/dist/blockstore/index.d.ts +21 -0
- package/dist/blockstore/index.d.ts.map +1 -0
- package/dist/blockstore.d.ts +2 -0
- package/dist/blockstore.d.ts.map +1 -0
- package/dist/blockstore.js +24 -0
- package/dist/blockstore.js.map +1 -0
- package/dist/chunk-6MQKRL6W.js +86 -0
- package/dist/chunk-6MQKRL6W.js.map +1 -0
- package/dist/chunk-7MW34UEO.js +40 -0
- package/dist/chunk-7MW34UEO.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-7Z5YDQKK.js.map +1 -0
- package/dist/chunk-CY4NLISM.js +144 -0
- package/dist/chunk-CY4NLISM.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/chunk-E46VTKTZ.js.map +1 -0
- package/dist/chunk-O43W7UW6.js +434 -0
- package/dist/chunk-O43W7UW6.js.map +1 -0
- package/dist/chunk-XIQSYEV3.js +1604 -0
- package/dist/chunk-XIQSYEV3.js.map +1 -0
- package/dist/chunk-XVGW4QC3.js +55 -0
- package/dist/chunk-XVGW4QC3.js.map +1 -0
- package/dist/chunk-YDAKBU6Q.js +9 -0
- 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/chunk-ZXCJRYD7.js +883 -0
- package/dist/chunk-ZXCJRYD7.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/ipfs/car.d.ts +59 -0
- package/dist/ipfs/car.d.ts.map +1 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
- package/dist/ipfs/ipfs-utils.d.ts +35 -0
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
- package/dist/ipfs.d.ts +4 -0
- package/dist/ipfs.d.ts.map +1 -0
- package/dist/ipfs.js +60 -0
- package/dist/ipfs.js.map +1 -0
- package/dist/ipns/ipns-record.d.ts +34 -0
- package/dist/ipns/ipns-record.d.ts.map +1 -0
- package/dist/ipns.d.ts +2 -0
- package/dist/ipns.d.ts.map +1 -0
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/connector.d.ts +9 -0
- package/dist/pubsub/connector.d.ts.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +14 -0
- package/dist/pubsub/pub-pull.d.ts.map +1 -0
- package/dist/pubsub/pubsub-types.d.ts +72 -0
- package/dist/pubsub/pubsub-types.d.ts.map +1 -0
- package/dist/pubsub/snap-push.d.ts +41 -0
- package/dist/pubsub/snap-push.d.ts.map +1 -0
- package/dist/pubsub/ucan-example.d.ts +3 -0
- package/dist/pubsub/ucan-example.d.ts.map +1 -0
- package/dist/pubsub/ucan.d.ts +16 -0
- package/dist/pubsub/ucan.d.ts.map +1 -0
- package/dist/pubsub.d.ts +5 -0
- package/dist/pubsub.d.ts.map +1 -0
- package/dist/pubsub.js +31 -0
- package/dist/pubsub.js.map +1 -0
- package/dist/query/basic.d.ts +105 -0
- package/dist/query/basic.d.ts.map +1 -0
- package/dist/query/divergences.d.ts +12 -0
- package/dist/query/divergences.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +4 -0
- package/dist/query/matchers.d.ts.map +1 -0
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/query-steps.d.ts +4 -0
- package/dist/query/query-steps.d.ts.map +1 -0
- package/dist/query/situations.d.ts +80 -0
- package/dist/query/situations.d.ts.map +1 -0
- package/dist/query/subscribable.d.ts +102 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +70 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +108 -0
- package/dist/query.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -0
- package/dist/retrieve/update-thread.d.ts +64 -0
- package/dist/retrieve/update-thread.d.ts.map +1 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +14 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/thread/basic.d.ts +60 -0
- package/dist/thread/basic.d.ts.map +1 -0
- package/dist/thread/filters.d.ts +47 -0
- package/dist/thread/filters.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +31 -0
- package/dist/thread/mapped.d.ts.map +1 -0
- package/dist/thread/utils.d.ts +23 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/writeable.d.ts +41 -0
- package/dist/thread/writeable.d.ts.map +1 -0
- package/dist/thread.d.ts +6 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +54 -0
- package/dist/thread.js.map +1 -0
- package/dist/types/typescript-utils.d.ts +34 -0
- package/dist/types/typescript-utils.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/package.json +110 -0
- package/src/applog/applog-helpers.ts +150 -0
- package/src/applog/applog-utils.ts +398 -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/basic.ts +1061 -0
- package/src/query/divergences.ts +50 -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 +536 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +225 -0
- package/src/query/types.ts +155 -0
- package/src/query.ts +7 -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 +175 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +234 -0
- package/src/thread/mapped.ts +166 -0
- package/src/thread/utils.ts +146 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +5 -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
|
@@ -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,234 @@
|
|
|
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 { ApplogEventMapper, MappedThread } 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 initialLogs = filter(thread.applogs)
|
|
23
|
+
|
|
24
|
+
const handleUpdateEvent: ApplogEventMapper = (event) => {
|
|
25
|
+
let mappedEvent: ThreadEvent
|
|
26
|
+
if (isInitEvent(event)) {
|
|
27
|
+
mappedEvent = { init: filter(event.init) }
|
|
28
|
+
} else {
|
|
29
|
+
mappedEvent = {
|
|
30
|
+
added: filter(event.added),
|
|
31
|
+
removed: event.removed, // whatever's removed shall be removed
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
VERBOSE(
|
|
35
|
+
`rollingFilter{${thread.nameAndSizeUntracked} | ${opts.name ? ` '${opts.name}'}` : ''} parentUpdate`,
|
|
36
|
+
pattern,
|
|
37
|
+
event,
|
|
38
|
+
'=>',
|
|
39
|
+
mappedEvent,
|
|
40
|
+
)
|
|
41
|
+
return mappedEvent
|
|
42
|
+
}
|
|
43
|
+
const mappedThread = new MappedThread(
|
|
44
|
+
`${thread.name} | ${opts.name || `rollingFilter{${stringify(pattern)}}`}`,
|
|
45
|
+
thread,
|
|
46
|
+
[...thread.filters, ...(opts.extraFilterName ? [opts.extraFilterName] : [])],
|
|
47
|
+
initialLogs,
|
|
48
|
+
handleUpdateEvent,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return mappedThread
|
|
52
|
+
}, {
|
|
53
|
+
argsDebugName: (thread, pattern, opts) => createDebugName({ caller: 'rollingFilter', thread, pattern, args: opts }),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const rollingMapper = memoizedFn('rollingMapper', function rollingMapper(
|
|
57
|
+
thread: Thread,
|
|
58
|
+
eventMapper: ApplogEventMapper,
|
|
59
|
+
opts: { name?: string; extraFilterName?: string } = {},
|
|
60
|
+
) {
|
|
61
|
+
const initialMapResult = eventMapper.call(null, { init: thread.applogs }, thread)
|
|
62
|
+
if (!isInitEvent(initialMapResult)) {
|
|
63
|
+
throw ERROR('Initial run must return init event')
|
|
64
|
+
}
|
|
65
|
+
const initialLogs = initialMapResult.init
|
|
66
|
+
|
|
67
|
+
return new MappedThread(
|
|
68
|
+
`${thread.name} | ${opts.name || `rollingMapper`}`,
|
|
69
|
+
thread,
|
|
70
|
+
[...thread.filters, ...(opts.extraFilterName ? [opts.extraFilterName] as const : [] as const)],
|
|
71
|
+
// @ts-expect-error readonly.... FIXME
|
|
72
|
+
initialLogs,
|
|
73
|
+
eventMapper,
|
|
74
|
+
)
|
|
75
|
+
}, {
|
|
76
|
+
argsDebugName: (thread, _mapper, opts) => createDebugName({ caller: 'rollingMapper', thread, args: opts }),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export const rollingAcc = memoizedFn(
|
|
80
|
+
'rollingAcc',
|
|
81
|
+
function rollingAcc<ACC>(
|
|
82
|
+
thread: Thread,
|
|
83
|
+
acc: ACC,
|
|
84
|
+
eventMapper: (event: ThreadEvent, acc: ACC) => void,
|
|
85
|
+
opts: { name?: string } = {},
|
|
86
|
+
): Subscribable<ACC> {
|
|
87
|
+
eventMapper({ init: thread.applogs }, acc) // Do initial mapping
|
|
88
|
+
|
|
89
|
+
const result = new SubscribableImpl<ACC>(acc, () =>
|
|
90
|
+
thread.subscribe(event => {
|
|
91
|
+
eventMapper(event, acc)
|
|
92
|
+
result._set(acc) // notify — same reference but contents mutated
|
|
93
|
+
}, 'derived'),
|
|
94
|
+
{ equals: false }, // accumulator is mutated in-place
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
},
|
|
99
|
+
{ argsDebugName: (thread, _acc, _mapper, opts) => `rollingAcc{${thread.nameAndSizeUntracked}${opts?.name ? ` | ${opts?.name}` : ''}}` },
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
export const getUntrackedPattern = function getUntrackedPattern(
|
|
103
|
+
pattern: DatalogQueryPattern,
|
|
104
|
+
) {
|
|
105
|
+
if (!Object.entries(pattern).length) {
|
|
106
|
+
throw new Error(`Pattern is empty`)
|
|
107
|
+
}
|
|
108
|
+
return pattern
|
|
109
|
+
}
|
|
110
|
+
export function makeFilter(
|
|
111
|
+
pattern: DatalogQueryPattern,
|
|
112
|
+
) {
|
|
113
|
+
return function madeFilter(logs: readonly Applog[]) {
|
|
114
|
+
return logs.filter(function madeFilterSingleLog(applog) {
|
|
115
|
+
for (const field of Object.keys(pattern)) {
|
|
116
|
+
let patternValue = pattern[field] // (i) not using .entries bc. https://gists.cwidanage.com/2018/06/how-to-iterate-over-object-entries-in.html
|
|
117
|
+
if (patternValue === undefined) continue // undefined in pattern means "no constraint/query on this field"
|
|
118
|
+
const applogValue = applog[field.startsWith('!') ? field.slice(1) : field]
|
|
119
|
+
const patternValT: ValueOrMatcher<ApplogValue> = patternValue
|
|
120
|
+
if (!matchPartStatic(field as keyof Applog, patternValT, applogValue)) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* // ! think twice before using
|
|
131
|
+
*/
|
|
132
|
+
export const getUntrackedFilterResults = function getUntrackedFilterResults(
|
|
133
|
+
thread: Thread,
|
|
134
|
+
pattern: DatalogQueryPattern,
|
|
135
|
+
opts: { name?: string } = {},
|
|
136
|
+
) {
|
|
137
|
+
const untrackedPattern = getUntrackedPattern(pattern)
|
|
138
|
+
const filter = makeFilter(untrackedPattern)
|
|
139
|
+
return filter(thread.applogs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
///////////////////////////
|
|
143
|
+
// FILTERED THREAD TYPES //
|
|
144
|
+
///////////////////////////
|
|
145
|
+
export type ThreadWithFilter<T extends string> = Thread & {
|
|
146
|
+
filters: readonly T[]
|
|
147
|
+
}
|
|
148
|
+
export function hasFilter<T extends string>(thread: Thread, filter: T): thread is ThreadWithFilter<T> {
|
|
149
|
+
return thread.filters.includes(filter)
|
|
150
|
+
}
|
|
151
|
+
export function assertRaw(thread: Thread) /* : thread is ThreadWithoutFilters */ {
|
|
152
|
+
if (thread.filters.length) {
|
|
153
|
+
WARN(`[assertRaw] but '${thread.nameAndSizeUntracked}' has filters:`, thread.filters)
|
|
154
|
+
}
|
|
155
|
+
return thread as ThreadWithoutFilters
|
|
156
|
+
}
|
|
157
|
+
export function assertOnlyCurrent(thread: Thread) /* : thread is ThreadOnlyCurrent */ {
|
|
158
|
+
if (
|
|
159
|
+
!hasFilter(thread, 'lastWriteWins') ||
|
|
160
|
+
!hasFilter(thread, 'withoutDeleted')
|
|
161
|
+
) throw ERROR(`should be filtered thread, but is:`, thread.filters)
|
|
162
|
+
return thread
|
|
163
|
+
}
|
|
164
|
+
export type ThreadOnlyCurrent = ThreadWithFilter<'lastWriteWins'>
|
|
165
|
+
export type ThreadNoDeleted = ThreadWithFilter<'withoutDeleted'>
|
|
166
|
+
export type ThreadOnlyCurrentNoDeleted = ThreadWithFilter<'lastWriteWins' | 'withoutDeleted'>
|
|
167
|
+
export type ThreadWithoutFilters = Thread & { filters: [] }
|
|
168
|
+
|
|
169
|
+
/** Re-export for convenience */
|
|
170
|
+
export const asReadOnly = MappedThread.asReadOnly
|
|
171
|
+
|
|
172
|
+
//////////////////
|
|
173
|
+
// MISC HELPERS //
|
|
174
|
+
//////////////////
|
|
175
|
+
|
|
176
|
+
export type ApplogMapper = (log: Applog, sourceThread: Thread) => ApplogForInsert
|
|
177
|
+
export const simpleApplogMapper = function simpleApplogMapper(
|
|
178
|
+
thread: Thread,
|
|
179
|
+
logMapper: ApplogMapper,
|
|
180
|
+
opts: { name?: string; extraFilterName?: string } = {},
|
|
181
|
+
) {
|
|
182
|
+
const mappedTo = new Map<CidString, Applog>() // source log CID to result log
|
|
183
|
+
const mapLogs = (applogs: readonly Applog[], thread: Thread) => {
|
|
184
|
+
const ts = dateNowIso()
|
|
185
|
+
return applogs.map(log => {
|
|
186
|
+
const mapped = logMapper(log, thread)
|
|
187
|
+
let mapTo: Applog
|
|
188
|
+
if (mapped === log) {
|
|
189
|
+
mapTo = log
|
|
190
|
+
} else {
|
|
191
|
+
if ((mapped as Applog).cid === log.cid) {
|
|
192
|
+
delete (mapped as Applog).cid // for convenience of not needing to remove it in the mapper
|
|
193
|
+
}
|
|
194
|
+
mapTo = finalizeApplogForInsert(mapped, {
|
|
195
|
+
ts,
|
|
196
|
+
threadForPv: null, // ? should not be inferred, right?
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
mappedTo.set(log.cid, mapTo)
|
|
200
|
+
return mapTo
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const handleUpdateEvent: ApplogEventMapper = (event) => {
|
|
205
|
+
let mappedEvent: ThreadEvent
|
|
206
|
+
if (isInitEvent(event)) {
|
|
207
|
+
mappedEvent = { init: mapLogs(event.init, thread) }
|
|
208
|
+
} else {
|
|
209
|
+
mappedEvent = {
|
|
210
|
+
added: mapLogs(event.added, thread),
|
|
211
|
+
removed: event.removed.map(removedSourceLog => {
|
|
212
|
+
const mappedLog = mappedTo.get(removedSourceLog.cid)
|
|
213
|
+
if (!mappedLog) {
|
|
214
|
+
throw ERROR(`[simpleApplogMapper] Parent remove event for Applog that we don't know about`, { removedSourceLog })
|
|
215
|
+
}
|
|
216
|
+
mappedTo.delete(removedSourceLog.cid)
|
|
217
|
+
return mappedLog
|
|
218
|
+
}),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
VERBOSE(
|
|
222
|
+
`simpleApplogMapper{${thread.nameAndSizeUntracked} | ${opts?.name ? ` '${opts?.name}'}` : ''} parentUpdate`,
|
|
223
|
+
event,
|
|
224
|
+
'=>',
|
|
225
|
+
mappedEvent,
|
|
226
|
+
)
|
|
227
|
+
// console.trace('rollingFilter.addFiltered', getObserverTree(observableArr))
|
|
228
|
+
// trace()
|
|
229
|
+
return mappedEvent
|
|
230
|
+
}
|
|
231
|
+
const mappedThread = rollingMapper(thread, handleUpdateEvent, opts)
|
|
232
|
+
VERBOSE.isDisabled || VERBOSE(`simpleApplogMapper<${thread.nameAndSizeUntracked}> initial mapped to`, mappedThread.applogs)
|
|
233
|
+
return mappedThread
|
|
234
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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 { isInitEvent, Thread, ThreadEvent } from './basic.ts'
|
|
5
|
+
|
|
6
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
7
|
+
|
|
8
|
+
export type ApplogEventMapper = (this: MappedThread, event: ThreadEvent, sourceThread: Thread) => ThreadEvent
|
|
9
|
+
// 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
|
|
10
|
+
export type ApplogWriteMapper = (this: MappedThread, applogs: Applog[] | ApplogForInsert[]) => Applog[] | ApplogForInsert[] | null
|
|
11
|
+
|
|
12
|
+
export class MappedThread extends Thread {
|
|
13
|
+
static mapWrites(parent: Thread, name: string, mapper: ApplogWriteMapper) {
|
|
14
|
+
return new MappedThread(
|
|
15
|
+
`${name}<${parent.nameAndSizeUntracked}>`,
|
|
16
|
+
parent,
|
|
17
|
+
parent.filters,
|
|
18
|
+
// @ts-expect-error ? what's the proper way
|
|
19
|
+
parent._applogs,
|
|
20
|
+
null,
|
|
21
|
+
mapper,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
static asReadOnly(parent: Thread) {
|
|
25
|
+
if (parent.readOnly) return parent // already read-only, no need to wrap
|
|
26
|
+
return new MappedThread(
|
|
27
|
+
`readOnly(${parent.name})`,
|
|
28
|
+
parent,
|
|
29
|
+
parent.filters,
|
|
30
|
+
// @ts-expect-error ? what's the proper way
|
|
31
|
+
parent._applogs,
|
|
32
|
+
null,
|
|
33
|
+
null,
|
|
34
|
+
true, // readOnly
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
private _parentSubscriptions: Map<Thread, (() => void)> = null // mapped to unsubscribe function
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
readonly name: string,
|
|
41
|
+
parents: Thread | readonly Thread[],
|
|
42
|
+
filters: readonly string[],
|
|
43
|
+
readonly _initialLogs: Applog[],
|
|
44
|
+
private _eventMapper: ApplogEventMapper | null,
|
|
45
|
+
private _writeMapper: ApplogWriteMapper = null,
|
|
46
|
+
private _readOnly?: boolean,
|
|
47
|
+
) {
|
|
48
|
+
super(
|
|
49
|
+
name,
|
|
50
|
+
parents,
|
|
51
|
+
filters,
|
|
52
|
+
_eventMapper ?
|
|
53
|
+
[..._initialLogs] // clone — mapped thread manages its own array
|
|
54
|
+
: _initialLogs, // if we don't map events, we just re-use the array and then don't need to subscribe
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (_eventMapper) { // if we don't map events, we just re-use the array and then don't need to subscribe
|
|
58
|
+
this.subscribeToParents()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public insert(appLogsToInsert: ApplogForInsert[]) {
|
|
63
|
+
if (this.readOnly) throw ERROR(`[MappedThread] insert() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
64
|
+
const mapped = this._writeMapper ? this._writeMapper(appLogsToInsert) : appLogsToInsert
|
|
65
|
+
if (this._writeMapper && !mapped) return
|
|
66
|
+
return this.parents.forEach(parent => parent.insert(mapped))
|
|
67
|
+
}
|
|
68
|
+
public insertRaw(appLogsToInsert: Applog[]) {
|
|
69
|
+
if (this.readOnly) throw ERROR(`[MappedThread] insertRaw() called on read-only thread:`, this.nameAndSizeUntracked)
|
|
70
|
+
const mapped = this._writeMapper ? this._writeMapper(appLogsToInsert) : appLogsToInsert
|
|
71
|
+
if (this._writeMapper && !mapped) return
|
|
72
|
+
return this.parents.forEach(parent => parent.insertRaw(mapped as Applog[]))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private subscribeToParents() {
|
|
76
|
+
this._parentSubscriptions = new Map()
|
|
77
|
+
if (!this.parents.length) {
|
|
78
|
+
WARN(`MappedThread has no parents`, this)
|
|
79
|
+
}
|
|
80
|
+
VERBOSE(`[MappedThread: ${this.name}] subscribing to parents:`, this.parents.map(p => p.name))
|
|
81
|
+
|
|
82
|
+
for (const p of this.parents) {
|
|
83
|
+
VERBOSE(`[MappedThread: ${this.name}] sub to parent`, p.nameAndSizeUntracked)
|
|
84
|
+
const sub = this.onParentUpdate.bind(this, p)
|
|
85
|
+
const unsubscribe = p.subscribe(sub, 'derived')
|
|
86
|
+
this._parentSubscriptions.set(p, unsubscribe)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Tear down parent subscriptions and clear internal state */
|
|
91
|
+
dispose() {
|
|
92
|
+
if (this._parentSubscriptions) {
|
|
93
|
+
for (const [, unsubscribe] of this._parentSubscriptions) {
|
|
94
|
+
unsubscribe()
|
|
95
|
+
}
|
|
96
|
+
this._parentSubscriptions = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Swap parents at runtime — re-subscribes and recomputes applogs, notifying downstream */
|
|
101
|
+
setParents(newParents: readonly Thread[]) {
|
|
102
|
+
this.dispose()
|
|
103
|
+
;(this as { parents: readonly Thread[] }).parents = newParents
|
|
104
|
+
this.subscribeToParents()
|
|
105
|
+
this.triggerRemap()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
subscribe(callback: (event: ThreadEvent) => void, type?: 'derived' | 'reaction') {
|
|
109
|
+
if (!this._parentSubscriptions) {
|
|
110
|
+
this.subscribeToParents()
|
|
111
|
+
}
|
|
112
|
+
return super.subscribe(callback, type)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* // HACK to trigger remap on pattern change in rollingFilter
|
|
117
|
+
* should not be used lightly
|
|
118
|
+
*/
|
|
119
|
+
triggerRemap() {
|
|
120
|
+
if (!this._eventMapper) throw ERROR(`triggerRemap on a thread that is not actually mapping?!`, this.nameAndSizeUntracked)
|
|
121
|
+
DEBUG(`MappedThread{${this.nameAndSizeUntracked}} triggerRemap`)
|
|
122
|
+
if (this.parents.length !== 1) {
|
|
123
|
+
WARN(`MappedThread{${this.nameAndSizeUntracked}} triggerRemap with parentCount=${this.parents.length} - not meant for this`)
|
|
124
|
+
}
|
|
125
|
+
this.parents.forEach(p => {
|
|
126
|
+
this.onParentUpdate(p, { init: [...p.applogs] })
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected onParentUpdate(thread: Thread, event: ThreadEvent) {
|
|
131
|
+
VERBOSE(`MappedThread{${this.nameAndSizeUntracked}} parentUpdate`, event)
|
|
132
|
+
const mapResult = this._eventMapper ? this._eventMapper(event, thread) : event
|
|
133
|
+
if (this._eventMapper) {
|
|
134
|
+
VERBOSE(`MappedThread{${this.nameAndSizeUntracked}} parentUpdate => mapped`, mapResult)
|
|
135
|
+
if (isInitEvent(mapResult)) {
|
|
136
|
+
this._applogs.length = 0
|
|
137
|
+
this._applogs.push(...mapResult.init)
|
|
138
|
+
} else {
|
|
139
|
+
for (const log of mapResult.added) {
|
|
140
|
+
// insert at right location to maintain sort order
|
|
141
|
+
this._applogs.splice(sortedIndexBy(this._applogs, log, 'ts'), 0, log)
|
|
142
|
+
}
|
|
143
|
+
if (mapResult.removed) {
|
|
144
|
+
for (const toRemove of mapResult.removed) {
|
|
145
|
+
const idx = this._applogs.indexOf(toRemove)
|
|
146
|
+
if (idx >= 0) {
|
|
147
|
+
this._applogs.splice(idx, 1)
|
|
148
|
+
} else if (!isInitEvent(event) && event.removed?.includes(toRemove)) {
|
|
149
|
+
DEBUG(`Ignoring remove event for non-existent because it was part of parent event's removed`, toRemove, event) // ? convenience? - or is this a code smell?
|
|
150
|
+
} else {
|
|
151
|
+
throw ERROR(`MappedThread{${this.name}} toRemove contained log that doesn't exist`, toRemove, {
|
|
152
|
+
thread: this,
|
|
153
|
+
event,
|
|
154
|
+
mapResult,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} // (i) else, the _applogs is reactive to parent automatically, so no need
|
|
161
|
+
this.notifySubscribers(mapResult)
|
|
162
|
+
}
|
|
163
|
+
get readOnly() {
|
|
164
|
+
return this._readOnly ?? false
|
|
165
|
+
}
|
|
166
|
+
}
|