@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.
Files changed (180) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +3 -0
  3. package/dist/applog/applog-helpers.d.ts +47 -0
  4. package/dist/applog/applog-helpers.d.ts.map +1 -0
  5. package/dist/applog/applog-utils.d.ts +57 -0
  6. package/dist/applog/applog-utils.d.ts.map +1 -0
  7. package/dist/applog/datom-types.d.ts +128 -0
  8. package/dist/applog/datom-types.d.ts.map +1 -0
  9. package/dist/applog.d.ts +4 -0
  10. package/dist/applog.d.ts.map +1 -0
  11. package/dist/applog.js +101 -0
  12. package/dist/applog.js.map +1 -0
  13. package/dist/blockstore/index.d.ts +21 -0
  14. package/dist/blockstore/index.d.ts.map +1 -0
  15. package/dist/blockstore.d.ts +2 -0
  16. package/dist/blockstore.d.ts.map +1 -0
  17. package/dist/blockstore.js +24 -0
  18. package/dist/blockstore.js.map +1 -0
  19. package/dist/chunk-6MQKRL6W.js +86 -0
  20. package/dist/chunk-6MQKRL6W.js.map +1 -0
  21. package/dist/chunk-7MW34UEO.js +40 -0
  22. package/dist/chunk-7MW34UEO.js.map +1 -0
  23. package/dist/chunk-7Z5YDQKK.js +1 -0
  24. package/dist/chunk-7Z5YDQKK.js.map +1 -0
  25. package/dist/chunk-CY4NLISM.js +144 -0
  26. package/dist/chunk-CY4NLISM.js.map +1 -0
  27. package/dist/chunk-E46VTKTZ.js +1 -0
  28. package/dist/chunk-E46VTKTZ.js.map +1 -0
  29. package/dist/chunk-O43W7UW6.js +434 -0
  30. package/dist/chunk-O43W7UW6.js.map +1 -0
  31. package/dist/chunk-XIQSYEV3.js +1604 -0
  32. package/dist/chunk-XIQSYEV3.js.map +1 -0
  33. package/dist/chunk-XVGW4QC3.js +55 -0
  34. package/dist/chunk-XVGW4QC3.js.map +1 -0
  35. package/dist/chunk-YDAKBU6Q.js +9 -0
  36. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  37. package/dist/chunk-ZAADLBSB.js +36 -0
  38. package/dist/chunk-ZAADLBSB.js.map +1 -0
  39. package/dist/chunk-ZXCJRYD7.js +883 -0
  40. package/dist/chunk-ZXCJRYD7.js.map +1 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +354 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/ipfs/car.d.ts +59 -0
  46. package/dist/ipfs/car.d.ts.map +1 -0
  47. package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
  48. package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
  49. package/dist/ipfs/ipfs-utils.d.ts +35 -0
  50. package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
  51. package/dist/ipfs.d.ts +4 -0
  52. package/dist/ipfs.d.ts.map +1 -0
  53. package/dist/ipfs.js +60 -0
  54. package/dist/ipfs.js.map +1 -0
  55. package/dist/ipns/ipns-record.d.ts +34 -0
  56. package/dist/ipns/ipns-record.d.ts.map +1 -0
  57. package/dist/ipns.d.ts +2 -0
  58. package/dist/ipns.d.ts.map +1 -0
  59. package/dist/ipns.js +64 -0
  60. package/dist/ipns.js.map +1 -0
  61. package/dist/pubsub/connector.d.ts +9 -0
  62. package/dist/pubsub/connector.d.ts.map +1 -0
  63. package/dist/pubsub/pub-pull.d.ts +14 -0
  64. package/dist/pubsub/pub-pull.d.ts.map +1 -0
  65. package/dist/pubsub/pubsub-types.d.ts +72 -0
  66. package/dist/pubsub/pubsub-types.d.ts.map +1 -0
  67. package/dist/pubsub/snap-push.d.ts +41 -0
  68. package/dist/pubsub/snap-push.d.ts.map +1 -0
  69. package/dist/pubsub/ucan-example.d.ts +3 -0
  70. package/dist/pubsub/ucan-example.d.ts.map +1 -0
  71. package/dist/pubsub/ucan.d.ts +16 -0
  72. package/dist/pubsub/ucan.d.ts.map +1 -0
  73. package/dist/pubsub.d.ts +5 -0
  74. package/dist/pubsub.d.ts.map +1 -0
  75. package/dist/pubsub.js +31 -0
  76. package/dist/pubsub.js.map +1 -0
  77. package/dist/query/basic.d.ts +105 -0
  78. package/dist/query/basic.d.ts.map +1 -0
  79. package/dist/query/divergences.d.ts +12 -0
  80. package/dist/query/divergences.d.ts.map +1 -0
  81. package/dist/query/matchers.d.ts +4 -0
  82. package/dist/query/matchers.d.ts.map +1 -0
  83. package/dist/query/memoized.d.ts +66 -0
  84. package/dist/query/memoized.d.ts.map +1 -0
  85. package/dist/query/query-steps.d.ts +4 -0
  86. package/dist/query/query-steps.d.ts.map +1 -0
  87. package/dist/query/situations.d.ts +80 -0
  88. package/dist/query/situations.d.ts.map +1 -0
  89. package/dist/query/subscribable.d.ts +102 -0
  90. package/dist/query/subscribable.d.ts.map +1 -0
  91. package/dist/query/types.d.ts +70 -0
  92. package/dist/query/types.d.ts.map +1 -0
  93. package/dist/query.d.ts +8 -0
  94. package/dist/query.d.ts.map +1 -0
  95. package/dist/query.js +108 -0
  96. package/dist/query.js.map +1 -0
  97. package/dist/retrieve/index.d.ts +2 -0
  98. package/dist/retrieve/index.d.ts.map +1 -0
  99. package/dist/retrieve/update-thread.d.ts +64 -0
  100. package/dist/retrieve/update-thread.d.ts.map +1 -0
  101. package/dist/retrieve.d.ts +2 -0
  102. package/dist/retrieve.d.ts.map +1 -0
  103. package/dist/retrieve.js +14 -0
  104. package/dist/retrieve.js.map +1 -0
  105. package/dist/thread/basic.d.ts +60 -0
  106. package/dist/thread/basic.d.ts.map +1 -0
  107. package/dist/thread/filters.d.ts +47 -0
  108. package/dist/thread/filters.d.ts.map +1 -0
  109. package/dist/thread/mapped.d.ts +31 -0
  110. package/dist/thread/mapped.d.ts.map +1 -0
  111. package/dist/thread/utils.d.ts +23 -0
  112. package/dist/thread/utils.d.ts.map +1 -0
  113. package/dist/thread/writeable.d.ts +41 -0
  114. package/dist/thread/writeable.d.ts.map +1 -0
  115. package/dist/thread.d.ts +6 -0
  116. package/dist/thread.d.ts.map +1 -0
  117. package/dist/thread.js +54 -0
  118. package/dist/thread.js.map +1 -0
  119. package/dist/types/typescript-utils.d.ts +34 -0
  120. package/dist/types/typescript-utils.d.ts.map +1 -0
  121. package/dist/types.d.ts +2 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +26 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/debug-name.d.ts +13 -0
  126. package/dist/utils/debug-name.d.ts.map +1 -0
  127. package/dist/utils.d.ts +4 -0
  128. package/dist/utils.d.ts.map +1 -0
  129. package/dist/utils.js +9 -0
  130. package/dist/utils.js.map +1 -0
  131. package/package.json +110 -0
  132. package/src/applog/applog-helpers.ts +150 -0
  133. package/src/applog/applog-utils.ts +398 -0
  134. package/src/applog/datom-types.ts +148 -0
  135. package/src/applog.ts +3 -0
  136. package/src/blockstore/index.ts +36 -0
  137. package/src/blockstore.ts +1 -0
  138. package/src/index.ts +8 -0
  139. package/src/ipfs/car.ts +291 -0
  140. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  141. package/src/ipfs/ipfs-utils.ts +132 -0
  142. package/src/ipfs.ts +3 -0
  143. package/src/ipns/ipns-record.ts +115 -0
  144. package/src/ipns.ts +1 -0
  145. package/src/pubsub/UCAN Specs Overview.md +217 -0
  146. package/src/pubsub/connector.ts +9 -0
  147. package/src/pubsub/pub-pull.ts +31 -0
  148. package/src/pubsub/pubsub-types.ts +90 -0
  149. package/src/pubsub/snap-push.ts +277 -0
  150. package/src/pubsub/ucan-example.ts +61 -0
  151. package/src/pubsub/ucan.ts +56 -0
  152. package/src/pubsub.ts +4 -0
  153. package/src/query/basic.ts +1061 -0
  154. package/src/query/divergences.ts +50 -0
  155. package/src/query/matchers.ts +8 -0
  156. package/src/query/memoized.test.ts +151 -0
  157. package/src/query/memoized.ts +180 -0
  158. package/src/query/query-steps.ts +4 -0
  159. package/src/query/query.test.ts +536 -0
  160. package/src/query/situations.ts +261 -0
  161. package/src/query/subscribable.test.ts +245 -0
  162. package/src/query/subscribable.ts +225 -0
  163. package/src/query/types.ts +155 -0
  164. package/src/query.ts +7 -0
  165. package/src/retrieve/index.ts +1 -0
  166. package/src/retrieve/update-thread.ts +248 -0
  167. package/src/retrieve.ts +1 -0
  168. package/src/test/perf/query.1m.perf.test.ts +94 -0
  169. package/src/test/perf/query.perf.test.ts +389 -0
  170. package/src/test/perf/query.realdata.perf.test.ts +175 -0
  171. package/src/thread/basic.ts +209 -0
  172. package/src/thread/filters.ts +234 -0
  173. package/src/thread/mapped.ts +166 -0
  174. package/src/thread/utils.ts +146 -0
  175. package/src/thread/writeable.ts +163 -0
  176. package/src/thread.ts +5 -0
  177. package/src/types/typescript-utils.ts +64 -0
  178. package/src/types.ts +1 -0
  179. package/src/utils/debug-name.ts +54 -0
  180. 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
+ }