@mblaney/holster 1.0.36 → 2.0.1
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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +3 -3
- package/dist/holster.js.map +3 -3
- package/package.json +4 -5
- package/src/dup.js +4 -5
- package/src/get.js +4 -1
- package/src/ham.js +5 -2
- package/src/holster.js +78 -47
- package/src/index.js +1 -1
- package/src/radisk.js +90 -87
- package/src/sea-utils.js +1 -1
- package/src/store.js +1 -1
- package/src/user.js +0 -4
- package/src/wire.js +204 -80
- package/ts/src/dup.ts +6 -17
- package/ts/src/ham.ts +5 -1
- package/ts/src/holster.ts +66 -33
- package/ts/src/radisk.ts +87 -81
- package/ts/src/schemas.ts +1 -5
- package/ts/src/sea-utils.ts +1 -1
- package/ts/src/store.ts +1 -1
- package/ts/src/user.ts +0 -4
- package/ts/src/wire.ts +201 -69
package/src/wire.js
CHANGED
|
@@ -4,7 +4,7 @@ import Ham from "./ham.js"
|
|
|
4
4
|
import Store from "./store.js"
|
|
5
5
|
import * as utils from "./utils.js"
|
|
6
6
|
|
|
7
|
-
const isNode = typeof
|
|
7
|
+
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
8
8
|
|
|
9
9
|
const wsModule = isNode ? await import("ws") : undefined
|
|
10
10
|
|
|
@@ -169,18 +169,31 @@ const createRetryHandler = (maxRetries = 5) => {
|
|
|
169
169
|
// Wire starts a websocket client or server and returns get and put methods
|
|
170
170
|
// for access to the wire spec and storage.
|
|
171
171
|
const Wire = opt => {
|
|
172
|
-
const dup = Dup(
|
|
172
|
+
const dup = Dup()
|
|
173
173
|
const store = Store(opt)
|
|
174
174
|
const graph = {}
|
|
175
175
|
const queue = {}
|
|
176
176
|
const listen = {}
|
|
177
|
+
// Tracks souls that have had a complete full-node store fetch, so subsequent
|
|
178
|
+
// full-node requests can read directly from the in-memory graph without a
|
|
179
|
+
// redundant store round-trip. Kept in sync with graph eviction via Ham.mix.
|
|
180
|
+
const hasNode = new Set()
|
|
177
181
|
|
|
178
182
|
// Track references we want but don't have yet
|
|
179
183
|
const pendingReferences = new Set()
|
|
180
184
|
|
|
185
|
+
// Debounce client-side wire update checks: skip if the same lex was checked
|
|
186
|
+
// recently. Keyed by "soul" or "soul.property".
|
|
187
|
+
const recentChecks = new Map()
|
|
188
|
+
const recentCheckTTL = 10000 // 10 seconds
|
|
189
|
+
|
|
181
190
|
// Track pending timeouts that should start when message is sent
|
|
182
191
|
const pendingTimeouts = new Map()
|
|
183
192
|
|
|
193
|
+
// Track offline null-response timeouts so they can be cancelled if the WS
|
|
194
|
+
// connects before they fire. Keyed by the same trackId as pendingTimeouts.
|
|
195
|
+
const offlineTimeouts = new Map()
|
|
196
|
+
|
|
184
197
|
// Helper to check if we have a soul in memory or storage
|
|
185
198
|
const hasSoul = async soul => {
|
|
186
199
|
if (graph[soul]) return true
|
|
@@ -206,25 +219,26 @@ const Wire = opt => {
|
|
|
206
219
|
const key = utils.userPublicKey
|
|
207
220
|
|
|
208
221
|
for (const soul of Object.keys(data)) {
|
|
222
|
+
const node = data[soul]
|
|
223
|
+
// If the incoming update doesn't include a userPublicKey property at all,
|
|
224
|
+
// allow it - the update is not trying to change the public key.
|
|
225
|
+
if (node[key] === undefined) continue
|
|
226
|
+
|
|
209
227
|
const msg = await new Promise(res => {
|
|
210
|
-
getWithCallback({"#": soul, ".": key}, res, send)
|
|
228
|
+
getWithCallback({"#": soul, ".": key}, res, send, {put: true})
|
|
211
229
|
})
|
|
212
230
|
if (msg.err) {
|
|
213
231
|
if (cb) cb(msg.err)
|
|
214
232
|
return false
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
const node = data[soul]
|
|
218
235
|
// If there is no current node then the data is ok to write without
|
|
219
236
|
// matching public keys, as the provided soul also needs a rel on the
|
|
220
237
|
// parent node which then also requires checking. Otherwise public keys
|
|
221
|
-
// need to match for existing data.
|
|
222
|
-
// include a userPublicKey property at all, allow it - the update is
|
|
223
|
-
// not trying to change the public key.
|
|
238
|
+
// need to match for existing data.
|
|
224
239
|
if (
|
|
225
240
|
!msg.put ||
|
|
226
241
|
!msg.put[soul] ||
|
|
227
|
-
node[key] === undefined ||
|
|
228
242
|
msg.put[soul][key] === node[key]
|
|
229
243
|
) {
|
|
230
244
|
continue
|
|
@@ -277,7 +291,7 @@ const Wire = opt => {
|
|
|
277
291
|
|
|
278
292
|
const put = async (msg, send) => {
|
|
279
293
|
// Store updates returned from Ham.mix and defer updates if required.
|
|
280
|
-
const update = await Ham.mix(msg.put, graph, opt.secure, listen)
|
|
294
|
+
const update = await Ham.mix(msg.put, graph, hasNode, opt.secure, listen)
|
|
281
295
|
if (Object.keys(update.now).length === 0) {
|
|
282
296
|
// No updates to store, check deferred.
|
|
283
297
|
if (Object.keys(update.defer).length !== 0) {
|
|
@@ -292,6 +306,7 @@ const Wire = opt => {
|
|
|
292
306
|
if (!(await check(update.now, send))) return
|
|
293
307
|
|
|
294
308
|
store.put(update.now, err => {
|
|
309
|
+
if (err) console.warn("store.put", err)
|
|
295
310
|
send(
|
|
296
311
|
JSON.stringify({
|
|
297
312
|
"#": dup.track(utils.text.random(9)),
|
|
@@ -316,7 +331,10 @@ const Wire = opt => {
|
|
|
316
331
|
|
|
317
332
|
if (!utils.obj.is(_opt)) _opt = {}
|
|
318
333
|
|
|
319
|
-
|
|
334
|
+
// For full-node requests on souls that have been fully fetched from store,
|
|
335
|
+
// treat the graph as authoritative — skip the redundant store round-trip.
|
|
336
|
+
const fast = _opt.fast || (!lex["."] && hasNode.has(lex["#"]))
|
|
337
|
+
const ack = Get(lex, graph, fast)
|
|
320
338
|
const track = utils.text.random(9)
|
|
321
339
|
const request = JSON.stringify({
|
|
322
340
|
"#": dup.track(track),
|
|
@@ -324,11 +342,30 @@ const Wire = opt => {
|
|
|
324
342
|
})
|
|
325
343
|
|
|
326
344
|
if (ack) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
345
|
+
if (!isNode) {
|
|
346
|
+
// Client: send request on the wire to check for updates,
|
|
347
|
+
// since it may have missed puts while disconnected.
|
|
348
|
+
// Skip if the same lex was checked recently to avoid flooding.
|
|
349
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
350
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
351
|
+
const now = Date.now()
|
|
352
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
353
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
354
|
+
recentChecks.set(checkKey, now)
|
|
355
|
+
const sendResult = send(request)
|
|
356
|
+
if (sendResult && sendResult.err) {
|
|
357
|
+
cb({err: sendResult.err})
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Don't skip checks for other types of lex queries.
|
|
363
|
+
const sendResult = send(request)
|
|
364
|
+
if (sendResult && sendResult.err) {
|
|
365
|
+
cb({err: sendResult.err})
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
}
|
|
332
369
|
}
|
|
333
370
|
cb({put: ack})
|
|
334
371
|
return
|
|
@@ -342,27 +379,86 @@ const Wire = opt => {
|
|
|
342
379
|
// request, so stale server responses with older state are correctly
|
|
343
380
|
// rejected by ham.mix.
|
|
344
381
|
for (const [soul, node] of Object.entries(ack)) {
|
|
345
|
-
if (!
|
|
382
|
+
if (!node) continue
|
|
383
|
+
if (!graph[soul]) {
|
|
384
|
+
graph[soul] = {...node}
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
// Merge properties absent from the current graph entry. Only add
|
|
388
|
+
// missing keys — existing entries were set by ham.mix and represent
|
|
389
|
+
// the authoritative merged state for those properties.
|
|
390
|
+
for (const [key, value] of Object.entries(node)) {
|
|
391
|
+
if (key === "_") continue
|
|
392
|
+
if (typeof graph[soul][key] !== "undefined") continue
|
|
393
|
+
graph[soul][key] = value
|
|
394
|
+
if (node._ && node._[">"] && typeof node._[">"][key] !== "undefined") {
|
|
395
|
+
const state = node._[">"][key]
|
|
396
|
+
graph[soul]._[">"][key] = state
|
|
397
|
+
if (node._["s"] && node._["s"][state]) {
|
|
398
|
+
if (!graph[soul]._["s"]) graph[soul]._["s"] = {}
|
|
399
|
+
graph[soul]._["s"][state] = node._["s"][state]
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
346
403
|
}
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
if
|
|
350
|
-
|
|
404
|
+
// Full-node fetch: the graph is now complete for this soul.
|
|
405
|
+
if (!lex["."] && ack[lex["#"]]) hasNode.add(lex["#"])
|
|
406
|
+
// Only serve from store if the requested property is present.
|
|
407
|
+
const node = ack[lex["#"]]
|
|
408
|
+
const hasProperty =
|
|
409
|
+
node &&
|
|
410
|
+
Object.keys(node).some(
|
|
411
|
+
key => key !== "_" && utils.match(lex["."], key),
|
|
412
|
+
)
|
|
413
|
+
if (hasProperty) {
|
|
414
|
+
// Public keys are immutable once written — never need a wire check.
|
|
415
|
+
if (!isNode && lex["."] !== utils.userPublicKey) {
|
|
416
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
417
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
418
|
+
const now = Date.now()
|
|
419
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
420
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
421
|
+
recentChecks.set(checkKey, now)
|
|
422
|
+
const sendResult = send(request)
|
|
423
|
+
if (sendResult && sendResult.err) {
|
|
424
|
+
cb({err: sendResult.err})
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
const sendResult = send(request)
|
|
430
|
+
if (sendResult && sendResult.err) {
|
|
431
|
+
cb({err: sendResult.err})
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
351
437
|
return
|
|
352
438
|
}
|
|
353
|
-
cb(err ? {put: ack, err: err} : {put: ack})
|
|
354
|
-
return
|
|
355
439
|
}
|
|
356
440
|
|
|
441
|
+
// Data wasn't found locally, log errors and call back after a timeout.
|
|
357
442
|
if (err) console.log(err)
|
|
358
443
|
|
|
444
|
+
// Put requests only need a local answer — skip the wire round-trip.
|
|
445
|
+
if (_opt && _opt.put) {
|
|
446
|
+
const id = lex["#"]
|
|
447
|
+
const ack = {[id]: null}
|
|
448
|
+
if (typeof lex["."] === "string") {
|
|
449
|
+
ack[id] = {[lex["."]]: null}
|
|
450
|
+
}
|
|
451
|
+
cb({put: ack})
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
359
455
|
queue[track] = cb
|
|
360
456
|
|
|
361
|
-
// Store
|
|
362
|
-
pendingTimeouts.set(track,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
457
|
+
// Store lex to construct the null ack when the timeout fires
|
|
458
|
+
pendingTimeouts.set(track, lex)
|
|
459
|
+
|
|
460
|
+
// Ensure the requested soul is stored when the response arrives.
|
|
461
|
+
pendingReferences.add(lex["#"])
|
|
366
462
|
|
|
367
463
|
const sendResult = send(request)
|
|
368
464
|
if (sendResult && sendResult.err) {
|
|
@@ -393,7 +489,7 @@ const Wire = opt => {
|
|
|
393
489
|
// here using the api. This is ok because correct timestamps should be
|
|
394
490
|
// used whereas wire spec needs to handle clock skew for updates
|
|
395
491
|
// across the network.
|
|
396
|
-
const update = await Ham.mix(data, graph, opt.secure, listen)
|
|
492
|
+
const update = await Ham.mix(data, graph, hasNode, opt.secure, listen)
|
|
397
493
|
if (Object.keys(update.now).length === 0) {
|
|
398
494
|
// No updates, still respond to callback.
|
|
399
495
|
if (cb) cb(null)
|
|
@@ -467,7 +563,7 @@ const Wire = opt => {
|
|
|
467
563
|
}
|
|
468
564
|
}
|
|
469
565
|
|
|
470
|
-
if (isNode) {
|
|
566
|
+
if (isNode && (opt.wss || opt.server || opt.port != null)) {
|
|
471
567
|
let wss = opt.wss
|
|
472
568
|
// Node's websocket server provides clients as an array, whereas
|
|
473
569
|
// mock-sockets provides clients as a function that returns an array.
|
|
@@ -475,7 +571,7 @@ const Wire = opt => {
|
|
|
475
571
|
if (!wss) {
|
|
476
572
|
const config = opt.server
|
|
477
573
|
? {server: opt.server}
|
|
478
|
-
: {port: opt.port
|
|
574
|
+
: {port: opt.port}
|
|
479
575
|
wss = new wsModule.WebSocketServer(config)
|
|
480
576
|
clients = () => wss.clients
|
|
481
577
|
}
|
|
@@ -485,22 +581,21 @@ const Wire = opt => {
|
|
|
485
581
|
const msg = JSON.parse(data)
|
|
486
582
|
const trackId = msg["#"]
|
|
487
583
|
if (trackId && pendingTimeouts.has(trackId)) {
|
|
488
|
-
const
|
|
584
|
+
const lex = pendingTimeouts.get(trackId)
|
|
489
585
|
pendingTimeouts.delete(trackId)
|
|
490
586
|
|
|
491
|
-
|
|
492
|
-
timeoutConfig.timeoutId = setTimeout(() => {
|
|
587
|
+
setTimeout(() => {
|
|
493
588
|
const cb = queue[trackId]
|
|
494
589
|
if (cb) {
|
|
495
|
-
const id =
|
|
590
|
+
const id = lex["#"]
|
|
496
591
|
const ack = {[id]: null}
|
|
497
|
-
if (typeof
|
|
498
|
-
ack[id] = {[
|
|
592
|
+
if (typeof lex["."] === "string") {
|
|
593
|
+
ack[id] = {[lex["."]]: null}
|
|
499
594
|
}
|
|
500
595
|
cb({put: ack})
|
|
501
596
|
delete queue[trackId]
|
|
502
597
|
}
|
|
503
|
-
},
|
|
598
|
+
}, isTestEnv ? 100 : 10000)
|
|
504
599
|
}
|
|
505
600
|
|
|
506
601
|
clients().forEach(client => {
|
|
@@ -634,6 +729,24 @@ const Wire = opt => {
|
|
|
634
729
|
let queueProcessor = null
|
|
635
730
|
const maxQueueLength = opt.maxQueueLength || 10000
|
|
636
731
|
|
|
732
|
+
const startResponseTimeout = (trackId, lex, delay = isTestEnv ? 100 : 10000) => {
|
|
733
|
+
return setTimeout(() => {
|
|
734
|
+
offlineTimeouts.delete(trackId)
|
|
735
|
+
pendingTimeouts.delete(trackId)
|
|
736
|
+
const cb = queue[trackId]
|
|
737
|
+
// No-op on successful send as callback is removed from the queue.
|
|
738
|
+
if (cb) {
|
|
739
|
+
const id = lex["#"]
|
|
740
|
+
const ack = {[id]: null}
|
|
741
|
+
if (typeof lex["."] === "string") {
|
|
742
|
+
ack[id] = {[lex["."]]: null}
|
|
743
|
+
}
|
|
744
|
+
cb({put: ack})
|
|
745
|
+
delete queue[trackId]
|
|
746
|
+
}
|
|
747
|
+
}, delay)
|
|
748
|
+
}
|
|
749
|
+
|
|
637
750
|
const processQueue = () => {
|
|
638
751
|
if (messageQueue.length === 0) {
|
|
639
752
|
queueProcessor = null
|
|
@@ -661,30 +774,27 @@ const Wire = opt => {
|
|
|
661
774
|
const data = messageQueue[0]
|
|
662
775
|
const sent = sendToPeers(data)
|
|
663
776
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
messageQueue.shift()
|
|
777
|
+
const msg = JSON.parse(data)
|
|
778
|
+
const trackId = msg["#"]
|
|
667
779
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (trackId && pendingTimeouts.has(trackId)) {
|
|
672
|
-
const timeoutConfig = pendingTimeouts.get(trackId)
|
|
673
|
-
pendingTimeouts.delete(trackId)
|
|
780
|
+
// Drop GET messages with no pending callback — these were already served
|
|
781
|
+
// from local store so no caller is waiting for the wire response.
|
|
782
|
+
const isStaleCheckMessage = !sent && msg.get && !pendingTimeouts.has(trackId)
|
|
674
783
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
784
|
+
if (sent || isStaleCheckMessage) {
|
|
785
|
+
messageQueue.shift()
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (trackId && pendingTimeouts.has(trackId)) {
|
|
789
|
+
const lex = pendingTimeouts.get(trackId)
|
|
790
|
+
if (sent) {
|
|
791
|
+
if (offlineTimeouts.has(trackId)) {
|
|
792
|
+
clearTimeout(offlineTimeouts.get(trackId))
|
|
793
|
+
offlineTimeouts.delete(trackId)
|
|
794
|
+
}
|
|
795
|
+
startResponseTimeout(trackId, lex)
|
|
796
|
+
} else if (!offlineTimeouts.has(trackId)) {
|
|
797
|
+
offlineTimeouts.set(trackId, startResponseTimeout(trackId, lex, 0))
|
|
688
798
|
}
|
|
689
799
|
}
|
|
690
800
|
|
|
@@ -782,39 +892,53 @@ const Wire = opt => {
|
|
|
782
892
|
dup.track(msg["#"])
|
|
783
893
|
if (msg.get) get(msg, send)
|
|
784
894
|
if (msg.put) {
|
|
785
|
-
//
|
|
786
|
-
|
|
895
|
+
// Pre-pass: expand pendingReferences to include rels of every soul
|
|
896
|
+
// that will be stored in this batch. This handles the case where a
|
|
897
|
+
// server batch contains both a parent soul (e.g. day_soul with item
|
|
898
|
+
// rels) and its child souls (item_souls), in any order — without the
|
|
899
|
+
// pre-pass, a child soul that appears before its parent in the batch
|
|
900
|
+
// would be missed because pendingReferences hasn't been updated yet.
|
|
901
|
+
const hasSoulMap = new Map()
|
|
787
902
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (
|
|
794
|
-
for (const [key, value] of Object.entries(node)) {
|
|
795
|
-
const soulId = utils.rel.is(value)
|
|
796
|
-
if (soulId) {
|
|
797
|
-
// Add referenced soul to pending list
|
|
798
|
-
pendingReferences.add(soulId)
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
903
|
+
const known = await hasSoul(soul)
|
|
904
|
+
hasSoulMap.set(soul, known)
|
|
905
|
+
if ((!known && !pendingReferences.has(soul)) || !node || typeof node !== "object") continue
|
|
906
|
+
for (const [, value] of Object.entries(node)) {
|
|
907
|
+
const soulId = utils.rel.is(value)
|
|
908
|
+
if (soulId) pendingReferences.add(soulId)
|
|
802
909
|
}
|
|
803
|
-
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Main pass: filter using cached hasSoul results and the now-complete
|
|
913
|
+
// pendingReferences set.
|
|
914
|
+
const filteredPut = {}
|
|
915
|
+
const pendingToDelete = []
|
|
916
|
+
for (const [soul, node] of Object.entries(msg.put)) {
|
|
917
|
+
let shouldStore = hasSoulMap.get(soul)
|
|
804
918
|
if (pendingReferences.has(soul)) {
|
|
805
919
|
shouldStore = true
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
920
|
+
// Don't delete from pendingReferences yet — concurrent onmessage
|
|
921
|
+
// handlers for the same soul may have yielded at await hasSoul()
|
|
922
|
+
// before this pass ran. Deleting here would cause those handlers
|
|
923
|
+
// to see shouldStore=false and skip storing the soul's data.
|
|
924
|
+
// Instead, defer the delete until after put() updates the graph,
|
|
925
|
+
// so any concurrent handler can find the soul via hasSoul(graph).
|
|
926
|
+
pendingToDelete.push(soul)
|
|
810
927
|
}
|
|
928
|
+
if (shouldStore) filteredPut[soul] = node
|
|
811
929
|
}
|
|
930
|
+
|
|
812
931
|
// Store filtered data if any
|
|
813
932
|
if (Object.keys(filteredPut).length > 0) {
|
|
814
933
|
// Create filtered message for Ham.mix with original message ID
|
|
815
934
|
const filteredMsg = {put: filteredPut, "#": msg["#"]}
|
|
816
935
|
await put(filteredMsg, send)
|
|
817
936
|
}
|
|
937
|
+
|
|
938
|
+
// Safe to delete now — put() has run Ham.mix which updates graph
|
|
939
|
+
// in-place, so hasSoul() will return true for these souls going
|
|
940
|
+
// forward, making pendingReferences no longer needed for them.
|
|
941
|
+
pendingToDelete.forEach(soul => pendingReferences.delete(soul))
|
|
818
942
|
}
|
|
819
943
|
|
|
820
944
|
const id = msg["@"]
|
package/ts/src/dup.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dup - Duplicate message tracking for wire protocol
|
|
3
|
-
* Prevents processing the same message multiple times
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
export interface DupInterface {
|
|
7
2
|
store: Record<string, number>
|
|
8
3
|
check: (id: string) => string | false
|
|
@@ -10,13 +5,8 @@ export interface DupInterface {
|
|
|
10
5
|
expiry: NodeJS.Timeout | null
|
|
11
6
|
}
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* @param maxAge - Maximum age of tracked messages in milliseconds (default: 9000)
|
|
16
|
-
* @returns Dup interface for checking and tracking message IDs
|
|
17
|
-
*/
|
|
18
|
-
const Dup = (maxAge?: number): DupInterface => {
|
|
19
|
-
const maxAgeMs = maxAge || 9000
|
|
8
|
+
const Dup = (): DupInterface => {
|
|
9
|
+
const maxAge = 9000
|
|
20
10
|
const dup: DupInterface = {
|
|
21
11
|
store: {},
|
|
22
12
|
expiry: null,
|
|
@@ -28,16 +18,16 @@ const Dup = (maxAge?: number): DupInterface => {
|
|
|
28
18
|
dup.store[id] = Date.now()
|
|
29
19
|
if (!dup.expiry) {
|
|
30
20
|
dup.expiry = setTimeout(() => {
|
|
31
|
-
if (!dup.expiry) return
|
|
32
|
-
|
|
33
21
|
const now = Date.now()
|
|
34
22
|
Object.keys(dup.store).forEach(id => {
|
|
35
|
-
if (now - dup.store[id]! >
|
|
23
|
+
if (now - dup.store[id]! > maxAge) {
|
|
36
24
|
delete dup.store[id]
|
|
37
25
|
}
|
|
38
26
|
})
|
|
39
27
|
dup.expiry = null
|
|
40
|
-
},
|
|
28
|
+
}, maxAge)
|
|
29
|
+
// Don't block process exit — cleanup is best-effort.
|
|
30
|
+
if (dup.expiry.unref) dup.expiry.unref()
|
|
41
31
|
}
|
|
42
32
|
return id
|
|
43
33
|
},
|
|
@@ -46,4 +36,3 @@ const Dup = (maxAge?: number): DupInterface => {
|
|
|
46
36
|
}
|
|
47
37
|
|
|
48
38
|
export default Dup
|
|
49
|
-
|
package/ts/src/ham.ts
CHANGED
|
@@ -67,6 +67,7 @@ const Ham = (
|
|
|
67
67
|
Ham.mix = async (
|
|
68
68
|
change: Graph,
|
|
69
69
|
graph: Graph,
|
|
70
|
+
hasNode: Set<string> | undefined,
|
|
70
71
|
secure: boolean,
|
|
71
72
|
listen: ListenMap
|
|
72
73
|
): Promise<HamMixResult> => {
|
|
@@ -244,7 +245,10 @@ Ham.mix = async (
|
|
|
244
245
|
})
|
|
245
246
|
.sort((a, b) => a.maxState - b.maxState)
|
|
246
247
|
const remove = soulsByAge.slice(0, souls.length - MAX_GRAPH_SIZE)
|
|
247
|
-
remove.forEach(({ soul }) =>
|
|
248
|
+
remove.forEach(({ soul }) => {
|
|
249
|
+
delete graph[soul]
|
|
250
|
+
if (hasNode) hasNode.delete(soul)
|
|
251
|
+
})
|
|
248
252
|
}
|
|
249
253
|
|
|
250
254
|
return { now, defer, wait, listeners }
|