@wovin/core 0.3.3 → 0.3.4
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/dist/applog/applog-utils.d.ts +12 -1
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog.js +5 -1
- package/dist/{chunk-N5QPZNKD.js → chunk-523IW54O.js} +3 -3
- package/dist/{chunk-4MKPGQIM.js → chunk-7VHIXHNF.js} +97 -62
- package/dist/chunk-7VHIXHNF.js.map +1 -0
- package/dist/{chunk-H4YVJKB7.js → chunk-ERVSWD63.js} +2 -2
- package/dist/{chunk-N7SEGHU4.js → chunk-GQDLPZEZ.js} +33 -24
- package/dist/chunk-GQDLPZEZ.js.map +1 -0
- package/dist/{chunk-6CSJTSQP.js → chunk-OB7M55WA.js} +3 -3
- package/dist/{chunk-H3JNNTVP.js → chunk-PTZJ7LVV.js} +27 -6
- package/dist/chunk-PTZJ7LVV.js.map +1 -0
- package/dist/{chunk-BIYQEX3N.js → chunk-XEUQDEA4.js} +2 -2
- package/dist/index.js +11 -7
- package/dist/ipfs.js +4 -4
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-watcher.d.ts.map +1 -1
- package/dist/ipns.js +6 -8
- package/dist/ipns.js.map +1 -1
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub.js +4 -4
- package/dist/query/basic.d.ts.map +1 -1
- package/dist/query.js +3 -3
- package/dist/retrieve.js +4 -4
- package/dist/thread/basic.d.ts.map +1 -1
- package/dist/thread/mapped.d.ts.map +1 -1
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread.js +1 -1
- package/dist/viewmodel/index.js +2 -2
- package/package.json +1 -1
- package/src/applog/applog-utils.ts +33 -1
- package/src/ipns/ipns-record.test.ts +72 -0
- package/src/ipns/ipns-record.ts +23 -8
- package/src/ipns/ipns-watcher.ts +10 -11
- package/src/pubsub/snap-push.ts +25 -3
- package/src/query/basic.ts +124 -81
- package/src/thread/basic.ts +21 -2
- package/src/thread/mapped.ts +12 -4
- package/src/thread/utils.ts +5 -5
- package/dist/chunk-4MKPGQIM.js.map +0 -1
- package/dist/chunk-H3JNNTVP.js.map +0 -1
- package/dist/chunk-N7SEGHU4.js.map +0 -1
- /package/dist/{chunk-N5QPZNKD.js.map → chunk-523IW54O.js.map} +0 -0
- /package/dist/{chunk-H4YVJKB7.js.map → chunk-ERVSWD63.js.map} +0 -0
- /package/dist/{chunk-6CSJTSQP.js.map → chunk-OB7M55WA.js.map} +0 -0
- /package/dist/{chunk-BIYQEX3N.js.map → chunk-XEUQDEA4.js.map} +0 -0
package/src/ipns/ipns-record.ts
CHANGED
|
@@ -95,8 +95,25 @@ export async function resolveIPNSSequence(
|
|
|
95
95
|
return 0n
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/**
|
|
99
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Regex to detect Kubo IPNS sequence conflict in error messages.
|
|
100
|
+
*
|
|
101
|
+
* Matches messages like:
|
|
102
|
+
* "existing IPNS record has sequence 406 >= new record sequence 406"
|
|
103
|
+
*
|
|
104
|
+
* The operator between the two sequence numbers is matched as an opaque
|
|
105
|
+
* non-whitespace token (`\S{1,12}`) on purpose: Go's JSON encoder HTML-escapes
|
|
106
|
+
* `>` to `>`, so the string that actually reaches us through the storage
|
|
107
|
+
* proxy reads `... sequence 406 >= new record sequence 406 ...`. A regex
|
|
108
|
+
* hard-coded to `>=` (or even a non-digit bridge like `\D+`, which trips over
|
|
109
|
+
* the digits inside `>`) would silently fail to match, the
|
|
110
|
+
* sequence-coherence retry below would never fire, and the publish would
|
|
111
|
+
* surface the generic "All IPNS publish targets failed" error instead of
|
|
112
|
+
* self-healing.
|
|
113
|
+
*
|
|
114
|
+
* Capture 1 = existing (server) sequence, capture 2 = rejected (new) sequence.
|
|
115
|
+
*/
|
|
116
|
+
const SEQ_CONFLICT_RE = /existing IPNS record has sequence (\d+) \S{1,12} new record sequence (\d+)/
|
|
100
117
|
|
|
101
118
|
/**
|
|
102
119
|
* Options for {@link publishIPNSRecord}.
|
|
@@ -145,7 +162,7 @@ export async function publishIPNSRecord(
|
|
|
145
162
|
let sequence: bigint | undefined
|
|
146
163
|
let bumpFloor: bigint | undefined
|
|
147
164
|
|
|
148
|
-
for (let attempt = 0
|
|
165
|
+
for (let attempt = 0;; attempt++) {
|
|
149
166
|
// Compute next sequence on first pass or after a conflict bump
|
|
150
167
|
if (sequence === undefined) {
|
|
151
168
|
sequence = await pickNextSequence(ipnsName, targets)
|
|
@@ -171,9 +188,7 @@ export async function publishIPNSRecord(
|
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
// Check if any failure is a sequence conflict we can recover from
|
|
174
|
-
const conflictReasons = failures.filter(({ r }) =>
|
|
175
|
-
SEQ_CONFLICT_RE.test(typeof r.reason === 'string' ? r.reason : String(r.reason)),
|
|
176
|
-
)
|
|
191
|
+
const conflictReasons = failures.filter(({ r }) => SEQ_CONFLICT_RE.test(typeof r.reason === 'string' ? r.reason : String(r.reason)))
|
|
177
192
|
|
|
178
193
|
if (conflictReasons.length > 0 && attempt < maxBumps) {
|
|
179
194
|
// Parse the highest existing sequence from each conflict error
|
|
@@ -189,7 +204,7 @@ export async function publishIPNSRecord(
|
|
|
189
204
|
if (bump > sequence) {
|
|
190
205
|
console.warn(
|
|
191
206
|
`[publishIPNSRecord] Sequence conflict: server seq ${bump - 1n} >= current ${sequence}, ` +
|
|
192
|
-
|
|
207
|
+
`bumping to ${bump} (attempt ${attempt + 1}/${maxBumps})`,
|
|
193
208
|
)
|
|
194
209
|
sequence = bump
|
|
195
210
|
continue
|
|
@@ -208,7 +223,7 @@ export async function publishIPNSRecord(
|
|
|
208
223
|
if (conflictReasons.length > 0) {
|
|
209
224
|
throw new Error(
|
|
210
225
|
`IPNS sequence conflict not resolved after ${Math.min(attempt + 1, maxBumps)} bumps. ` +
|
|
211
|
-
|
|
226
|
+
`All publish targets failed: ${failures.map(({ r, name }) => `${name}: ${r.reason}`).join('; ')}`,
|
|
212
227
|
)
|
|
213
228
|
}
|
|
214
229
|
throw new Error(
|
package/src/ipns/ipns-watcher.ts
CHANGED
|
@@ -17,21 +17,21 @@ const { WARN, LOG, DEBUG, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-
|
|
|
17
17
|
* these to fail at runtime and handle the error in their `onError` handler.
|
|
18
18
|
*/
|
|
19
19
|
function buildWsUrl(nameBaseUrl: string, name: string) {
|
|
20
|
-
const base = nameBaseUrl.replace(/\/+$/, '').replace(/^http/, 'ws')
|
|
21
|
-
return `${base}/${name}/watch`
|
|
20
|
+
const base = nameBaseUrl.replace(/\/+$/, '').replace(/\/name$/, '').replace(/^http/, 'ws')
|
|
21
|
+
return `${base}/name/${name}/watch`
|
|
22
22
|
}
|
|
23
23
|
function buildHttpUrl(nameBaseUrl: string, name: string) {
|
|
24
|
-
const base = nameBaseUrl.replace(/\/+$/, '')
|
|
25
|
-
return `${base}/${name}`
|
|
24
|
+
const base = nameBaseUrl.replace(/\/+$/, '').replace(/\/name$/, '')
|
|
25
|
+
return `${base}/name/${name}`
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function requireNameBaseUrl(nameBaseUrl: string | undefined, fn: string): string {
|
|
29
29
|
if (!nameBaseUrl) {
|
|
30
30
|
throw new Error(
|
|
31
31
|
`[${fn}] nameBaseUrl is required. The legacy default `
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
+ `https://name.web3.storage is shut down. Pass the base URL of a naming `
|
|
33
|
+
+ `service that supports WebSocket subscriptions to /name/<ipns>/watch `
|
|
34
|
+
+ `and HTTP GET on /name/<ipns>.`,
|
|
35
35
|
)
|
|
36
36
|
}
|
|
37
37
|
return nameBaseUrl
|
|
@@ -261,8 +261,7 @@ export class IpnsWatcher {
|
|
|
261
261
|
// Check for current state on first connect if requested
|
|
262
262
|
if (this.isFirstConnect && (options.fetchInitialState ?? false)) {
|
|
263
263
|
this.checkForMissedUpdates()
|
|
264
|
-
}
|
|
265
|
-
// Check for missed updates on reconnect
|
|
264
|
+
} // Check for missed updates on reconnect
|
|
266
265
|
else if (!this.isFirstConnect && (options.catchUpOnReconnect ?? true)) {
|
|
267
266
|
this.checkForMissedUpdates()
|
|
268
267
|
}
|
|
@@ -449,8 +448,8 @@ export class IpnsWatcher {
|
|
|
449
448
|
const silenceDuration = this.lastMessageAt
|
|
450
449
|
? now.getTime() - this.lastMessageAt.getTime()
|
|
451
450
|
: this.connectedAt
|
|
452
|
-
|
|
453
|
-
|
|
451
|
+
? now.getTime() - this.connectedAt.getTime()
|
|
452
|
+
: 0
|
|
454
453
|
|
|
455
454
|
const staleInfo: StaleConnectionInfo = {
|
|
456
455
|
connectedAt: this.connectedAt ?? now,
|
package/src/pubsub/snap-push.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Logger } from 'besonders-logger'
|
|
|
3
3
|
import { CID } from 'multiformats/cid'
|
|
4
4
|
import stringify from 'safe-stable-stringify'
|
|
5
5
|
import { ensureTsPvAndFinalizeApplog } from '../applog/applog-helpers.ts'
|
|
6
|
+
import { applogsSummary } from '../applog/applog-utils.ts'
|
|
6
7
|
import type {
|
|
7
8
|
Applog,
|
|
8
9
|
ApplogArrayMaybeEncrypted,
|
|
@@ -159,10 +160,31 @@ export async function prepareSnapshotForPush(
|
|
|
159
160
|
const applogsToEncode = keepTruthy(maybeEncryptedApplogs)
|
|
160
161
|
const infologsToEncode = keepTruthy(infoLogs)
|
|
161
162
|
if (!applogsToEncode.length) {
|
|
162
|
-
|
|
163
|
+
// all applogs were filtered out as falsy — show raw→kept counts + a sample so we can see what came in
|
|
164
|
+
throw ERROR('no valid applogs', {
|
|
165
|
+
agent: agent.did,
|
|
166
|
+
prevSnapCID: prevSnapCID?.toString(),
|
|
167
|
+
counts: {
|
|
168
|
+
rawApplogs: maybeEncryptedApplogs.length,
|
|
169
|
+
keptApplogs: applogsToEncode.length,
|
|
170
|
+
rawInfo: infoLogs.length,
|
|
171
|
+
keptInfo: infologsToEncode.length,
|
|
172
|
+
},
|
|
173
|
+
rawApplogsSample: applogsSummary(maybeEncryptedApplogs),
|
|
174
|
+
})
|
|
163
175
|
}
|
|
164
176
|
if (!infologsToEncode.length) {
|
|
165
|
-
throw ERROR('no valid infologs', {
|
|
177
|
+
throw ERROR('no valid infologs', {
|
|
178
|
+
agent: agent.did,
|
|
179
|
+
prevSnapCID: prevSnapCID?.toString(),
|
|
180
|
+
counts: {
|
|
181
|
+
rawApplogs: maybeEncryptedApplogs.length,
|
|
182
|
+
keptApplogs: applogsToEncode.length,
|
|
183
|
+
rawInfo: infoLogs.length,
|
|
184
|
+
keptInfo: infologsToEncode.length,
|
|
185
|
+
},
|
|
186
|
+
rawInfoSample: applogsSummary(infoLogs),
|
|
187
|
+
})
|
|
166
188
|
}
|
|
167
189
|
const encodedSnapshot = await encodeSnapshotAsCar(agent, applogsToEncode, infologsToEncode, prevSnapCID, prevCounter)
|
|
168
190
|
DEBUG('inPrepareSnapshotForPush', { encodedSnapshot })
|
|
@@ -253,7 +275,7 @@ export async function encodeSnapshotApplogsAsCar(
|
|
|
253
275
|
applogs: ApplogArrayMaybeEncryptedRO,
|
|
254
276
|
) {
|
|
255
277
|
const encoded = await encodeApplogsAsIPLD(applogs)
|
|
256
|
-
if (!encoded) throw ERROR('invalid applogs cannot continue', {
|
|
278
|
+
if (!encoded) throw ERROR('invalid applogs cannot continue', { encoded, applogs: applogsSummary(applogs) })
|
|
257
279
|
const { cids, encodedApplogs } = encoded
|
|
258
280
|
const root = { applogs: cids }
|
|
259
281
|
const encodedRoot = await encodeBlockOriginal(root)
|
package/src/query/basic.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AgentHash,
|
|
3
|
+
Applog,
|
|
4
|
+
ApplogValue,
|
|
5
|
+
CidString,
|
|
6
|
+
DatalogQueryPattern,
|
|
7
|
+
EntityID,
|
|
8
|
+
SearchContext,
|
|
9
|
+
ValueOrMatcher,
|
|
10
|
+
} from '../applog/datom-types.ts'
|
|
2
11
|
|
|
3
12
|
import { Logger } from 'besonders-logger'
|
|
4
13
|
|
|
5
14
|
import { isEmpty } from 'lodash-es'
|
|
6
15
|
import stringify from 'safe-stable-stringify'
|
|
7
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
applogsSummary,
|
|
18
|
+
isLaterByTsAndPv,
|
|
19
|
+
isoDateStrCompare,
|
|
20
|
+
isVariable,
|
|
21
|
+
resolveOrRemoveVariables,
|
|
22
|
+
sortApplogsByTs,
|
|
23
|
+
} from '../applog/applog-utils.ts'
|
|
8
24
|
import { createDebugName } from '../utils/debug-name.ts'
|
|
9
25
|
import { isInitEvent, StaticThread, Thread, ThreadEvent } from '../thread/basic.ts'
|
|
10
26
|
import { hasFilter, makeFilter, rollingFilter, rollingMapper, ThreadOnlyCurrent } from '../thread/filters.ts'
|
|
@@ -239,10 +255,8 @@ export const withoutDeleted = memoizedFn('withoutDeleted', function withoutDelet
|
|
|
239
255
|
// Pass-through: a parent log goes through to subscribers iff the entity
|
|
240
256
|
// wasn't hidden before AND isn't hidden now. Transitions are owned by the
|
|
241
257
|
// synthetic streams above.
|
|
242
|
-
const passAdded = delta.added.filter(log =>
|
|
243
|
-
|
|
244
|
-
const passRemoved = delta.removed?.filter(log =>
|
|
245
|
-
!hiddenBefore.has(log.en)) ?? []
|
|
258
|
+
const passAdded = delta.added.filter(log => !hiddenBefore.has(log.en) && !isHidden(log.en))
|
|
259
|
+
const passRemoved = delta.removed?.filter(log => !hiddenBefore.has(log.en)) ?? []
|
|
246
260
|
|
|
247
261
|
return {
|
|
248
262
|
added: [...passAdded, ...syntheticAdditions],
|
|
@@ -324,21 +338,28 @@ export function queryStepOnce(
|
|
|
324
338
|
const matchingLogs = filter(thread.applogs)
|
|
325
339
|
const varMapper = createObjMapper(variablesToFill)
|
|
326
340
|
|
|
327
|
-
const nodes = matchingLogs.map(log =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
341
|
+
const nodes = matchingLogs.map(log =>
|
|
342
|
+
makeQueryNode(
|
|
343
|
+
log,
|
|
344
|
+
node,
|
|
345
|
+
varMapper,
|
|
346
|
+
createDebugName({
|
|
347
|
+
caller: 'QueryNode',
|
|
348
|
+
thread,
|
|
349
|
+
pattern: `${stringify(Object.assign({}, node?.variables, varMapper(log)))}@${stringify(patternWithResolvedVars)}`,
|
|
350
|
+
}),
|
|
351
|
+
)
|
|
352
|
+
)
|
|
335
353
|
|
|
336
354
|
if (VERBOSE.isEnabled) VERBOSE(`[queryStepOnce.doQuery] nodes:`, nodes.map(n => n.variables))
|
|
337
355
|
if (opts.debug) {
|
|
338
|
-
LOG(
|
|
339
|
-
|
|
340
|
-
thread
|
|
341
|
-
|
|
356
|
+
LOG(
|
|
357
|
+
`[queryStepOnce] step result:`,
|
|
358
|
+
nodes.map(({ variables, logsOfThisNode: thread }) => ({
|
|
359
|
+
variables,
|
|
360
|
+
thread,
|
|
361
|
+
})),
|
|
362
|
+
)
|
|
342
363
|
}
|
|
343
364
|
|
|
344
365
|
return nodes
|
|
@@ -404,7 +425,9 @@ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
|
|
|
404
425
|
|
|
405
426
|
function makeNode(log: Applog): QueryNode {
|
|
406
427
|
return makeQueryNode(
|
|
407
|
-
log,
|
|
428
|
+
log,
|
|
429
|
+
node,
|
|
430
|
+
varMapper,
|
|
408
431
|
createDebugName({
|
|
409
432
|
caller: 'QueryNode',
|
|
410
433
|
thread: applogsMatchingStatic,
|
|
@@ -418,31 +441,33 @@ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
|
|
|
418
441
|
|
|
419
442
|
if (VERBOSE.isEnabled) VERBOSE(`[liveQueryStep.doQuery] initial nodes:`, initialNodes.map(n => n.variables))
|
|
420
443
|
if (opts.debug) {
|
|
421
|
-
LOG(
|
|
422
|
-
|
|
423
|
-
thread
|
|
424
|
-
|
|
444
|
+
LOG(
|
|
445
|
+
`[liveQueryStep] step result:`,
|
|
446
|
+
initialNodes.map(({ variables, logsOfThisNode: thread }) => ({
|
|
447
|
+
variables,
|
|
448
|
+
thread,
|
|
449
|
+
})),
|
|
450
|
+
)
|
|
425
451
|
}
|
|
426
452
|
|
|
427
453
|
// Upstream subscription activates lazily — only when someone subscribes to us
|
|
428
454
|
const result = new SubscribableArrayImpl<QueryNode>(
|
|
429
455
|
initialNodes,
|
|
430
|
-
() =>
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
removedCids.has(qn.logsOfThisNode.applogs[0]?.cid)
|
|
441
|
-
|
|
442
|
-
|
|
456
|
+
() =>
|
|
457
|
+
applogsMatchingStatic.subscribe((event) => {
|
|
458
|
+
if (isInitEvent(event)) {
|
|
459
|
+
result._reset(event.init.map(makeNode))
|
|
460
|
+
} else {
|
|
461
|
+
if (event.added.length) {
|
|
462
|
+
result._push(...event.added.map(makeNode))
|
|
463
|
+
}
|
|
464
|
+
if (event.removed?.length) {
|
|
465
|
+
const removedCids = new Set(event.removed.map(log => log.cid))
|
|
466
|
+
const toRemove = result.items.filter(qn => removedCids.has(qn.logsOfThisNode.applogs[0]?.cid))
|
|
467
|
+
if (toRemove.length) result._remove(toRemove)
|
|
468
|
+
}
|
|
443
469
|
}
|
|
444
|
-
}
|
|
445
|
-
}, 'derived'),
|
|
470
|
+
}, 'derived'),
|
|
446
471
|
)
|
|
447
472
|
return result
|
|
448
473
|
}
|
|
@@ -466,9 +491,9 @@ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
|
|
|
466
491
|
initialItems,
|
|
467
492
|
() => {
|
|
468
493
|
const subsByInputNode = new Map<QueryNode, {
|
|
469
|
-
inner: SubscribableArray<QueryNode
|
|
470
|
-
unsub: Unsubscribe
|
|
471
|
-
nodes: QueryNode[]
|
|
494
|
+
inner: SubscribableArray<QueryNode>
|
|
495
|
+
unsub: Unsubscribe
|
|
496
|
+
nodes: QueryNode[]
|
|
472
497
|
}>()
|
|
473
498
|
|
|
474
499
|
function wireInner(inputNode: QueryNode, inner: SubscribableArray<QueryNode>): QueryNode[] {
|
|
@@ -521,7 +546,8 @@ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
|
|
|
521
546
|
const prevUnsub = nodeSet.subscribe((event) => {
|
|
522
547
|
if (isArrayInitEvent(event)) {
|
|
523
548
|
for (const [, entry] of subsByInputNode) {
|
|
524
|
-
entry.unsub()
|
|
549
|
+
entry.unsub()
|
|
550
|
+
entry.inner.dispose()
|
|
525
551
|
}
|
|
526
552
|
subsByInputNode.clear()
|
|
527
553
|
const allNodes: QueryNode[] = []
|
|
@@ -550,7 +576,8 @@ export const liveQueryStep = memoizedFn('liveQueryStep', function liveQueryStep(
|
|
|
550
576
|
return () => {
|
|
551
577
|
prevUnsub()
|
|
552
578
|
for (const [, entry] of subsByInputNode) {
|
|
553
|
-
entry.unsub()
|
|
579
|
+
entry.unsub()
|
|
580
|
+
entry.inner.dispose()
|
|
554
581
|
}
|
|
555
582
|
subsByInputNode.clear()
|
|
556
583
|
}
|
|
@@ -688,7 +715,10 @@ export const liveQueryNot = memoizedFn('liveQueryNot', function liveQueryNot(
|
|
|
688
715
|
}
|
|
689
716
|
}, 'derived')
|
|
690
717
|
|
|
691
|
-
return () => {
|
|
718
|
+
return () => {
|
|
719
|
+
threadUnsub()
|
|
720
|
+
upstreamUnsub()
|
|
721
|
+
}
|
|
692
722
|
},
|
|
693
723
|
)
|
|
694
724
|
|
|
@@ -755,27 +785,28 @@ export const liveFilterAndMap = memoizedFn('liveFilterAndMap', function liveFilt
|
|
|
755
785
|
const initial = filtered.applogs.map(mapAndTrack)
|
|
756
786
|
const result = new SubscribableArrayImpl<R>(
|
|
757
787
|
initial,
|
|
758
|
-
() =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
788
|
+
() =>
|
|
789
|
+
filtered.subscribe((event) => {
|
|
790
|
+
if (isInitEvent(event)) {
|
|
791
|
+
cidToMapped.clear()
|
|
792
|
+
result._reset(event.init.map(mapAndTrack))
|
|
793
|
+
} else {
|
|
794
|
+
if (event.added.length) result._push(...event.added.map(mapAndTrack))
|
|
795
|
+
if (event.removed?.length) {
|
|
796
|
+
const toRemove: R[] = []
|
|
797
|
+
for (const log of event.removed) {
|
|
798
|
+
const r = cidToMapped.get(log.cid)
|
|
799
|
+
if (r === undefined) {
|
|
800
|
+
WARN(`[liveFilterAndMap] removed log not in cidToMapped`, { log })
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
803
|
+
cidToMapped.delete(log.cid)
|
|
804
|
+
toRemove.push(r)
|
|
771
805
|
}
|
|
772
|
-
|
|
773
|
-
toRemove.push(r)
|
|
806
|
+
if (toRemove.length) result._remove(toRemove)
|
|
774
807
|
}
|
|
775
|
-
if (toRemove.length) result._remove(toRemove)
|
|
776
808
|
}
|
|
777
|
-
}
|
|
778
|
-
}, 'derived'),
|
|
809
|
+
}, 'derived'),
|
|
779
810
|
)
|
|
780
811
|
return result
|
|
781
812
|
}, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveFilterAndMap', thread, pattern }) })
|
|
@@ -809,9 +840,10 @@ export const liveQueryAndMap = memoizedFn('liveQueryAndMap', function liveQueryA
|
|
|
809
840
|
|
|
810
841
|
const result = new SubscribableArrayImpl<R>(
|
|
811
842
|
computeAll(),
|
|
812
|
-
() =>
|
|
813
|
-
|
|
814
|
-
|
|
843
|
+
() =>
|
|
844
|
+
live.subscribe(() => {
|
|
845
|
+
result._reset(computeAll())
|
|
846
|
+
}, 'derived'),
|
|
815
847
|
)
|
|
816
848
|
return result
|
|
817
849
|
}, { argsDebugName: (thread, pattern) => createDebugName({ caller: 'liveQueryAndMap', thread, pattern }) })
|
|
@@ -856,9 +888,10 @@ export const liveQueryEntity = memoizedFn('liveQueryEntity', function liveQueryE
|
|
|
856
888
|
|
|
857
889
|
const result = new SubscribableImpl<Record<string, ApplogValue> | null>(
|
|
858
890
|
compute(),
|
|
859
|
-
() =>
|
|
860
|
-
|
|
861
|
-
|
|
891
|
+
() =>
|
|
892
|
+
filtered.subscribe(() => {
|
|
893
|
+
result._set(compute())
|
|
894
|
+
}, 'derived'),
|
|
862
895
|
)
|
|
863
896
|
return result
|
|
864
897
|
}, {
|
|
@@ -882,9 +915,10 @@ export const liveEntityAt = memoizedFn('liveEntityAt', function liveEntityAt<T e
|
|
|
882
915
|
|
|
883
916
|
const result = new SubscribableImpl<T | null>(
|
|
884
917
|
compute(),
|
|
885
|
-
() =>
|
|
886
|
-
|
|
887
|
-
|
|
918
|
+
() =>
|
|
919
|
+
filtered.subscribe(() => {
|
|
920
|
+
result._set(compute())
|
|
921
|
+
}, 'derived'),
|
|
888
922
|
)
|
|
889
923
|
return result
|
|
890
924
|
}, {
|
|
@@ -968,7 +1002,10 @@ export const liveEntityOverlapCount = memoizedFn(
|
|
|
968
1002
|
() => {
|
|
969
1003
|
const unsub1 = threadA.subscribe(() => result._set(compute()), 'derived')
|
|
970
1004
|
const unsub2 = threadB.subscribe(() => result._set(compute()), 'derived')
|
|
971
|
-
return () => {
|
|
1005
|
+
return () => {
|
|
1006
|
+
unsub1()
|
|
1007
|
+
unsub2()
|
|
1008
|
+
}
|
|
972
1009
|
},
|
|
973
1010
|
)
|
|
974
1011
|
return result
|
|
@@ -985,7 +1022,9 @@ export const querySingle = memoizedFn('querySingle', function querySingle(
|
|
|
985
1022
|
if (result.isEmpty) return null
|
|
986
1023
|
if (result.size > 1) throw ERROR(`[querySingle] got`, result.size, `results:`, result)
|
|
987
1024
|
const logsOfThisNode = result.nodes[0].logsOfThisNode
|
|
988
|
-
if (logsOfThisNode.size != 1)
|
|
1025
|
+
if (logsOfThisNode.size != 1) {
|
|
1026
|
+
throw ERROR(`[querySingle] single result, but got`, logsOfThisNode.size, `logs:`, applogsSummary(logsOfThisNode.applogs))
|
|
1027
|
+
}
|
|
989
1028
|
return logsOfThisNode.applogs[0]
|
|
990
1029
|
}, {
|
|
991
1030
|
argsDebugName: (thread, pattern) => createDebugName({ caller: 'querySingle', thread, pattern }),
|
|
@@ -1031,9 +1070,10 @@ export const liveQuerySingle = memoizedFn('liveQuerySingle', function liveQueryS
|
|
|
1031
1070
|
|
|
1032
1071
|
const result = new SubscribableImpl<Applog | null>(
|
|
1033
1072
|
compute(),
|
|
1034
|
-
() =>
|
|
1035
|
-
|
|
1036
|
-
|
|
1073
|
+
() =>
|
|
1074
|
+
live.subscribe(() => {
|
|
1075
|
+
result._set(compute())
|
|
1076
|
+
}),
|
|
1037
1077
|
)
|
|
1038
1078
|
return result
|
|
1039
1079
|
}, {
|
|
@@ -1063,9 +1103,10 @@ export const liveQuerySingleAndMap = memoizedFn(
|
|
|
1063
1103
|
|
|
1064
1104
|
const result = new SubscribableImpl<any>(
|
|
1065
1105
|
compute(),
|
|
1066
|
-
() =>
|
|
1067
|
-
|
|
1068
|
-
|
|
1106
|
+
() =>
|
|
1107
|
+
liveSingle.subscribe(() => {
|
|
1108
|
+
result._set(compute())
|
|
1109
|
+
}),
|
|
1069
1110
|
)
|
|
1070
1111
|
return result
|
|
1071
1112
|
},
|
|
@@ -1214,7 +1255,8 @@ function warnIfDisjointQuerySteps(patterns: DatalogQueryPattern[]) {
|
|
|
1214
1255
|
if (stepVars.size === 0) {
|
|
1215
1256
|
WARN(
|
|
1216
1257
|
`[query] Step ${i} has no variables — it produces identical results regardless of previous steps (cartesian product).`,
|
|
1217
|
-
`Patterns:`,
|
|
1258
|
+
`Patterns:`,
|
|
1259
|
+
patterns,
|
|
1218
1260
|
)
|
|
1219
1261
|
continue
|
|
1220
1262
|
}
|
|
@@ -1225,7 +1267,8 @@ function warnIfDisjointQuerySteps(patterns: DatalogQueryPattern[]) {
|
|
|
1225
1267
|
`This produces a cartesian product instead of a join.`,
|
|
1226
1268
|
`Step ${i} variables: {${[...stepVars].join(', ')}}`,
|
|
1227
1269
|
`Reachable from prior steps: {${[...reachable].join(', ')}}`,
|
|
1228
|
-
`Patterns:`,
|
|
1270
|
+
`Patterns:`,
|
|
1271
|
+
patterns,
|
|
1229
1272
|
)
|
|
1230
1273
|
}
|
|
1231
1274
|
for (const v of stepVars) reachable.add(v)
|
package/src/thread/basic.ts
CHANGED
|
@@ -71,7 +71,10 @@ export abstract class Thread implements SubscribableArray<Applog> {
|
|
|
71
71
|
|
|
72
72
|
protected notifySubscribers(event: ThreadEvent) {
|
|
73
73
|
this._version++
|
|
74
|
-
DEBUG(`[thread: ${this.name}] notifying`, this._derivedSubscribers.length, 'derived +', this._subscribers.length, 'subscribers of', {
|
|
74
|
+
DEBUG(`[thread: ${this.name}] notifying`, this._derivedSubscribers.length, 'derived +', this._subscribers.length, 'subscribers of', {
|
|
75
|
+
...event,
|
|
76
|
+
subs: this._subscribers,
|
|
77
|
+
})
|
|
75
78
|
const derived = [...this._derivedSubscribers] // snapshot — safe if a subscriber unsubs during iteration
|
|
76
79
|
for (const subscriber of derived) {
|
|
77
80
|
subscriber(event)
|
|
@@ -83,7 +86,9 @@ export abstract class Thread implements SubscribableArray<Applog> {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// ── SubscribableArray<Applog> ──
|
|
86
|
-
get items(): readonly Applog[] {
|
|
89
|
+
get items(): readonly Applog[] {
|
|
90
|
+
return this._applogs
|
|
91
|
+
}
|
|
87
92
|
|
|
88
93
|
dispose() {
|
|
89
94
|
this._derivedSubscribers.length = 0
|
|
@@ -186,6 +191,20 @@ export abstract class Thread implements SubscribableArray<Applog> {
|
|
|
186
191
|
get hasParents() {
|
|
187
192
|
return !!this.parents?.length
|
|
188
193
|
}
|
|
194
|
+
|
|
195
|
+
// Compact representation for server-side / plain console logging (Node's util.inspect).
|
|
196
|
+
// ⚠ Only consulted by Node — browser devtools ignore this symbol and keep rendering the
|
|
197
|
+
// live, expandable object, so passing `thread` to a logger stays clickable in devtools
|
|
198
|
+
// while no longer splatting every applog + the whole parent chain + subscribers to stdout.
|
|
199
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
200
|
+
// Must never throw — an inspect that throws breaks the very error log it's part of.
|
|
201
|
+
// e.g. `ERROR('falsy applogs of thread', s)` where s._applogs is malformed.
|
|
202
|
+
try {
|
|
203
|
+
return `${this.constructor.name}<${this.nameAndSizeUntracked}${this.filters?.length ? ` |${this.filters.join(',')}` : ''}>`
|
|
204
|
+
} catch {
|
|
205
|
+
return `${this.constructor.name}<${this.name} (size?)>`
|
|
206
|
+
}
|
|
207
|
+
}
|
|
189
208
|
}
|
|
190
209
|
|
|
191
210
|
export const getLogsFromThread = (logsOrThread: ApplogsOrThread) => logsOrThread instanceof Thread ? logsOrThread.applogs : logsOrThread
|
package/src/thread/mapped.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { Logger } from 'besonders-logger'
|
|
2
2
|
import { DebouncedFunc, pull, sortedIndexBy } from 'lodash-es'
|
|
3
3
|
import { Applog, ApplogForInsert } from '../applog/datom-types.ts'
|
|
4
|
+
import { applogsSummary } from '../applog/applog-utils.ts'
|
|
4
5
|
import { arrayIfSingle } from '../types/typescript-utils.ts'
|
|
5
6
|
import { isInitEvent, Thread, ThreadEvent } from './basic.ts'
|
|
6
7
|
|
|
7
8
|
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
8
9
|
|
|
10
|
+
/** Bounded, debuggable one-line summary of a ThreadEvent (init vs added/removed with counts + samples). */
|
|
11
|
+
const threadEventSummary = (e: ThreadEvent) =>
|
|
12
|
+
isInitEvent(e)
|
|
13
|
+
? `init ${applogsSummary(e.init)}`
|
|
14
|
+
: `added ${applogsSummary(e.added)}${e.removed ? ` removed ${applogsSummary(e.removed)}` : ''}`
|
|
15
|
+
|
|
9
16
|
// ── ThreadDerivation ────────────────────────────────────────────
|
|
10
17
|
|
|
11
18
|
/** Delta event — only incremental changes (no init) */
|
|
12
|
-
export type DeltaEvent = { added: readonly Applog[]
|
|
19
|
+
export type DeltaEvent = { added: readonly Applog[]; removed: readonly Applog[] | null }
|
|
13
20
|
|
|
14
21
|
/** Context passed to mapDelta — explicit deps, no `this` needed */
|
|
15
22
|
export interface DeltaContext {
|
|
@@ -210,9 +217,10 @@ export class MappedThread extends Thread {
|
|
|
210
217
|
this._applogs.splice(idx, 1)
|
|
211
218
|
} else {
|
|
212
219
|
throw ERROR(`MappedThread{${this.name}} toRemove: log not found`, toRemove, {
|
|
213
|
-
thread: this,
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
thread: this.nameAndSizeUntracked,
|
|
221
|
+
currentLogs: applogsSummary(this._applogs),
|
|
222
|
+
event: threadEventSummary(event),
|
|
223
|
+
result: threadEventSummary(result),
|
|
216
224
|
})
|
|
217
225
|
}
|
|
218
226
|
}
|
package/src/thread/utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { curry, debounce, partial, pull, uniq, uniqBy, uniqWith } from 'lodash-e
|
|
|
3
3
|
|
|
4
4
|
import { CID } from 'multiformats'
|
|
5
5
|
import { ensureTsPvAndFinalizeApplogs, joinThreads } from '../applog/applog-helpers.ts'
|
|
6
|
-
import { compareApplogsByEnAt, compareApplogsByTs, objEqualByKeys } from '../applog/applog-utils.ts'
|
|
6
|
+
import { applogsSummary, compareApplogsByEnAt, compareApplogsByTs, objEqualByKeys } from '../applog/applog-utils.ts'
|
|
7
7
|
import { Applog, ApplogForInsert, CidString, EntityID } from '../applog/datom-types.ts'
|
|
8
8
|
import { Thread } from './basic.ts'
|
|
9
9
|
import { MappedThread } from './mapped.ts'
|
|
@@ -39,7 +39,7 @@ export function debounceWrites(
|
|
|
39
39
|
let insertQueue: Array<Applog | ApplogForInsert> = []
|
|
40
40
|
|
|
41
41
|
const debouncedCommit = debounce(() => {
|
|
42
|
-
|
|
42
|
+
DEBUG(`Debounce tail`, { thread: thread.nameAndSizeUntracked, queued: applogsSummary(insertQueue) })
|
|
43
43
|
const toInsert = ensureTsPvAndFinalizeApplogs(
|
|
44
44
|
// ? uniq, sure - but which one is used? (update: seems the first one, so reverse)
|
|
45
45
|
uniqWith(insertQueue.reverse(), removeDuplicatesWith),
|
|
@@ -49,7 +49,7 @@ export function debounceWrites(
|
|
|
49
49
|
insertQueue.splice(0, insertQueue.length) // clear queue
|
|
50
50
|
}, wait)
|
|
51
51
|
const handleInsert = (applogs: Applog[] | ApplogForInsert[]) => {
|
|
52
|
-
DEBUG(`Debounce input:`, applogs, { thread
|
|
52
|
+
DEBUG(`Debounce input:`, applogsSummary(applogs), { thread: thread.nameAndSizeUntracked, queued: insertQueue.length })
|
|
53
53
|
insertQueue.push(...applogs)
|
|
54
54
|
debouncedCommit()
|
|
55
55
|
return null // don't insert anything
|
|
@@ -131,8 +131,8 @@ export const excludeApplogsContainedIn = (
|
|
|
131
131
|
const excludeCids: Set<string> = exclude instanceof Set
|
|
132
132
|
? new Set([...exclude].map(c => c.toString()))
|
|
133
133
|
: exclude instanceof Thread
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
? new Set(exclude.applogsCids)
|
|
135
|
+
: new Set(exclude.map(a => a.cid))
|
|
136
136
|
|
|
137
137
|
return applogs.filter(applog => {
|
|
138
138
|
if (!applog.cid) {
|