@wovin/core 0.1.36 → 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.
Files changed (212) hide show
  1. package/README.md +0 -12
  2. package/dist/applog/applog-helpers.d.ts +12 -12
  3. package/dist/applog/applog-helpers.d.ts.map +1 -1
  4. package/dist/applog/applog-utils.d.ts +25 -6
  5. package/dist/applog/applog-utils.d.ts.map +1 -1
  6. package/dist/applog/datom-types.d.ts +4 -5
  7. package/dist/applog/datom-types.d.ts.map +1 -1
  8. package/dist/applog.d.ts +3 -3
  9. package/dist/applog.d.ts.map +1 -1
  10. package/dist/{applog.min.js → applog.js} +6 -7
  11. package/dist/blockstore.d.ts +1 -1
  12. package/dist/blockstore.d.ts.map +1 -1
  13. package/dist/{blockstore.min.js → blockstore.js} +1 -3
  14. package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
  15. package/dist/{chunk-KXMTKPF4.min.js → chunk-3JZMOEOD.js} +8 -8
  16. package/dist/chunk-3JZMOEOD.js.map +1 -0
  17. package/dist/chunk-3WZVG277.js +434 -0
  18. package/dist/chunk-3WZVG277.js.map +1 -0
  19. package/dist/chunk-7Z5YDQKK.js +1 -0
  20. package/dist/chunk-CPSDKFBG.js +147 -0
  21. package/dist/chunk-CPSDKFBG.js.map +1 -0
  22. package/dist/chunk-E46VTKTZ.js +1 -0
  23. package/dist/{chunk-H3VQJP56.min.js → chunk-J2FDHGOZ.js} +9 -9
  24. package/dist/chunk-J2FDHGOZ.js.map +1 -0
  25. package/dist/chunk-L5EEEGE6.js +1862 -0
  26. package/dist/chunk-L5EEEGE6.js.map +1 -0
  27. package/dist/{chunk-BRC7LSM6.min.js → chunk-PD3C7XUM.js} +5 -5
  28. package/dist/chunk-PD3C7XUM.js.map +1 -0
  29. package/dist/chunk-QZXKQCAY.js +1026 -0
  30. package/dist/chunk-QZXKQCAY.js.map +1 -0
  31. package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
  32. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  33. package/dist/chunk-ZAADLBSB.js +36 -0
  34. package/dist/chunk-ZAADLBSB.js.map +1 -0
  35. package/dist/index.d.ts +7 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/{index.min.js → index.js} +73 -46
  38. package/dist/ipfs/car.d.ts +11 -11
  39. package/dist/ipfs/car.d.ts.map +1 -1
  40. package/dist/ipfs/ipfs-utils.d.ts +2 -2
  41. package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
  42. package/dist/ipfs.d.ts +3 -3
  43. package/dist/ipfs.d.ts.map +1 -1
  44. package/dist/{ipfs.min.js → ipfs.js} +7 -10
  45. package/dist/ipns.d.ts +1 -1
  46. package/dist/ipns.d.ts.map +1 -1
  47. package/dist/ipns.js +64 -0
  48. package/dist/ipns.js.map +1 -0
  49. package/dist/pubsub/pub-pull.d.ts +3 -3
  50. package/dist/pubsub/pub-pull.d.ts.map +1 -1
  51. package/dist/pubsub/pubsub-types.d.ts +3 -3
  52. package/dist/pubsub/pubsub-types.d.ts.map +1 -1
  53. package/dist/pubsub/snap-push.d.ts +4 -4
  54. package/dist/pubsub/snap-push.d.ts.map +1 -1
  55. package/dist/pubsub/ucan.d.ts +1 -1
  56. package/dist/pubsub/ucan.d.ts.map +1 -1
  57. package/dist/pubsub.d.ts +4 -4
  58. package/dist/pubsub.d.ts.map +1 -1
  59. package/dist/{pubsub.min.js → pubsub.js} +7 -10
  60. package/dist/query/attr-helpers.d.ts +5 -0
  61. package/dist/query/attr-helpers.d.ts.map +1 -0
  62. package/dist/query/basic.d.ts +85 -21
  63. package/dist/query/basic.d.ts.map +1 -1
  64. package/dist/query/divergences.d.ts +5 -5
  65. package/dist/query/divergences.d.ts.map +1 -1
  66. package/dist/query/entity-collection.d.ts +19 -0
  67. package/dist/query/entity-collection.d.ts.map +1 -0
  68. package/dist/query/matchers.d.ts +1 -1
  69. package/dist/query/matchers.d.ts.map +1 -1
  70. package/dist/query/memoized.d.ts +66 -0
  71. package/dist/query/memoized.d.ts.map +1 -0
  72. package/dist/query/situations.d.ts +2 -1
  73. package/dist/query/situations.d.ts.map +1 -1
  74. package/dist/query/subscribable.d.ts +111 -0
  75. package/dist/query/subscribable.d.ts.map +1 -0
  76. package/dist/query/types.d.ts +54 -14
  77. package/dist/query/types.d.ts.map +1 -1
  78. package/dist/query.d.ts +9 -5
  79. package/dist/query.d.ts.map +1 -1
  80. package/dist/{query.min.js → query.js} +51 -32
  81. package/dist/retrieve/index.d.ts +1 -1
  82. package/dist/retrieve/index.d.ts.map +1 -1
  83. package/dist/retrieve/update-thread.d.ts +3 -3
  84. package/dist/retrieve/update-thread.d.ts.map +1 -1
  85. package/dist/retrieve.d.ts +1 -1
  86. package/dist/retrieve.d.ts.map +1 -1
  87. package/dist/retrieve.js +14 -0
  88. package/dist/thread/basic.d.ts +15 -19
  89. package/dist/thread/basic.d.ts.map +1 -1
  90. package/dist/thread/filters.d.ts +8 -10
  91. package/dist/thread/filters.d.ts.map +1 -1
  92. package/dist/thread/indexes.d.ts +56 -0
  93. package/dist/thread/indexes.d.ts.map +1 -0
  94. package/dist/thread/mapped.d.ts +40 -11
  95. package/dist/thread/mapped.d.ts.map +1 -1
  96. package/dist/thread/utils.d.ts +5 -5
  97. package/dist/thread/utils.d.ts.map +1 -1
  98. package/dist/thread/writeable.d.ts +2 -2
  99. package/dist/thread/writeable.d.ts.map +1 -1
  100. package/dist/thread.d.ts +6 -5
  101. package/dist/thread.d.ts.map +1 -1
  102. package/dist/{thread.min.js → thread.js} +9 -6
  103. package/dist/types/typescript-utils.d.ts +6 -5
  104. package/dist/types/typescript-utils.d.ts.map +1 -1
  105. package/dist/types.d.ts +1 -1
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/{types.min.js → types.js} +3 -4
  108. package/dist/utils/debug-name.d.ts +13 -0
  109. package/dist/utils/debug-name.d.ts.map +1 -0
  110. package/dist/utils.d.ts +1 -1
  111. package/dist/utils.d.ts.map +1 -1
  112. package/dist/utils.js +9 -0
  113. package/package.json +32 -23
  114. package/src/applog/applog-helpers.ts +155 -0
  115. package/src/applog/applog-utils.test.ts +108 -0
  116. package/src/applog/applog-utils.ts +507 -0
  117. package/src/applog/datom-types.ts +148 -0
  118. package/src/applog.ts +3 -0
  119. package/src/blockstore/index.ts +36 -0
  120. package/src/blockstore.ts +1 -0
  121. package/src/index.ts +8 -0
  122. package/src/ipfs/car.ts +291 -0
  123. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  124. package/src/ipfs/ipfs-utils.ts +132 -0
  125. package/src/ipfs.ts +3 -0
  126. package/src/ipns/ipns-record.ts +115 -0
  127. package/src/ipns.ts +1 -0
  128. package/src/pubsub/UCAN Specs Overview.md +217 -0
  129. package/src/pubsub/connector.ts +9 -0
  130. package/src/pubsub/pub-pull.ts +31 -0
  131. package/src/pubsub/pubsub-types.ts +90 -0
  132. package/src/pubsub/snap-push.ts +277 -0
  133. package/src/pubsub/ucan-example.ts +61 -0
  134. package/src/pubsub/ucan.ts +56 -0
  135. package/src/pubsub.ts +4 -0
  136. package/src/query/attr-helpers.ts +5 -0
  137. package/src/query/basic.ts +1245 -0
  138. package/src/query/divergences.ts +50 -0
  139. package/src/query/entity-collection.ts +131 -0
  140. package/src/query/liveFilterAndMap.test.ts +102 -0
  141. package/src/query/matchers.ts +8 -0
  142. package/src/query/memoized.test.ts +151 -0
  143. package/src/query/memoized.ts +180 -0
  144. package/src/query/query-steps.ts +4 -0
  145. package/src/query/query.test.ts +538 -0
  146. package/src/query/situations.ts +261 -0
  147. package/src/query/subscribable.test.ts +245 -0
  148. package/src/query/subscribable.ts +234 -0
  149. package/src/query/types.ts +155 -0
  150. package/src/query/withoutDeleted.test.ts +204 -0
  151. package/src/query.ts +9 -0
  152. package/src/retrieve/index.ts +1 -0
  153. package/src/retrieve/update-thread.ts +248 -0
  154. package/src/retrieve.ts +1 -0
  155. package/src/test/perf/query.1m.perf.test.ts +94 -0
  156. package/src/test/perf/query.perf.test.ts +389 -0
  157. package/src/test/perf/query.realdata.perf.test.ts +182 -0
  158. package/src/thread/basic.ts +209 -0
  159. package/src/thread/filters.ts +227 -0
  160. package/src/thread/indexes.ts +250 -0
  161. package/src/thread/joinThreads.test.ts +304 -0
  162. package/src/thread/mapped.ts +226 -0
  163. package/src/thread/utils.ts +144 -0
  164. package/src/thread/writeable.ts +163 -0
  165. package/src/thread.ts +6 -0
  166. package/src/types/typescript-utils.ts +64 -0
  167. package/src/types.ts +1 -0
  168. package/src/utils/debug-name.ts +54 -0
  169. package/src/utils.ts +4 -0
  170. package/dist/chunk-2Y2PYHGR.min.js +0 -65
  171. package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
  172. package/dist/chunk-5MMGBK2U.min.js +0 -1
  173. package/dist/chunk-7IDQIMQO.min.js +0 -1
  174. package/dist/chunk-BRC7LSM6.min.js.map +0 -1
  175. package/dist/chunk-COXXILXC.min.js +0 -512
  176. package/dist/chunk-COXXILXC.min.js.map +0 -1
  177. package/dist/chunk-GDX2OO7L.min.js +0 -9080
  178. package/dist/chunk-GDX2OO7L.min.js.map +0 -1
  179. package/dist/chunk-H3VQJP56.min.js.map +0 -1
  180. package/dist/chunk-HYMC7W6S.min.js +0 -1549
  181. package/dist/chunk-HYMC7W6S.min.js.map +0 -1
  182. package/dist/chunk-KEHU7HGZ.min.js +0 -5216
  183. package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
  184. package/dist/chunk-KXMTKPF4.min.js.map +0 -1
  185. package/dist/chunk-PHITDXZT.min.js +0 -36
  186. package/dist/chunk-QO2KMGDN.min.js +0 -3771
  187. package/dist/chunk-QO2KMGDN.min.js.map +0 -1
  188. package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
  189. package/dist/chunk-WXLCBTHX.min.js +0 -1606
  190. package/dist/chunk-WXLCBTHX.min.js.map +0 -1
  191. package/dist/ipns.min.js +0 -6419
  192. package/dist/ipns.min.js.map +0 -1
  193. package/dist/mobx/mobx-utils.d.ts +0 -82
  194. package/dist/mobx/mobx-utils.d.ts.map +0 -1
  195. package/dist/mobx.d.ts +0 -2
  196. package/dist/mobx.d.ts.map +0 -1
  197. package/dist/mobx.min.js +0 -141
  198. package/dist/retrieve.min.js +0 -17
  199. package/dist/types.min.js.map +0 -1
  200. package/dist/utils.min.js +0 -10
  201. package/dist/utils.min.js.map +0 -1
  202. /package/dist/{applog.min.js.map → applog.js.map} +0 -0
  203. /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
  204. /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
  205. /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
  206. /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
  207. /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
  208. /package/dist/{mobx.min.js.map → query.js.map} +0 -0
  209. /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
  210. /package/dist/{query.min.js.map → thread.js.map} +0 -0
  211. /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
  212. /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
+ )