@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,1061 @@
1
+ import { AgentHash, Applog, ApplogValue, DatalogQueryPattern, EntityID, SearchContext, ValueOrMatcher } from '../applog/datom-types.ts'
2
+
3
+ import { Logger } from 'besonders-logger'
4
+
5
+ import { isEmpty } from 'lodash-es'
6
+ import stringify from 'safe-stable-stringify'
7
+ import { resolveOrRemoveVariables, sortApplogsByTs } from '../applog/applog-utils.ts'
8
+ import { createDebugName } from '../utils/debug-name.ts'
9
+ import { isInitEvent, StaticThread, Thread, ThreadEvent } from '../thread/basic.ts'
10
+ import { hasFilter, makeFilter, rollingFilter, rollingMapper, ThreadOnlyCurrent } from '../thread/filters.ts'
11
+ import { MappedThread } from '../thread/mapped.ts'
12
+ import { ThreadInMemory } from '../thread/writeable.ts'
13
+ import { memoizedFn } from './memoized.ts'
14
+ import { isArrayInitEvent, SubscribableArray, SubscribableArrayImpl, SubscribableImpl, Unsubscribe } from './subscribable.ts'
15
+ import { LiveQueryResult, QueryNode, QueryResult } from './types.ts'
16
+
17
+ const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO, { prefix: '[q]' }) // eslint-disable-line no-unused-vars
18
+
19
+ function assertLWW(thread: Thread) {
20
+ if (!hasFilter(thread, 'lastWriteWins')) {
21
+ throw ERROR(`requires lastWriteWins-filtered thread, got filters:`, thread.filters, { name: thread.name })
22
+ }
23
+ }
24
+
25
+ let globalQueryTimeoutTime = null
26
+
27
+ // util.inspect.defaultOptions.depth = 5;
28
+
29
+ // export interface QueryExecutorArguments {
30
+ // db: Thread
31
+ // // applogs: AppLog[]
32
+ // nodes: SearchContextWithLog[]
33
+ // }
34
+ // export interface QueryExecutorResult {
35
+ // // applogs: AppLog[]
36
+ // nodes: SearchContextWithLog[]
37
+ // }
38
+ // export type QueryExecutor = (args: QueryExecutorArguments) => QueryExecutorResult
39
+
40
+ /////////////
41
+ // QUERIES //
42
+ /////////////
43
+
44
+ /**
45
+ * Keep only the latest logs for each en&at (= last write wins)
46
+ */
47
+ export const lastWriteWins = memoizedFn('lastWriteWins', function lastWriteWins(
48
+ thread: Thread,
49
+ { inverseToOnlyReturnFirstLogs, tolerateAlreadyFiltered }: {
50
+ inverseToOnlyReturnFirstLogs?: boolean
51
+ tolerateAlreadyFiltered?: boolean
52
+ } = {},
53
+ ): ThreadOnlyCurrent {
54
+ VERBOSE(`lastWriteWins${inverseToOnlyReturnFirstLogs ? '.inversed' : ''} < ${thread.nameAndSizeUntracked} > initializing`)
55
+ if (thread.filters.includes('lastWriteWins')) {
56
+ if (tolerateAlreadyFiltered) {
57
+ DEBUG(`[lastWriteWins] already filtered, but tolerateAlreadyFiltered=true, so returning`)
58
+ return thread as ThreadOnlyCurrent
59
+ }
60
+ throw ERROR(`thread already filtered lastWriteWins:`, thread.filters, { name: thread.name })
61
+ }
62
+
63
+ let rollingMap: Map<string, Applog>
64
+ const mappedThread = rollingMapper(thread, function lastWriteWinsMapper(event, sourceThread) {
65
+ const isInitial = isInitEvent(event)
66
+
67
+ let newLogs: readonly Applog[]
68
+ const toAdd = [] as Applog[]
69
+ const toRemove = isInitial ? null : [] as Applog[]
70
+ if (isInitial) {
71
+ rollingMap = new Map()
72
+ newLogs = event.init
73
+ } else {
74
+ newLogs = event.added
75
+ }
76
+
77
+ let tsCheck: string
78
+ for (
79
+ let i = inverseToOnlyReturnFirstLogs ? 0 : newLogs.length - 1;
80
+ inverseToOnlyReturnFirstLogs ? i < newLogs.length : i >= 0;
81
+ inverseToOnlyReturnFirstLogs ? i++ : i--
82
+ ) {
83
+ const log = newLogs[i]
84
+ const key = log.en + '|' + log.at
85
+
86
+ // TODO: use isoDateStrCompare ?
87
+ if (tsCheck && (inverseToOnlyReturnFirstLogs ? tsCheck > log.ts : tsCheck < log.ts)) {
88
+ throw ERROR(`lastWriteWins.mapper logs not sorted:`, tsCheck, inverseToOnlyReturnFirstLogs ? '>' : '<', log.ts, {
89
+ log,
90
+ i,
91
+ newLogs,
92
+ inverseToOnlyReturnFirstLogs,
93
+ })
94
+ }
95
+ tsCheck = log.ts
96
+
97
+ const existing = rollingMap.get(key)
98
+ if (!existing || (inverseToOnlyReturnFirstLogs ? (existing.ts > log.ts) : (existing.ts < log.ts))) {
99
+ if (existing && !isInitial) toRemove.push(existing)
100
+ toAdd.push(log)
101
+ rollingMap.set(key, log)
102
+ }
103
+ }
104
+ sortApplogsByTs(toAdd) // HACK: find logical solution
105
+ VERBOSE.isDisabled ||
106
+ VERBOSE(
107
+ `lastWriteWins${inverseToOnlyReturnFirstLogs ? '.inversed' : ''}<${thread.nameAndSizeUntracked}> mapped event`,
108
+ isInitial ?
109
+ { ...Object.fromEntries(Object.entries(event).map(([k, v]) => [k, v?.length])), toAdd: toAdd.length, toRemove } :
110
+ { ...event, toAdd, toRemove },
111
+ )
112
+ return isInitial ?
113
+ { init: toAdd }
114
+ : { added: toAdd, removed: toRemove }
115
+ }, { name: `lastWriteWins${inverseToOnlyReturnFirstLogs ? '.inversed' : ''}`, extraFilterName: 'lastWriteWins' })
116
+ VERBOSE.isDisabled || VERBOSE(`lastWriteWins<${thread.nameAndSizeUntracked}> filtered down to`, mappedThread.applogs.length)
117
+ return mappedThread as ThreadOnlyCurrent
118
+ }, { argsDebugName: (thread) => createDebugName({ caller: 'lastWriteWins', thread }) })
119
+
120
+ /**
121
+ * Remove all applogs for entities that have an applog: { at: `isDeleted`, val: true }
122
+ * ! WARNING: If not based on lastWriteWins, it will not respect un-deletions yet (isDeleted: false)
123
+ */
124
+ export const withoutDeleted = memoizedFn('withoutDeleted', function withoutDeleted(
125
+ thread: Thread,
126
+ ) {
127
+ if (VERBOSE.isEnabled) VERBOSE(`withoutDeleted<${thread.nameAndSizeUntracked}>`)
128
+ if (thread.filters.includes('withoutDeleted')) {
129
+ throw ERROR(`thread already filtered withoutDeleted:`, thread.filters, { name: thread.name })
130
+ }
131
+
132
+ const deletionLogs = rollingFilter(
133
+ thread, // TODO: handle un-deletion
134
+ { at: ['isDeleted', 'relation/isDeleted', 'block/isDeleted'], vl: true },
135
+ { name: 'isDeleted' },
136
+ )
137
+ VERBOSE.isEnabled &&
138
+ VERBOSE(`withoutDeleted<${thread.nameAndSizeUntracked}> deletionLogs:`, [...deletionLogs.applogs])
139
+
140
+ // Build set of deleted entity IDs, kept up-to-date via subscribe
141
+ const deleted = new Set(deletionLogs.map(log => log.en))
142
+ const unsubDeletions = deletionLogs.subscribe(event => {
143
+ if (isInitEvent(event)) {
144
+ deleted.clear()
145
+ for (const log of event.init) deleted.add(log.en)
146
+ } else {
147
+ for (const log of event.added) deleted.add(log.en)
148
+ // Note: un-deletion not handled yet (TODO)
149
+ }
150
+ }, 'derived')
151
+
152
+ VERBOSE.isEnabled && VERBOSE(`withoutDeleted<${thread.nameAndSizeUntracked}> deleted:`, [...deleted])
153
+
154
+ const result = rollingFilter(thread, { '!en': deleted }, { name: `withoutDeleted`, extraFilterName: 'withoutDeleted' })
155
+ // Chain cleanup: when the result thread is disposed, also unsub from deletion tracking
156
+ const origDispose = result.dispose.bind(result)
157
+ result.dispose = () => { unsubDeletions(); origDispose() }
158
+ return result
159
+ })
160
+
161
+ ///////////////////////////
162
+ // ONE-OFF QUERY (snapshot) //
163
+ ///////////////////////////
164
+
165
+ /** Shared helper: create a QueryNode from a log and its context */
166
+ function makeQueryNode(
167
+ log: Applog,
168
+ parentNode: QueryNode | null,
169
+ varMapper: (log: Applog) => SearchContext,
170
+ threadName: string,
171
+ ): QueryNode {
172
+ const nodeVars = Object.assign({}, parentNode?.variables, varMapper(log))
173
+ return new QueryNode(
174
+ StaticThread.fromArray([log], threadName),
175
+ nodeVars,
176
+ parentNode,
177
+ )
178
+ }
179
+
180
+ /**
181
+ * One-off query — returns a plain snapshot. No subscriptions, no stale-data risk.
182
+ */
183
+ export const query = memoizedFn('query', function query(
184
+ threadOrLogs: Thread | Applog[],
185
+ patternOrPatterns: DatalogQueryPattern | DatalogQueryPattern[],
186
+ startVariables: SearchContext = {},
187
+ opts: { debug?: boolean } = {},
188
+ ): QueryResult {
189
+ throwOnTimeout()
190
+ const thread = threadFromMaybeArray(threadOrLogs)
191
+ DEBUG(`query<${thread.nameAndSizeUntracked}>:`, patternOrPatterns)
192
+ const patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]) as DatalogQueryPattern[]
193
+
194
+ let prevNodes: readonly QueryNode[] | null
195
+ if (patterns.length === 1) {
196
+ prevNodes = null
197
+ } else {
198
+ const patternsExceptLast = patterns.slice(0, -1)
199
+ prevNodes = query(thread, patternsExceptLast, startVariables, opts).nodes
200
+ }
201
+ const lastPattern = patterns[patterns.length - 1]
202
+ const stepResult = queryStepOnce(thread, prevNodes, lastPattern, opts)
203
+ VERBOSE.isDisabled || VERBOSE(`query result:`, stepResult.nodes)
204
+ return stepResult
205
+ }, {
206
+ argsDebugName: (thread, pattern, startVars) =>
207
+ createDebugName({ caller: 'query', thread, args: startVars ? { pattern, startVars } : pattern }),
208
+ })
209
+
210
+ /**
211
+ * One-off query step — pure filtering via makeFilter, no subscriptions.
212
+ */
213
+ export function queryStepOnce(
214
+ thread: Thread,
215
+ prevNodes: readonly QueryNode[] | null,
216
+ pattern: DatalogQueryPattern,
217
+ opts: { debug?: boolean } = {},
218
+ ): QueryResult {
219
+ DEBUG(`queryStepOnce<${thread.nameAndSizeUntracked}> with`, prevNodes?.length ?? 'all', 'nodes, pattern:', pattern)
220
+ if (!Object.entries(pattern).length) throw new Error(`Pattern is empty`)
221
+
222
+ function doQueryOnce(node: QueryNode | null): QueryNode[] {
223
+ const [patternWithResolvedVars, variablesToFill] = resolveOrRemoveVariables(pattern, node?.variables ?? {})
224
+ VERBOSE(`[queryStepOnce.doQuery] patternWithoutVars: `, patternWithResolvedVars)
225
+ const filter = makeFilter(patternWithResolvedVars)
226
+ const matchingLogs = filter(thread.applogs)
227
+ const varMapper = createObjMapper(variablesToFill)
228
+
229
+ const nodes = matchingLogs.map(log => makeQueryNode(
230
+ log, node, varMapper,
231
+ createDebugName({
232
+ caller: 'QueryNode',
233
+ thread,
234
+ pattern: `${stringify(Object.assign({}, node?.variables, varMapper(log)))}@${stringify(patternWithResolvedVars)}`,
235
+ }),
236
+ ))
237
+
238
+ if (VERBOSE.isEnabled) VERBOSE(`[queryStepOnce.doQuery] nodes:`, nodes.map(n => n.variables))
239
+ if (opts.debug) {
240
+ LOG(`[queryStepOnce] step result:`, nodes.map(({ variables, logsOfThisNode: thread }) => ({
241
+ variables,
242
+ thread,
243
+ })))
244
+ }
245
+
246
+ return nodes
247
+ }
248
+
249
+ if (!prevNodes) {
250
+ return new QueryResult(doQueryOnce(null))
251
+ }
252
+
253
+ const allNodes = prevNodes.flatMap(inputNode => doQueryOnce(inputNode))
254
+ return new QueryResult(allNodes)
255
+ }
256
+
257
+ ///////////////////////////
258
+ // LIVE QUERY (reactive) //
259
+ ///////////////////////////
260
+
261
+ /**
262
+ * Live query — eagerly activated, always up-to-date.
263
+ * Returns LiveQueryResult with subscribe + dispose.
264
+ */
265
+ export const liveQuery = memoizedFn('liveQuery', function liveQuery(
266
+ threadOrLogs: Thread | Applog[],
267
+ patternOrPatterns: DatalogQueryPattern | DatalogQueryPattern[],
268
+ startVariables: SearchContext = {},
269
+ opts: { debug?: boolean } = {},
270
+ ): LiveQueryResult {
271
+ throwOnTimeout()
272
+ const thread = threadFromMaybeArray(threadOrLogs)
273
+ DEBUG(`liveQuery<${thread.nameAndSizeUntracked}>:`, patternOrPatterns)
274
+ const patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]) as DatalogQueryPattern[]
275
+
276
+ let prevResult: LiveQueryResult | null
277
+ if (patterns.length === 1) {
278
+ prevResult = null
279
+ } else {
280
+ const patternsExceptLast = patterns.slice(0, -1)
281
+ prevResult = liveQuery(thread, patternsExceptLast, startVariables, opts)
282
+ }
283
+ const lastPattern = patterns[patterns.length - 1]
284
+ const stepResult = liveQueryStep(thread, prevResult, lastPattern, opts)
285
+ VERBOSE.isDisabled || VERBOSE(`liveQuery result:`, stepResult.nodes)
286
+ return stepResult
287
+ }, {
288
+ argsDebugName: (thread, pattern, startVars) =>
289
+ createDebugName({ caller: 'liveQuery', thread, args: startVars ? { pattern, startVars } : pattern }),
290
+ })
291
+
292
+ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
293
+ thread: Thread,
294
+ nodeSet: LiveQueryResult | null,
295
+ pattern: DatalogQueryPattern,
296
+ opts: { debug?: boolean } = {},
297
+ ): LiveQueryResult {
298
+ DEBUG(`liveQueryStep<${thread.nameAndSizeUntracked}> with`, nodeSet?.untrackedSize ?? 'all', 'nodes, pattern:', pattern)
299
+ if (!Object.entries(pattern).length) throw new Error(`Pattern is empty`)
300
+
301
+ function doQuery(node: QueryNode | null): SubscribableArray<QueryNode> {
302
+ const [patternWithResolvedVars, variablesToFill] = resolveOrRemoveVariables(pattern, node?.variables ?? {})
303
+ VERBOSE(`[liveQueryStep.doQuery] patternWithoutVars: `, patternWithResolvedVars)
304
+ const applogsMatchingStatic = rollingFilter(thread, patternWithResolvedVars)
305
+ const varMapper = createObjMapper(variablesToFill)
306
+
307
+ function makeNode(log: Applog): QueryNode {
308
+ return makeQueryNode(
309
+ log, node, varMapper,
310
+ createDebugName({
311
+ caller: 'QueryNode',
312
+ thread: applogsMatchingStatic,
313
+ pattern: `${stringify(Object.assign({}, node?.variables, varMapper(log)))}@${stringify(patternWithResolvedVars)}`,
314
+ }),
315
+ )
316
+ }
317
+
318
+ // Compute initial result synchronously
319
+ const initialNodes = applogsMatchingStatic.applogs.map(makeNode)
320
+
321
+ if (VERBOSE.isEnabled) VERBOSE(`[liveQueryStep.doQuery] initial nodes:`, initialNodes.map(n => n.variables))
322
+ if (opts.debug) {
323
+ LOG(`[liveQueryStep] step result:`, initialNodes.map(({ variables, logsOfThisNode: thread }) => ({
324
+ variables,
325
+ thread,
326
+ })))
327
+ }
328
+
329
+ // Upstream subscription activates lazily — only when someone subscribes to us
330
+ const result = new SubscribableArrayImpl<QueryNode>(
331
+ initialNodes,
332
+ () => applogsMatchingStatic.subscribe((event) => {
333
+ if (isInitEvent(event)) {
334
+ result._reset(event.init.map(makeNode))
335
+ } else {
336
+ if (event.added.length) {
337
+ result._push(...event.added.map(makeNode))
338
+ }
339
+ if (event.removed?.length) {
340
+ const removedCids = new Set(event.removed.map(log => log.cid))
341
+ const toRemove = result.items.filter(qn =>
342
+ removedCids.has(qn.logsOfThisNode.applogs[0]?.cid)
343
+ )
344
+ if (toRemove.length) result._remove(toRemove)
345
+ }
346
+ }
347
+ }, 'derived'),
348
+ )
349
+ return result
350
+ }
351
+
352
+ // ── Single-step query (nodeSet === null) ──────────────────────
353
+ if (!nodeSet) {
354
+ return new LiveQueryResult(doQuery(null))
355
+ }
356
+
357
+ // ── Multi-step query (nodeSet !== null) ────────────────────────
358
+
359
+ // Compute initial result synchronously
360
+ const initialInners = nodeSet.nodes.map(inputNode => ({
361
+ inputNode,
362
+ inner: doQuery(inputNode),
363
+ }))
364
+ const initialItems = initialInners.flatMap(({ inner }) => [...inner.items])
365
+
366
+ // Lazy activation: upstream subscriptions only created when someone subscribes
367
+ const aggregated = new SubscribableArrayImpl<QueryNode>(
368
+ initialItems,
369
+ () => {
370
+ const subsByInputNode = new Map<QueryNode, {
371
+ inner: SubscribableArray<QueryNode>,
372
+ unsub: Unsubscribe,
373
+ nodes: QueryNode[],
374
+ }>()
375
+
376
+ function wireInner(inputNode: QueryNode, inner: SubscribableArray<QueryNode>): QueryNode[] {
377
+ const entry = { inner, unsub: null! as Unsubscribe, nodes: [...inner.items] }
378
+
379
+ entry.unsub = inner.subscribe((event) => {
380
+ if (isArrayInitEvent(event)) {
381
+ if (entry.nodes.length) aggregated._remove(entry.nodes)
382
+ entry.nodes = [...event.init]
383
+ if (entry.nodes.length) aggregated._push(...entry.nodes)
384
+ } else {
385
+ if (event.added.length) {
386
+ entry.nodes.push(...event.added)
387
+ aggregated._push(...event.added)
388
+ }
389
+ if (event.removed?.length) {
390
+ for (const r of event.removed) {
391
+ const idx = entry.nodes.indexOf(r)
392
+ if (idx >= 0) entry.nodes.splice(idx, 1)
393
+ }
394
+ aggregated._remove(event.removed)
395
+ }
396
+ }
397
+ }, 'derived')
398
+
399
+ subsByInputNode.set(inputNode, entry)
400
+ return entry.nodes
401
+ }
402
+
403
+ function addInputNode(inputNode: QueryNode): QueryNode[] {
404
+ return wireInner(inputNode, doQuery(inputNode))
405
+ }
406
+
407
+ function removeInputNode(inputNode: QueryNode): QueryNode[] {
408
+ const entry = subsByInputNode.get(inputNode)
409
+ if (!entry) return []
410
+ entry.unsub()
411
+ entry.inner.dispose()
412
+ const removed = entry.nodes
413
+ subsByInputNode.delete(inputNode)
414
+ return removed
415
+ }
416
+
417
+ // Reuse pre-computed inners (no re-creation of sub-queries)
418
+ for (const { inputNode, inner } of initialInners) {
419
+ wireInner(inputNode, inner)
420
+ }
421
+
422
+ // Subscribe to previous step for FUTURE changes only (no init)
423
+ const prevUnsub = nodeSet.subscribe((event) => {
424
+ if (isArrayInitEvent(event)) {
425
+ for (const [, entry] of subsByInputNode) {
426
+ entry.unsub(); entry.inner.dispose()
427
+ }
428
+ subsByInputNode.clear()
429
+ const allNodes: QueryNode[] = []
430
+ for (const node of event.init) {
431
+ allNodes.push(...addInputNode(node))
432
+ }
433
+ aggregated._reset(allNodes)
434
+ } else {
435
+ if (event.added.length) {
436
+ const allAdded: QueryNode[] = []
437
+ for (const node of event.added) {
438
+ allAdded.push(...addInputNode(node))
439
+ }
440
+ if (allAdded.length) aggregated._push(...allAdded)
441
+ }
442
+ if (event.removed?.length) {
443
+ const allRemoved: QueryNode[] = []
444
+ for (const node of event.removed) {
445
+ allRemoved.push(...removeInputNode(node))
446
+ }
447
+ if (allRemoved.length) aggregated._remove(allRemoved)
448
+ }
449
+ }
450
+ }, 'derived')
451
+
452
+ return () => {
453
+ prevUnsub()
454
+ for (const [, entry] of subsByInputNode) {
455
+ entry.unsub(); entry.inner.dispose()
456
+ }
457
+ subsByInputNode.clear()
458
+ }
459
+ },
460
+ )
461
+
462
+ if (VERBOSE.isEnabled) VERBOSE(`[liveQueryStep] aggregated initial:`, [...aggregated.items])
463
+ return new LiveQueryResult(aggregated)
464
+ }, { argsDebugName: (thread, _nodes, pattern) => createDebugName({ caller: 'liveQueryStep', thread, pattern }) })
465
+
466
+ export const queryNot = memoizedFn('queryNot', function queryNot(
467
+ thread: Thread,
468
+ startNodes: QueryResult,
469
+ patternOrPatterns: DatalogQueryPattern | DatalogQueryPattern[],
470
+ opts: { debug?: boolean } = {},
471
+ ) {
472
+ const nodes = startNodes.nodes
473
+ DEBUG(`queryNot<${thread.nameAndSizeUntracked}> from: ${nodes.length} nodes`)
474
+ const patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]) as DatalogQueryPattern[]
475
+
476
+ // For each node, run all patterns as a joined multi-step query.
477
+ // Exclude the node if ANY complete binding exists across all steps.
478
+ const filtered = nodes.filter(function innerNodeFilter({ variables }) {
479
+ // Start with a single binding from the node's variables
480
+ let bindings: Record<string, any>[] = [variables ?? {}]
481
+
482
+ for (const pattern of patterns) {
483
+ if (!Object.entries(pattern).length) throw new Error(`Pattern is empty`)
484
+ const nextBindings: Record<string, any>[] = []
485
+
486
+ for (const binding of bindings) {
487
+ const [resolved, varsToFill] = resolveOrRemoveVariables(pattern, binding)
488
+ const filter = makeFilter(resolved)
489
+ const matchingLogs = filter(thread.applogs)
490
+ const varMapper = createObjMapper(varsToFill)
491
+
492
+ for (const log of matchingLogs) {
493
+ nextBindings.push({ ...binding, ...varMapper(log) })
494
+ }
495
+ }
496
+
497
+ bindings = nextBindings
498
+ if (bindings.length === 0) break // no matches — node is safe, skip remaining patterns
499
+ }
500
+
501
+ VERBOSE(`[queryNot] node:`, variables, '=> bindings:', bindings.length)
502
+ if (opts.debug) LOG(`[queryNot] node result:`, variables, '=>', bindings)
503
+ return bindings.length === 0 // keep node if no complete match found
504
+ })
505
+ return new QueryResult([...filtered])
506
+ }, { argsDebugName: (thread, nodes, pattern) => createDebugName({ caller: 'queryNot', thread, pattern }) })
507
+
508
+ /** Live variant: queryNot with incremental updates.
509
+ * - Thread additions: O(new_applogs × included_nodes) — only checks new applogs
510
+ * - Thread removals/resets: full recompute (rare for append-mostly logs)
511
+ * - Upstream node additions: O(new_nodes × applogs)
512
+ * - Upstream node removals: removed from output
513
+ */
514
+ export const liveQueryNot = memoizedFn('liveQueryNot', function liveQueryNot(
515
+ thread: Thread,
516
+ upstream: LiveQueryResult,
517
+ patternOrPatterns: DatalogQueryPattern | DatalogQueryPattern[],
518
+ opts: { debug?: boolean } = {},
519
+ ) {
520
+ const patterns = (Array.isArray(patternOrPatterns) ? patternOrPatterns : [patternOrPatterns]) as DatalogQueryPattern[]
521
+
522
+ /** Check if a node should be excluded (matches the NOT patterns as a joined multi-step query) */
523
+ function nodeMatchesNot(node: QueryNode, applogs: readonly Applog[]): boolean {
524
+ let bindings: Record<string, any>[] = [node.variables ?? {}]
525
+ for (const pattern of patterns) {
526
+ const nextBindings: Record<string, any>[] = []
527
+ for (const binding of bindings) {
528
+ const [resolved, varsToFill] = resolveOrRemoveVariables(pattern, binding)
529
+ const filter = makeFilter(resolved)
530
+ const varMapper = createObjMapper(varsToFill)
531
+ for (const log of filter(applogs)) {
532
+ nextBindings.push({ ...binding, ...varMapper(log) })
533
+ }
534
+ }
535
+ bindings = nextBindings
536
+ if (bindings.length === 0) return false // no matches — node passes
537
+ }
538
+ return bindings.length > 0 // excluded if any complete binding exists
539
+ }
540
+
541
+ /** Full recompute: filter all upstream nodes against all thread applogs */
542
+ function computeAll(): QueryNode[] {
543
+ return upstream.nodes.filter(node => !nodeMatchesNot(node, thread.applogs))
544
+ }
545
+
546
+ const result = new SubscribableArrayImpl<QueryNode>(
547
+ computeAll(),
548
+ () => {
549
+ // Subscribe to thread changes
550
+ const threadUnsub = thread.subscribe((event) => {
551
+ if (isInitEvent(event)) {
552
+ // Full reset — recompute everything
553
+ result._reset(computeAll())
554
+ return
555
+ }
556
+
557
+ if (event.removed?.length) {
558
+ // Removals: a previously-excluded node might now pass — full recompute
559
+ result._reset(computeAll())
560
+ return
561
+ }
562
+
563
+ if (event.added.length) {
564
+ // Additions: only check new applogs against currently-included nodes
565
+ const toRemove = result.items.filter(node => nodeMatchesNot(node, event.added))
566
+ if (toRemove.length > 0) {
567
+ result._remove(toRemove)
568
+ }
569
+ }
570
+ }, 'derived')
571
+
572
+ // Subscribe to upstream node changes
573
+ const upstreamUnsub = upstream.subscribe((event) => {
574
+ if (isArrayInitEvent(event)) {
575
+ result._reset(computeAll())
576
+ return
577
+ }
578
+
579
+ // New upstream nodes: check each against full thread
580
+ if (event.added.length) {
581
+ const passing = event.added.filter(node => !nodeMatchesNot(node, thread.applogs))
582
+ if (passing.length > 0) result._push(...passing)
583
+ }
584
+
585
+ // Removed upstream nodes: remove from our output
586
+ if (event.removed?.length) {
587
+ const removedSet = new Set(event.removed)
588
+ const toRemove = result.items.filter(node => removedSet.has(node))
589
+ if (toRemove.length > 0) result._remove(toRemove)
590
+ }
591
+ }, 'derived')
592
+
593
+ return () => { threadUnsub(); upstreamUnsub() }
594
+ },
595
+ )
596
+
597
+ return new LiveQueryResult(result)
598
+ }, { argsDebugName: (thread, _nodes, pattern) => createDebugName({ caller: 'liveQueryNot', thread, pattern }) })
599
+
600
+ // export function or(queries: QueryExecutor[]) {
601
+ // return tagged(
602
+ // `or{${stringify(queries)} } `,
603
+ // function orExecutor(args: QueryExecutorArguments) {
604
+ // const { db, nodes: contexts } = args
605
+ // VERBOSE('[or]', { queries, contexts })
606
+ // let results = []
607
+ // for (const query of queries) {
608
+ // const res = query(args)
609
+ // VERBOSE('[or] query', query, 'result =>', res)
610
+ // results.push(...res.nodes)
611
+ // }
612
+ // return { contexts: results }
613
+ // }
614
+ // )
615
+ // }
616
+
617
+ // export type Tagged<T> = T & { tag: string }
618
+ // export function tagged<T>(tag: string, thing: T): Tagged<T> {
619
+ // const e = thing as (T & { tag: string })
620
+ // e.tag = tag
621
+ // return e
622
+ // }
623
+
624
+ //////////////////////
625
+ // COMPOSED QUERIES //
626
+ //////////////////////
627
+
628
+ /** One-off: filter thread by pattern, map to values. Returns plain array. */
629
+ export const filterAndMap = memoizedFn('filterAndMap', function filterAndMap<R>(
630
+ thread: Thread,
631
+ pattern: DatalogQueryPattern,
632
+ mapper: (keyof Applog) | (Partial<{ [key in keyof Applog]: string }>) | ((applog: Applog) => R),
633
+ ) {
634
+ DEBUG(`filterAndMap<${thread.nameAndSizeUntracked}>`, pattern)
635
+ const filter = makeFilter(pattern)
636
+ const filtered = filter(thread.applogs)
637
+ return mapApplogsWith(filtered, mapper)
638
+ }, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'filterAndMap', thread, pattern }) })
639
+
640
+ /** Live variant: returns SubscribableArray that updates when thread changes. */
641
+ export const liveFilterAndMap = memoizedFn('liveFilterAndMap', function liveFilterAndMap<R>(
642
+ thread: Thread,
643
+ pattern: DatalogQueryPattern,
644
+ mapper: (keyof Applog) | (Partial<{ [key in keyof Applog]: string }>) | ((applog: Applog) => R),
645
+ ) {
646
+ DEBUG(`liveFilterAndMap<${thread.nameAndSizeUntracked}>`, pattern)
647
+ const filtered = rollingFilter(thread, pattern)
648
+ const mapFn = makeApplogMapper(mapper)
649
+
650
+ const initial = filtered.applogs.map(mapFn)
651
+ const result = new SubscribableArrayImpl<R>(
652
+ initial,
653
+ () => filtered.subscribe((event) => {
654
+ if (isInitEvent(event)) {
655
+ result._reset(event.init.map(mapFn))
656
+ } else {
657
+ if (event.added.length) result._push(...event.added.map(mapFn))
658
+ // Note: removed events not mapped — would need identity tracking
659
+ }
660
+ }, 'derived'),
661
+ )
662
+ return result
663
+ }, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveFilterAndMap', thread, pattern }) })
664
+
665
+ /** One-off: query and map results. Returns plain array. */
666
+ export const queryAndMap = memoizedFn('queryAndMap', function queryAndMap<R>(
667
+ threadOrLogs: Thread | Applog[],
668
+ patternOrPatterns: Parameters<typeof query>[1],
669
+ mapDef: string | (Partial<{ [key in keyof SearchContext]: string }>) | ((record: SearchContext) => R),
670
+ variables: SearchContext = {},
671
+ ) {
672
+ const thread = threadFromMaybeArray(threadOrLogs)
673
+ DEBUG(`queryAndMap<${thread.nameAndSizeUntracked}>`, { patternOrPatterns, variables, map: mapDef })
674
+ const queryResult = query(thread, patternOrPatterns)
675
+ return mapQueryResultWith(queryResult, mapDef)
676
+ }, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'queryAndMap', thread, pattern }) })
677
+
678
+ /** Live variant: query and map results, returns SubscribableArray that updates reactively. */
679
+ export const liveQueryAndMap = memoizedFn('liveQueryAndMap', function liveQueryAndMap<R>(
680
+ thread: Thread,
681
+ patternOrPatterns: Parameters<typeof liveQuery>[1],
682
+ mapDef: string | (Partial<{ [key in keyof SearchContext]: string }>) | ((record: SearchContext) => R),
683
+ ) {
684
+ DEBUG(`liveQueryAndMap<${thread.nameAndSizeUntracked}>`, { patternOrPatterns, map: mapDef })
685
+ const live = liveQuery(thread, patternOrPatterns)
686
+
687
+ function computeAll(): R[] {
688
+ const snapshot = new QueryResult(live.nodes)
689
+ return mapQueryResultWith(snapshot, mapDef) as R[]
690
+ }
691
+
692
+ const result = new SubscribableArrayImpl<R>(
693
+ computeAll(),
694
+ () => live.subscribe(() => {
695
+ result._reset(computeAll())
696
+ }, 'derived'),
697
+ )
698
+ return result
699
+ }, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveQueryAndMap', thread, pattern }) })
700
+
701
+ /** One-off: query entity attributes. Returns Record or null. Requires current-state thread (LWW). */
702
+ export const queryEntity = memoizedFn('queryEntity', function queryEntity(
703
+ thread: Thread,
704
+ name: string,
705
+ entityID: EntityID,
706
+ attributes: readonly string[],
707
+ ) {
708
+ assertLWW(thread)
709
+ DEBUG(`queryEntity<${thread.nameAndSizeUntracked}>`, entityID, name)
710
+ const filter = makeFilter({ en: entityID, at: prefixAttrs(name, attributes) })
711
+ const filtered = filter(thread.applogs)
712
+ VERBOSE(`queryEntity applogs:`, filtered)
713
+ if (filtered.length === 0) return null
714
+ return Object.fromEntries(
715
+ filtered.map(({ at, vl }) => [at.slice(name.length + 1), vl]),
716
+ )
717
+ }, {
718
+ argsDebugName: (thread, name, entityID) => createDebugName({ caller: 'queryEntity', thread, args: { name, entityID } }),
719
+ })
720
+
721
+ /** Live variant: returns Subscribable that updates when entity attributes change. Requires current-state thread (LWW). */
722
+ export const liveQueryEntity = memoizedFn('liveQueryEntity', function liveQueryEntity(
723
+ thread: Thread,
724
+ name: string,
725
+ entityID: EntityID,
726
+ attributes: readonly string[],
727
+ ) {
728
+ assertLWW(thread)
729
+ DEBUG(`liveQueryEntity<${thread.nameAndSizeUntracked}>`, entityID, name)
730
+ const filtered = rollingFilter(thread, { en: entityID, at: prefixAttrs(name, attributes) })
731
+
732
+ function compute() {
733
+ if (filtered.isEmpty) return null
734
+ return Object.fromEntries(
735
+ filtered.map(({ at, vl }) => [at.slice(name.length + 1), vl]),
736
+ )
737
+ }
738
+
739
+ const result = new SubscribableImpl<Record<string, ApplogValue> | null>(
740
+ compute(),
741
+ () => filtered.subscribe(() => {
742
+ result._set(compute())
743
+ }, 'derived'),
744
+ )
745
+ return result
746
+ }, {
747
+ argsDebugName: (thread, name, entityID) => createDebugName({ caller: 'liveQueryEntity', thread, args: { name, entityID } }),
748
+ })
749
+
750
+ /** Live single-attribute query. Requires current-state thread (LWW). Returns Subscribable<T | null>. */
751
+ export const liveEntityAt = memoizedFn('liveEntityAt', function liveEntityAt<T extends ApplogValue>(
752
+ thread: Thread,
753
+ entityID: EntityID,
754
+ at: string,
755
+ ) {
756
+ assertLWW(thread)
757
+ DEBUG(`liveEntityAt<${thread.nameAndSizeUntracked}>`, entityID, at)
758
+ const filtered = rollingFilter(thread, { en: entityID, at })
759
+
760
+ function compute(): T | null {
761
+ if (filtered.isEmpty) return null
762
+ return filtered.applogs[filtered.applogs.length - 1].vl as T
763
+ }
764
+
765
+ const result = new SubscribableImpl<T | null>(
766
+ compute(),
767
+ () => filtered.subscribe(() => {
768
+ result._set(compute())
769
+ }, 'derived'),
770
+ )
771
+ return result
772
+ }, {
773
+ argsDebugName: (thread, entityID, at) => createDebugName({ caller: 'liveEntityAt', thread, args: { entityID, at } }),
774
+ })
775
+
776
+ export const agentsOfThread = memoizedFn('agentsOfThread', function agentsOfThread(
777
+ thread: Thread,
778
+ ) {
779
+ DEBUG(`agentsOfThread<${thread.nameAndSizeUntracked}>`)
780
+
781
+ const mapped = new Map<string, number>()
782
+ const onEvent = (event: ThreadEvent) => {
783
+ for (const log of (isInitEvent(event) ? event.init : event.added)) {
784
+ const prev = mapped.get(log.ag) ?? 0
785
+ mapped.set(log.ag, prev + 1)
786
+ }
787
+ for (const log of (!isInitEvent(event) && event.removed || [])) {
788
+ const prev = mapped.get(log.ag)
789
+ if (!prev || prev < 1) throw ERROR(`[agentsOfThread] number is now negative`, { log, event, mapped, prev })
790
+ mapped.set(log.ag, prev - 1)
791
+ }
792
+ LOG(`agentsOfThread<${thread.nameAndSizeUntracked}> processed event`, { event, mapped })
793
+ }
794
+
795
+ onEvent({ init: thread.applogs })
796
+ thread.subscribe(onEvent, 'derived')
797
+ // TODO: cleanup via ref-counted disposal when no longer needed
798
+
799
+ return mapped
800
+ })
801
+
802
+ export const entityOverlap = memoizedFn('entityOverlap', function entityOverlapCount(
803
+ threadA: Thread,
804
+ threadB: Thread,
805
+ ) {
806
+ LOG(`entityOverlap<${threadA.nameAndSizeUntracked}, ${threadB.nameAndSizeUntracked}>`)
807
+
808
+ // Compute once — snapshot, not reactive (TODO: migrate to Subscribable)
809
+ const entitiesA = new Set(threadA.map(log => log.en))
810
+ const entitiesB = new Set(threadB.map(log => log.en))
811
+ return [...entitiesA].filter(en => entitiesB.has(en))
812
+ })
813
+
814
+ export const entityOverlapMap = function entityOverlapMap(
815
+ threadA: Thread,
816
+ threadB: Thread,
817
+ threadAName = 'incoming',
818
+ threadBName = 'current',
819
+ ) {
820
+ const useInferredVM = (en, thread: Thread) => en
821
+ const overlapping = entityOverlap(threadA, threadB)
822
+ const mapped = new Map()
823
+ overlapping.forEach(eachEntityID => (
824
+ mapped.set(eachEntityID, {
825
+ [threadAName]: useInferredVM(eachEntityID, threadA),
826
+ [threadBName]: useInferredVM(eachEntityID, threadB),
827
+ })
828
+ ))
829
+ }
830
+
831
+ export const entityOverlapCount = memoizedFn(
832
+ 'entityOverlapCount',
833
+ function entityOverlapCount(threadA: Thread, threadB: Thread) {
834
+ return entityOverlap(threadA, threadB).length
835
+ },
836
+ )
837
+
838
+ /** Live variant: entity overlap count as Subscribable<number>. */
839
+ export const liveEntityOverlapCount = memoizedFn(
840
+ 'liveEntityOverlapCount',
841
+ function liveEntityOverlapCount(threadA: Thread, threadB: Thread) {
842
+ function compute() {
843
+ const entitiesA = new Set(threadA.map(log => log.en))
844
+ const entitiesB = new Set(threadB.map(log => log.en))
845
+ return [...entitiesA].filter(en => entitiesB.has(en)).length
846
+ }
847
+
848
+ const result = new SubscribableImpl<number>(
849
+ compute(),
850
+ () => {
851
+ const unsub1 = threadA.subscribe(() => result._set(compute()), 'derived')
852
+ const unsub2 = threadB.subscribe(() => result._set(compute()), 'derived')
853
+ return () => { unsub1(); unsub2() }
854
+ },
855
+ )
856
+ return result
857
+ },
858
+ )
859
+
860
+ export const querySingle = memoizedFn('querySingle', function querySingle(
861
+ threadOrLogs: Thread | Applog[],
862
+ patternOrPatterns: Parameters<typeof query>[1],
863
+ variables: SearchContext = {},
864
+ ) {
865
+ const result = query(threadOrLogs, patternOrPatterns, variables)
866
+ // Snapshot — not reactive (TODO: migrate to Subscribable<Applog | null>)
867
+ if (result.isEmpty) return null
868
+ if (result.size > 1) throw ERROR(`[querySingle] got`, result.size, `results:`, result)
869
+ const logsOfThisNode = result.nodes[0].logsOfThisNode
870
+ if (logsOfThisNode.size != 1) throw ERROR(`[querySingle] single result, but got`, logsOfThisNode.size, `logs:`, logsOfThisNode.applogs)
871
+ return logsOfThisNode.applogs[0]
872
+ }, {
873
+ argsDebugName: (thread, pattern) => createDebugName({ caller: 'querySingle', thread, pattern }),
874
+ })
875
+
876
+ export const querySingleAndMap = memoizedFn(
877
+ 'querySingleAndMap',
878
+ function querySingleAndMap<MAP extends (keyof Applog | (Partial<{ [key in keyof Applog]: string }>))>(
879
+ threadOrLogs: Thread | Applog[],
880
+ patternOrPatterns: Parameters<typeof query>[1],
881
+ mapDef: MAP,
882
+ variables: SearchContext = {},
883
+ ) {
884
+ const log = querySingle(threadOrLogs, patternOrPatterns, variables)
885
+ // Snapshot — not reactive (TODO: migrate to Subscribable<T>)
886
+ if (!log) return undefined
887
+ if (typeof mapDef === 'string') {
888
+ return log[mapDef as string]
889
+ } else {
890
+ return createObjMapper(mapDef)(log)
891
+ }
892
+ },
893
+ {
894
+ argsDebugName: (thread, pattern) => createDebugName({ caller: 'querySingleAndMap', thread, pattern }),
895
+ },
896
+ )
897
+
898
+ /** Live variant: querySingle returning Subscribable<Applog | null>. */
899
+ export const liveQuerySingle = memoizedFn('liveQuerySingle', function liveQuerySingle(
900
+ thread: Thread,
901
+ patternOrPatterns: Parameters<typeof liveQuery>[1],
902
+ ) {
903
+ DEBUG(`liveQuerySingle<${thread.nameAndSizeUntracked}>`)
904
+ const live = liveQuery(thread, patternOrPatterns)
905
+
906
+ function compute(): Applog | null {
907
+ if (live.isEmpty) return null
908
+ if (live.size > 1) throw ERROR(`[liveQuerySingle] got`, live.size, `results`)
909
+ const logsOfThisNode = live.nodes[0].logsOfThisNode
910
+ if (logsOfThisNode.size !== 1) throw ERROR(`[liveQuerySingle] single result, but got`, logsOfThisNode.size, `logs`)
911
+ return logsOfThisNode.applogs[0]
912
+ }
913
+
914
+ const result = new SubscribableImpl<Applog | null>(
915
+ compute(),
916
+ () => live.subscribe(() => {
917
+ result._set(compute())
918
+ }),
919
+ )
920
+ return result
921
+ }, {
922
+ argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveQuerySingle', thread, pattern }),
923
+ })
924
+
925
+ /** Live variant: querySingleAndMap returning Subscribable<T | undefined>. */
926
+ export const liveQuerySingleAndMap = memoizedFn(
927
+ 'liveQuerySingleAndMap',
928
+ function liveQuerySingleAndMap<MAP extends (keyof Applog | (Partial<{ [key in keyof Applog]: string }>))>(
929
+ thread: Thread,
930
+ patternOrPatterns: Parameters<typeof liveQuery>[1],
931
+ mapDef: MAP,
932
+ ) {
933
+ DEBUG(`liveQuerySingleAndMap<${thread.nameAndSizeUntracked}>`)
934
+ const liveSingle = liveQuerySingle(thread, patternOrPatterns)
935
+
936
+ function compute() {
937
+ const log = liveSingle.value
938
+ if (!log) return undefined
939
+ if (typeof mapDef === 'string') {
940
+ return log[mapDef as string]
941
+ } else {
942
+ return createObjMapper(mapDef)(log)
943
+ }
944
+ }
945
+
946
+ const result = new SubscribableImpl<any>(
947
+ compute(),
948
+ () => liveSingle.subscribe(() => {
949
+ result._set(compute())
950
+ }),
951
+ )
952
+ return result
953
+ },
954
+ {
955
+ argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveQuerySingleAndMap', thread, pattern }),
956
+ },
957
+ )
958
+
959
+ /////////////
960
+ // HELPERS //
961
+ /////////////
962
+
963
+ /** Create a single-applog mapper function from a mapDef */
964
+ export function makeApplogMapper<R>(
965
+ mapDef: (keyof Applog) | (Partial<{ [key in keyof Applog]: string }>) | ((applog: Applog) => R),
966
+ ): (applog: Applog) => R {
967
+ if (typeof mapDef === 'function') {
968
+ return mapDef as (applog: Applog) => R
969
+ } else if (typeof mapDef === 'string') {
970
+ return (log: Applog) => log[mapDef] as R
971
+ } else {
972
+ return createObjMapper(mapDef) as (applog: Applog) => R
973
+ }
974
+ }
975
+
976
+ /** Map an array of applogs using a mapDef */
977
+ export function mapApplogsWith<R>(
978
+ applogs: readonly Applog[],
979
+ mapDef: (keyof Applog) | (Partial<{ [key in keyof Applog]: string }>) | ((applog: Applog) => R),
980
+ ) {
981
+ return applogs.map(makeApplogMapper(mapDef))
982
+ }
983
+
984
+ export const mapThreadWith = function filterAndMapGetterFx<R>(
985
+ thread: Thread,
986
+ mapDef: (keyof Applog) | (Partial<{ [key in keyof Applog]: string }>) | ((applog: Applog) => R),
987
+ ) {
988
+ return mapApplogsWith(thread.applogs, mapDef)
989
+ }
990
+ export const mapQueryResultWith = function filterAndMapGetterFx<R>(
991
+ queryResult: QueryResult,
992
+ mapDef: string | (Partial<{ [key in keyof SearchContext]: string }>) | ((record: SearchContext) => R),
993
+ ) {
994
+ if (typeof mapDef === 'function') {
995
+ return queryResult.records.map(mapDef)
996
+ } else if (typeof mapDef === 'string') {
997
+ return queryResult.nodes.map((node) => {
998
+ if (!Object.hasOwn(node.record, mapDef)) {
999
+ if (node.logsOfThisNode.size !== 1) {
1000
+ throw ERROR(`not sure what to map (it's not a var and a result node log count of ${node.logsOfThisNode.size})`)
1001
+ }
1002
+ return node.logsOfThisNode.firstLog[mapDef]
1003
+ }
1004
+ return node.record[mapDef]
1005
+ })
1006
+ } else {
1007
+ return queryResult.nodes.map((node) => {
1008
+ return createObjMapper(mapDef)(node.record)
1009
+ })
1010
+ }
1011
+ }
1012
+ /**
1013
+ * Map Applog to custom named record, e.g.:
1014
+ * { en: 'movieID', vl: 'movieName' }
1015
+ * will map the applog to { movieID: .., movieName: .. }
1016
+ */
1017
+ export function createObjMapper<FROM extends string, TO extends string>(applogFieldMap: Partial<{ [key in FROM]: TO }>) {
1018
+ return (applog: { [key in FROM]: any }) => {
1019
+ return Object.entries(applogFieldMap).reduce((acc, [key, value]) => {
1020
+ acc[value as TO] = applog[key]
1021
+ return acc
1022
+ }, {} as Partial<{ [key in TO]: ApplogValue }>)
1023
+ }
1024
+ }
1025
+
1026
+ export function startsWith(str: string) {
1027
+ return (value) => value.startsWith(str)
1028
+ }
1029
+
1030
+ export function prefixAttrs(prefix: string, attrs: readonly string[]) {
1031
+ return attrs.map(at => prefixAt(prefix, at))
1032
+ }
1033
+ export function prefixAt(prefix: string, attr: string) {
1034
+ return `${prefix}/${attr}`
1035
+ }
1036
+ export function threadFromMaybeArray(threadOrLogs: Thread | Applog[], name?: string) {
1037
+ if (!Array.isArray(threadOrLogs)) {
1038
+ return threadOrLogs
1039
+ }
1040
+ return ThreadInMemory.fromArray(threadOrLogs, name || `threadFromArray[${threadOrLogs.length}]`, true)
1041
+ }
1042
+ export function withTimeout<R>(timeoutMilliseconds: number, func: () => R) {
1043
+ if (globalQueryTimeoutTime) throw ERROR(`Nested timeout not supported`)
1044
+ globalQueryTimeoutTime = performance.now() + timeoutMilliseconds
1045
+ try {
1046
+ return func()
1047
+ } finally {
1048
+ globalQueryTimeoutTime = null
1049
+ }
1050
+ }
1051
+ export function throwOnTimeout() {
1052
+ if (globalQueryTimeoutTime == null) return
1053
+ if (performance.now() >= globalQueryTimeoutTime) {
1054
+ throw new QueryTimeoutError(globalQueryTimeoutTime)
1055
+ }
1056
+ }
1057
+ class QueryTimeoutError extends Error {
1058
+ constructor(message: string) {
1059
+ super(message)
1060
+ }
1061
+ }