@wovin/core 0.0.0-ciao-mobx-955482e8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/applog/applog-helpers.d.ts +47 -0
- package/dist/applog/applog-helpers.d.ts.map +1 -0
- package/dist/applog/applog-utils.d.ts +57 -0
- package/dist/applog/applog-utils.d.ts.map +1 -0
- package/dist/applog/datom-types.d.ts +128 -0
- package/dist/applog/datom-types.d.ts.map +1 -0
- package/dist/applog.d.ts +4 -0
- package/dist/applog.d.ts.map +1 -0
- package/dist/applog.js +101 -0
- package/dist/applog.js.map +1 -0
- package/dist/blockstore/index.d.ts +21 -0
- package/dist/blockstore/index.d.ts.map +1 -0
- package/dist/blockstore.d.ts +2 -0
- package/dist/blockstore.d.ts.map +1 -0
- package/dist/blockstore.js +24 -0
- package/dist/blockstore.js.map +1 -0
- package/dist/chunk-6MQKRL6W.js +86 -0
- package/dist/chunk-6MQKRL6W.js.map +1 -0
- package/dist/chunk-7MW34UEO.js +40 -0
- package/dist/chunk-7MW34UEO.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-7Z5YDQKK.js.map +1 -0
- package/dist/chunk-CY4NLISM.js +144 -0
- package/dist/chunk-CY4NLISM.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/chunk-E46VTKTZ.js.map +1 -0
- package/dist/chunk-O43W7UW6.js +434 -0
- package/dist/chunk-O43W7UW6.js.map +1 -0
- package/dist/chunk-XIQSYEV3.js +1604 -0
- package/dist/chunk-XIQSYEV3.js.map +1 -0
- package/dist/chunk-XVGW4QC3.js +55 -0
- package/dist/chunk-XVGW4QC3.js.map +1 -0
- package/dist/chunk-YDAKBU6Q.js +9 -0
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/chunk-ZXCJRYD7.js +883 -0
- package/dist/chunk-ZXCJRYD7.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/ipfs/car.d.ts +59 -0
- package/dist/ipfs/car.d.ts.map +1 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
- package/dist/ipfs/ipfs-utils.d.ts +35 -0
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
- package/dist/ipfs.d.ts +4 -0
- package/dist/ipfs.d.ts.map +1 -0
- package/dist/ipfs.js +60 -0
- package/dist/ipfs.js.map +1 -0
- package/dist/ipns/ipns-record.d.ts +34 -0
- package/dist/ipns/ipns-record.d.ts.map +1 -0
- package/dist/ipns.d.ts +2 -0
- package/dist/ipns.d.ts.map +1 -0
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/connector.d.ts +9 -0
- package/dist/pubsub/connector.d.ts.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +14 -0
- package/dist/pubsub/pub-pull.d.ts.map +1 -0
- package/dist/pubsub/pubsub-types.d.ts +72 -0
- package/dist/pubsub/pubsub-types.d.ts.map +1 -0
- package/dist/pubsub/snap-push.d.ts +41 -0
- package/dist/pubsub/snap-push.d.ts.map +1 -0
- package/dist/pubsub/ucan-example.d.ts +3 -0
- package/dist/pubsub/ucan-example.d.ts.map +1 -0
- package/dist/pubsub/ucan.d.ts +16 -0
- package/dist/pubsub/ucan.d.ts.map +1 -0
- package/dist/pubsub.d.ts +5 -0
- package/dist/pubsub.d.ts.map +1 -0
- package/dist/pubsub.js +31 -0
- package/dist/pubsub.js.map +1 -0
- package/dist/query/basic.d.ts +105 -0
- package/dist/query/basic.d.ts.map +1 -0
- package/dist/query/divergences.d.ts +12 -0
- package/dist/query/divergences.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +4 -0
- package/dist/query/matchers.d.ts.map +1 -0
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/query-steps.d.ts +4 -0
- package/dist/query/query-steps.d.ts.map +1 -0
- package/dist/query/situations.d.ts +80 -0
- package/dist/query/situations.d.ts.map +1 -0
- package/dist/query/subscribable.d.ts +102 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +70 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +108 -0
- package/dist/query.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -0
- package/dist/retrieve/update-thread.d.ts +64 -0
- package/dist/retrieve/update-thread.d.ts.map +1 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +14 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/thread/basic.d.ts +60 -0
- package/dist/thread/basic.d.ts.map +1 -0
- package/dist/thread/filters.d.ts +47 -0
- package/dist/thread/filters.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +31 -0
- package/dist/thread/mapped.d.ts.map +1 -0
- package/dist/thread/utils.d.ts +23 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/writeable.d.ts +41 -0
- package/dist/thread/writeable.d.ts.map +1 -0
- package/dist/thread.d.ts +6 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +54 -0
- package/dist/thread.js.map +1 -0
- package/dist/types/typescript-utils.d.ts +34 -0
- package/dist/types/typescript-utils.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/package.json +110 -0
- package/src/applog/applog-helpers.ts +150 -0
- package/src/applog/applog-utils.ts +398 -0
- package/src/applog/datom-types.ts +148 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +277 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/basic.ts +1061 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/matchers.ts +8 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +536 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +225 -0
- package/src/query/types.ts +155 -0
- package/src/query.ts +7 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +175 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +234 -0
- package/src/thread/mapped.ts +166 -0
- package/src/thread/utils.ts +146 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +5 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
|
@@ -0,0 +1,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
|
+
}
|