@mblaney/holster 1.0.35 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +4 -0
- package/dist/holster.js.map +7 -0
- package/package.json +22 -5
- package/src/dup.js +4 -5
- package/src/get.js +10 -19
- package/src/ham.js +22 -49
- package/src/holster.js +172 -58
- package/src/index.js +1 -1
- package/src/radisk.js +89 -86
- package/src/sea-utils.js +1 -1
- package/src/sea.js +0 -52
- package/src/store.js +8 -8
- package/src/user.js +0 -4
- package/src/wire.js +209 -79
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +38 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +258 -0
- package/ts/src/holster.ts +968 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +591 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +515 -0
- package/ts/src/sea-utils.ts +103 -0
- package/ts/src/sea.ts +388 -0
- package/ts/src/store.ts +321 -0
- package/ts/src/user.ts +377 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +961 -0
- package/.dockerignore +0 -9
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -33
- package/Dockerfile +0 -20
- package/examples/index.html +0 -33
- package/examples/index.js +0 -49
- package/examples/user.html +0 -31
- package/examples/user.js +0 -93
- package/examples/wire.html +0 -27
- package/examples/wire.js +0 -55
- package/test/holster.get.test.js +0 -94
- package/test/holster.lex.test.js +0 -196
- package/test/holster.off.test.js +0 -53
- package/test/holster.on.test.js +0 -264
- package/test/holster.put.test.js +0 -358
- package/test/holster.secure.test.js +0 -202
- package/test/holster.user.get.test.js +0 -114
- package/test/holster.user.lex.test.js +0 -149
- package/test/holster.user.off.test.js +0 -73
- package/test/holster.user.on.test.js +0 -300
- package/test/holster.user.put.test.js +0 -463
- package/test/kitty/! +0 -9
- package/test/names.js +0 -225
- package/test/radisk.cache.test.js +0 -213
- package/test/radisk.test.js +0 -331
- package/test/radix.test.js +0 -117
- package/test/sea.test.js +0 -284
- package/test/split.test.js +0 -317
- package/test/store.test.js +0 -316
- package/test/system/basic.test.js +0 -29
- package/test/system/chained-on.test.js +0 -41
- package/test/system/concurrent-listeners.test.js +0 -125
- package/test/system/immediate-read.test.js +0 -30
- package/test/system/listener-before-data.test.js +0 -51
- package/test/system/promise-wrapper.test.js +0 -47
- package/test/system/user-chained-on.test.js +0 -60
- package/test/system/user-concurrent-listeners.test.js +0 -142
- package/test/system/user-listener-before-data.test.js +0 -66
- package/test/user.test.js +0 -173
- package/test/wire.test.js +0 -158
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
|
|
@@ -338,25 +375,90 @@ const Wire = opt => {
|
|
|
338
375
|
lex,
|
|
339
376
|
(err, ack) => {
|
|
340
377
|
if (ack) {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
378
|
+
// Populate in-memory graph from store data before sending the server
|
|
379
|
+
// request, so stale server responses with older state are correctly
|
|
380
|
+
// rejected by ham.mix.
|
|
381
|
+
for (const [soul, node] of Object.entries(ack)) {
|
|
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
|
+
}
|
|
403
|
+
}
|
|
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})
|
|
345
437
|
return
|
|
346
438
|
}
|
|
347
|
-
cb({put: ack, err: err})
|
|
348
|
-
return
|
|
349
439
|
}
|
|
350
440
|
|
|
441
|
+
// Data wasn't found locally, log errors and call back after a timeout.
|
|
351
442
|
if (err) console.log(err)
|
|
352
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
|
+
|
|
353
455
|
queue[track] = cb
|
|
354
456
|
|
|
355
|
-
// Store
|
|
356
|
-
pendingTimeouts.set(track,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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["#"])
|
|
360
462
|
|
|
361
463
|
const sendResult = send(request)
|
|
362
464
|
if (sendResult && sendResult.err) {
|
|
@@ -387,7 +489,7 @@ const Wire = opt => {
|
|
|
387
489
|
// here using the api. This is ok because correct timestamps should be
|
|
388
490
|
// used whereas wire spec needs to handle clock skew for updates
|
|
389
491
|
// across the network.
|
|
390
|
-
const update = await Ham.mix(data, graph, opt.secure, listen)
|
|
492
|
+
const update = await Ham.mix(data, graph, hasNode, opt.secure, listen)
|
|
391
493
|
if (Object.keys(update.now).length === 0) {
|
|
392
494
|
// No updates, still respond to callback.
|
|
393
495
|
if (cb) cb(null)
|
|
@@ -461,7 +563,7 @@ const Wire = opt => {
|
|
|
461
563
|
}
|
|
462
564
|
}
|
|
463
565
|
|
|
464
|
-
if (isNode) {
|
|
566
|
+
if (isNode && (opt.wss || opt.server || opt.port != null)) {
|
|
465
567
|
let wss = opt.wss
|
|
466
568
|
// Node's websocket server provides clients as an array, whereas
|
|
467
569
|
// mock-sockets provides clients as a function that returns an array.
|
|
@@ -469,7 +571,7 @@ const Wire = opt => {
|
|
|
469
571
|
if (!wss) {
|
|
470
572
|
const config = opt.server
|
|
471
573
|
? {server: opt.server}
|
|
472
|
-
: {port: opt.port
|
|
574
|
+
: {port: opt.port}
|
|
473
575
|
wss = new wsModule.WebSocketServer(config)
|
|
474
576
|
clients = () => wss.clients
|
|
475
577
|
}
|
|
@@ -479,22 +581,21 @@ const Wire = opt => {
|
|
|
479
581
|
const msg = JSON.parse(data)
|
|
480
582
|
const trackId = msg["#"]
|
|
481
583
|
if (trackId && pendingTimeouts.has(trackId)) {
|
|
482
|
-
const
|
|
584
|
+
const lex = pendingTimeouts.get(trackId)
|
|
483
585
|
pendingTimeouts.delete(trackId)
|
|
484
586
|
|
|
485
|
-
|
|
486
|
-
timeoutConfig.timeoutId = setTimeout(() => {
|
|
587
|
+
setTimeout(() => {
|
|
487
588
|
const cb = queue[trackId]
|
|
488
589
|
if (cb) {
|
|
489
|
-
const id =
|
|
590
|
+
const id = lex["#"]
|
|
490
591
|
const ack = {[id]: null}
|
|
491
|
-
if (typeof
|
|
492
|
-
ack[id] = {[
|
|
592
|
+
if (typeof lex["."] === "string") {
|
|
593
|
+
ack[id] = {[lex["."]]: null}
|
|
493
594
|
}
|
|
494
595
|
cb({put: ack})
|
|
495
596
|
delete queue[trackId]
|
|
496
597
|
}
|
|
497
|
-
},
|
|
598
|
+
}, isTestEnv ? 100 : 10000)
|
|
498
599
|
}
|
|
499
600
|
|
|
500
601
|
clients().forEach(client => {
|
|
@@ -628,6 +729,24 @@ const Wire = opt => {
|
|
|
628
729
|
let queueProcessor = null
|
|
629
730
|
const maxQueueLength = opt.maxQueueLength || 10000
|
|
630
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
|
+
|
|
631
750
|
const processQueue = () => {
|
|
632
751
|
if (messageQueue.length === 0) {
|
|
633
752
|
queueProcessor = null
|
|
@@ -655,30 +774,27 @@ const Wire = opt => {
|
|
|
655
774
|
const data = messageQueue[0]
|
|
656
775
|
const sent = sendToPeers(data)
|
|
657
776
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
messageQueue.shift()
|
|
777
|
+
const msg = JSON.parse(data)
|
|
778
|
+
const trackId = msg["#"]
|
|
661
779
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (trackId && pendingTimeouts.has(trackId)) {
|
|
666
|
-
const timeoutConfig = pendingTimeouts.get(trackId)
|
|
667
|
-
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)
|
|
668
783
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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))
|
|
682
798
|
}
|
|
683
799
|
}
|
|
684
800
|
|
|
@@ -776,39 +892,53 @@ const Wire = opt => {
|
|
|
776
892
|
dup.track(msg["#"])
|
|
777
893
|
if (msg.get) get(msg, send)
|
|
778
894
|
if (msg.put) {
|
|
779
|
-
//
|
|
780
|
-
|
|
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()
|
|
781
902
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (
|
|
788
|
-
for (const [key, value] of Object.entries(node)) {
|
|
789
|
-
const soulId = utils.rel.is(value)
|
|
790
|
-
if (soulId) {
|
|
791
|
-
// Add referenced soul to pending list
|
|
792
|
-
pendingReferences.add(soulId)
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
}
|
|
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)
|
|
796
909
|
}
|
|
797
|
-
|
|
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)
|
|
798
918
|
if (pendingReferences.has(soul)) {
|
|
799
919
|
shouldStore = true
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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)
|
|
804
927
|
}
|
|
928
|
+
if (shouldStore) filteredPut[soul] = node
|
|
805
929
|
}
|
|
930
|
+
|
|
806
931
|
// Store filtered data if any
|
|
807
932
|
if (Object.keys(filteredPut).length > 0) {
|
|
808
933
|
// Create filtered message for Ham.mix with original message ID
|
|
809
934
|
const filteredMsg = {put: filteredPut, "#": msg["#"]}
|
|
810
935
|
await put(filteredMsg, send)
|
|
811
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))
|
|
812
942
|
}
|
|
813
943
|
|
|
814
944
|
const id = msg["@"]
|
package/ts/src/array.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeaArray - Extended Array with custom toString for encoding support
|
|
3
|
+
* Supports utf8, hex, and base64 encoding
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
if (typeof btoa === "undefined") {
|
|
7
|
+
globalThis.btoa = (data: string): string =>
|
|
8
|
+
Buffer.from(data, "binary").toString("base64")
|
|
9
|
+
globalThis.atob = (data: string): string =>
|
|
10
|
+
Buffer.from(data, "base64").toString("binary")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Encoding = "utf8" | "hex" | "base64"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SeaArray extends Array to provide encoding capabilities
|
|
17
|
+
*/
|
|
18
|
+
class SeaArray extends Array<number> {
|
|
19
|
+
/**
|
|
20
|
+
* Convert array to string with specified encoding
|
|
21
|
+
* @param enc - Encoding type (utf8, hex, or base64)
|
|
22
|
+
* @param start - Start index (default: 0)
|
|
23
|
+
* @param end - End index (default: array length)
|
|
24
|
+
*/
|
|
25
|
+
override toString(enc?: Encoding, start?: number, end?: number): string {
|
|
26
|
+
const encoding = enc || "utf8"
|
|
27
|
+
const startIdx = start || 0
|
|
28
|
+
const endIdx = end ? Math.min(Math.max(end, startIdx), this.length) : this.length
|
|
29
|
+
|
|
30
|
+
if (encoding === "hex") {
|
|
31
|
+
const buf = new Uint8Array(this.slice(startIdx, endIdx))
|
|
32
|
+
return Array.from(buf, byte => byte.toString(16).padStart(2, "0")).join("")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (encoding === "utf8") {
|
|
36
|
+
return Array.from({ length: endIdx - startIdx }, (_, i) => {
|
|
37
|
+
const charCode = this[i + startIdx]
|
|
38
|
+
// Use unicode replacement character for invalid character codes
|
|
39
|
+
if (
|
|
40
|
+
charCode === undefined ||
|
|
41
|
+
charCode < 0 ||
|
|
42
|
+
charCode > 0x10ffff
|
|
43
|
+
) {
|
|
44
|
+
return String.fromCharCode(0xfffd)
|
|
45
|
+
}
|
|
46
|
+
return String.fromCharCode(charCode)
|
|
47
|
+
}).join("")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (encoding === "base64") {
|
|
51
|
+
const utf8String = Array.from({ length: endIdx - startIdx }, (_, i) => {
|
|
52
|
+
const charCode = this[i + startIdx]
|
|
53
|
+
if (charCode === undefined || charCode < 0 || charCode > 255) {
|
|
54
|
+
return String.fromCharCode(0xfffd)
|
|
55
|
+
}
|
|
56
|
+
return String.fromCharCode(charCode)
|
|
57
|
+
}).join("")
|
|
58
|
+
return btoa(utf8String)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Default to utf8
|
|
62
|
+
return this.toString("utf8", startIdx, endIdx)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create SeaArray from iterable or array-like
|
|
67
|
+
*/
|
|
68
|
+
static override from<T>(
|
|
69
|
+
arrayLike: ArrayLike<T> | Iterable<T>
|
|
70
|
+
): SeaArray {
|
|
71
|
+
const arr = Array.from(arrayLike) as number[]
|
|
72
|
+
const seaArray = new SeaArray(...arr)
|
|
73
|
+
return seaArray
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default SeaArray
|
|
78
|
+
|