@mblaney/holster 2.0.2 → 2.1.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/README.md +37 -6
- 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 +1 -1
- package/src/ham.js +10 -3
- package/src/holster.js +36 -25
- package/src/radisk.js +26 -12
- package/src/user.js +92 -101
- package/src/wire.js +298 -104
- package/ts/src/array.ts +10 -13
- package/ts/src/buffer.ts +40 -20
- package/ts/src/get.ts +5 -6
- package/ts/src/ham.ts +47 -28
- package/ts/src/holster.ts +277 -132
- package/ts/src/index.ts +14 -15
- package/ts/src/radisk.ts +122 -37
- package/ts/src/radix.ts +50 -34
- package/ts/src/schemas.ts +44 -24
- package/ts/src/sea-utils.ts +7 -11
- package/ts/src/sea.ts +51 -54
- package/ts/src/store.ts +48 -42
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +453 -232
package/ts/src/wire.ts
CHANGED
|
@@ -23,13 +23,18 @@ import type {
|
|
|
23
23
|
type UnifiedWebSocket = WebSocket & {
|
|
24
24
|
_retryHandler?: ReturnType<typeof createRetryHandler>
|
|
25
25
|
on?: (event: string, cb: (...args: unknown[]) => void) => void
|
|
26
|
-
send: (
|
|
26
|
+
send: (
|
|
27
|
+
data: string | Buffer | ArrayBuffer | Uint8Array,
|
|
28
|
+
options?: unknown,
|
|
29
|
+
cb?: (err?: Error) => void,
|
|
30
|
+
) => void
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
30
34
|
|
|
31
35
|
// Dynamic import for Node.js ws module - won't execute in browser/service worker
|
|
32
36
|
const wsModule = isNode ? await import("ws") : undefined
|
|
37
|
+
const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
|
|
33
38
|
|
|
34
39
|
if (typeof globalThis.WebSocket === "undefined" && wsModule) {
|
|
35
40
|
globalThis.WebSocket = wsModule.WebSocket as unknown as typeof WebSocket
|
|
@@ -99,7 +104,9 @@ const createRateLimiter = (isTestEnv: boolean) => {
|
|
|
99
104
|
if (!client) return maxRequests
|
|
100
105
|
|
|
101
106
|
const now = Date.now()
|
|
102
|
-
const validRequests = client.requests.filter(
|
|
107
|
+
const validRequests = client.requests.filter(
|
|
108
|
+
time => now - time < windowMs,
|
|
109
|
+
)
|
|
103
110
|
return Math.max(0, maxRequests - validRequests.length)
|
|
104
111
|
},
|
|
105
112
|
|
|
@@ -130,29 +137,29 @@ const createRateLimiter = (isTestEnv: boolean) => {
|
|
|
130
137
|
|
|
131
138
|
const safeJSONParse = (
|
|
132
139
|
data: string | Buffer,
|
|
133
|
-
maxSize = 1024 * 1024
|
|
134
|
-
): {
|
|
140
|
+
maxSize = 1024 * 1024,
|
|
141
|
+
): {success: boolean; data?: WireMessage; error?: string} => {
|
|
135
142
|
try {
|
|
136
143
|
if (typeof data === "string" && data.length > maxSize) {
|
|
137
144
|
throw new Error("Message too large")
|
|
138
145
|
}
|
|
139
|
-
return {
|
|
146
|
+
return {success: true, data: JSON.parse(data.toString())}
|
|
140
147
|
} catch (error) {
|
|
141
|
-
return {
|
|
148
|
+
return {success: false, error: (error as Error).message}
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
const validateMessage = (
|
|
146
153
|
data: string | Buffer,
|
|
147
|
-
maxSize = 1024 * 1024
|
|
148
|
-
): {
|
|
154
|
+
maxSize = 1024 * 1024,
|
|
155
|
+
): {valid: boolean; error?: string} => {
|
|
149
156
|
if (typeof data === "string" && data.length > maxSize) {
|
|
150
|
-
return {
|
|
157
|
+
return {valid: false, error: "Message too large"}
|
|
151
158
|
}
|
|
152
159
|
if (Buffer.isBuffer && Buffer.isBuffer(data) && data.length > maxSize) {
|
|
153
|
-
return {
|
|
160
|
+
return {valid: false, error: "Message too large"}
|
|
154
161
|
}
|
|
155
|
-
return {
|
|
162
|
+
return {valid: true}
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
// ============================================================================
|
|
@@ -240,20 +247,131 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
240
247
|
const hasSoul = async (soul: string): Promise<boolean> => {
|
|
241
248
|
if (graph[soul]) return true
|
|
242
249
|
return new Promise(resolve => {
|
|
243
|
-
store.get({
|
|
250
|
+
store.get({"#": soul}, (err, data) => {
|
|
244
251
|
resolve(!err && !!data && !!data[soul])
|
|
245
252
|
})
|
|
246
253
|
})
|
|
247
254
|
}
|
|
248
255
|
|
|
249
|
-
const isTestEnv =
|
|
256
|
+
const isTestEnv =
|
|
257
|
+
isNode && (globalThis.WebSocket as unknown) !== wsModule?.WebSocket
|
|
250
258
|
const rateLimiter = createRateLimiter(isTestEnv)
|
|
251
|
-
const connectionManager = createConnectionManager(
|
|
259
|
+
const connectionManager = createConnectionManager(
|
|
260
|
+
options.maxConnections || 1000,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// User storage limiting — server-side only, requires options.userLimit: true.
|
|
264
|
+
const DEFAULT_STORAGE_LIMIT = 1 // 1 MB
|
|
265
|
+
const defaultLimit =
|
|
266
|
+
(typeof options.defaultLimit === "number"
|
|
267
|
+
? options.defaultLimit
|
|
268
|
+
: DEFAULT_STORAGE_LIMIT) * 1048576
|
|
269
|
+
const userStorage = new Map<string, number>() // Map<pubkey, totalBytes>
|
|
270
|
+
const userLimits = new Map<string, number>() // Map<pubkey, maxBytes> converted from MB in .user_limit.json
|
|
271
|
+
let saveStorageTimer: ReturnType<typeof setTimeout> | null = null
|
|
272
|
+
|
|
273
|
+
const loadUserStorage = (cb?: () => void): void => {
|
|
274
|
+
fs!.readFile(
|
|
275
|
+
`${options.file}/../.user_storage.json`,
|
|
276
|
+
"utf8",
|
|
277
|
+
(err: NodeJS.ErrnoException | null, raw: string) => {
|
|
278
|
+
if (err) {
|
|
279
|
+
if (cb) cb()
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const data = JSON.parse(raw) as Record<string, unknown>
|
|
284
|
+
userStorage.clear()
|
|
285
|
+
for (const [pub, total] of Object.entries(data)) {
|
|
286
|
+
if (typeof total === "number") userStorage.set(pub, total)
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Invalid JSON — start with empty map.
|
|
290
|
+
}
|
|
291
|
+
if (cb) cb()
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const saveUserStorage = (): void => {
|
|
297
|
+
if (saveStorageTimer) return
|
|
298
|
+
const t = setTimeout(() => {
|
|
299
|
+
saveStorageTimer = null
|
|
300
|
+
const data: Record<string, number> = {}
|
|
301
|
+
for (const [pub, total] of userStorage) {
|
|
302
|
+
data[pub] = total
|
|
303
|
+
}
|
|
304
|
+
fs!.writeFile(
|
|
305
|
+
`${options.file}/../.user_storage.json`,
|
|
306
|
+
JSON.stringify(data),
|
|
307
|
+
(err: NodeJS.ErrnoException | null) => {
|
|
308
|
+
if (err) console.warn("saveUserStorage error:", err)
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
}, 5000)
|
|
312
|
+
if ((t as unknown as {unref?: () => void}).unref)
|
|
313
|
+
(t as unknown as {unref: () => void}).unref()
|
|
314
|
+
saveStorageTimer = t
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const loadUserLimits = (cb?: () => void): void => {
|
|
318
|
+
fs!.readFile(
|
|
319
|
+
`${options.file}/../.user_limit.json`,
|
|
320
|
+
"utf8",
|
|
321
|
+
(err: NodeJS.ErrnoException | null, raw: string) => {
|
|
322
|
+
if (err) {
|
|
323
|
+
if (cb) cb()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const data = JSON.parse(raw) as Record<string, unknown>
|
|
328
|
+
userLimits.clear()
|
|
329
|
+
for (const [pub, limit] of Object.entries(data)) {
|
|
330
|
+
if (typeof limit === "number") userLimits.set(pub, limit * 1048576)
|
|
331
|
+
// Non-numeric values silently ignored — fall back to defaultLimit at use time.
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Invalid JSON — userLimits stays empty.
|
|
335
|
+
}
|
|
336
|
+
if (cb) cb()
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const formatBytes = (bytes: number): string => {
|
|
342
|
+
if (bytes < 1024) return `${bytes} B`
|
|
343
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`
|
|
344
|
+
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`
|
|
345
|
+
return `${(bytes / 1073741824).toFixed(1)} GB`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const logStorageSummary = (): void => {
|
|
349
|
+
const userCount = userStorage.size
|
|
350
|
+
const defLimitStr =
|
|
351
|
+
defaultLimit === 0 ? "unlisted users blocked" : formatBytes(defaultLimit)
|
|
352
|
+
console.log(
|
|
353
|
+
`[HOLSTER] Per-user storage limits enabled (${userCount} user${userCount !== 1 ? "s" : ""}, default: ${defLimitStr})`,
|
|
354
|
+
)
|
|
355
|
+
const sorted = [...userStorage.entries()].sort(([, a], [, b]) => b - a)
|
|
356
|
+
const formatUser = (pub: string): string => {
|
|
357
|
+
const total = userStorage.get(pub) ?? 0
|
|
358
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
359
|
+
const limitStr = limit === 0 ? "blocked" : formatBytes(limit)
|
|
360
|
+
return ` ${pub} — used: ${formatBytes(total)} / limit: ${limitStr}`
|
|
361
|
+
}
|
|
362
|
+
if (userCount <= 5) {
|
|
363
|
+
sorted.forEach(([pub]) => console.log(formatUser(pub)))
|
|
364
|
+
} else {
|
|
365
|
+
console.log(" Top 5 by storage used:")
|
|
366
|
+
sorted.slice(0, 5).forEach(([pub]) => console.log(formatUser(pub)))
|
|
367
|
+
console.log(` Run \`node examples/user-storage.js\` to view all users`)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
252
370
|
|
|
253
371
|
const check = async (
|
|
254
372
|
data: Graph,
|
|
255
|
-
send: (msg: string) => {
|
|
256
|
-
cb?: (err?: string | null) => void
|
|
373
|
+
send: (msg: string) => {err?: string} | void,
|
|
374
|
+
cb?: (err?: string | null) => void,
|
|
257
375
|
): Promise<boolean> => {
|
|
258
376
|
const key = utils.userPublicKey
|
|
259
377
|
|
|
@@ -264,7 +382,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
264
382
|
if (node[key] === undefined) continue
|
|
265
383
|
|
|
266
384
|
const msg = await new Promise<WireMessage>(res => {
|
|
267
|
-
getWithCallback({
|
|
385
|
+
getWithCallback({"#": soul, ".": key}, res, send, {put: true})
|
|
268
386
|
})
|
|
269
387
|
if (msg.err) {
|
|
270
388
|
if (cb) cb(msg.err)
|
|
@@ -275,11 +393,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
275
393
|
// matching public keys, as the provided soul also needs a rel on the
|
|
276
394
|
// parent node which then also requires checking. Otherwise public keys
|
|
277
395
|
// need to match for existing data.
|
|
278
|
-
if (
|
|
279
|
-
!msg.put ||
|
|
280
|
-
!msg.put[soul] ||
|
|
281
|
-
msg.put[soul]![key] === node[key]
|
|
282
|
-
) {
|
|
396
|
+
if (!msg.put || !msg.put[soul] || msg.put[soul]![key] === node[key]) {
|
|
283
397
|
continue
|
|
284
398
|
}
|
|
285
399
|
|
|
@@ -292,7 +406,10 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
292
406
|
return true
|
|
293
407
|
}
|
|
294
408
|
|
|
295
|
-
const get = (
|
|
409
|
+
const get = (
|
|
410
|
+
msg: {get: Lex; "#": string},
|
|
411
|
+
send: (msg: string) => void,
|
|
412
|
+
): void => {
|
|
296
413
|
const ack = Get(msg.get, graph)
|
|
297
414
|
if (ack) {
|
|
298
415
|
send(
|
|
@@ -300,7 +417,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
300
417
|
"#": dup.track(utils.text.random(9)),
|
|
301
418
|
"@": msg["#"],
|
|
302
419
|
put: ack,
|
|
303
|
-
})
|
|
420
|
+
}),
|
|
304
421
|
)
|
|
305
422
|
} else {
|
|
306
423
|
store.get(msg.get, (err, ack) => {
|
|
@@ -310,48 +427,99 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
310
427
|
"@": msg["#"],
|
|
311
428
|
put: ack,
|
|
312
429
|
err: err,
|
|
313
|
-
})
|
|
430
|
+
}),
|
|
314
431
|
)
|
|
315
432
|
})
|
|
316
433
|
}
|
|
317
434
|
}
|
|
318
435
|
|
|
319
436
|
const put = async (
|
|
320
|
-
msg: {
|
|
321
|
-
send: (msg: string) => void
|
|
437
|
+
msg: {put: Graph; "#": string},
|
|
438
|
+
send: (msg: string) => void,
|
|
322
439
|
): Promise<void> => {
|
|
323
|
-
|
|
440
|
+
// Capture old graph sizes for user souls before Ham.mix updates graph in place.
|
|
441
|
+
const oldGraphBytes: Record<string, number> = {}
|
|
442
|
+
if (options.userLimit) {
|
|
443
|
+
for (const [soul, node] of Object.entries(msg.put)) {
|
|
444
|
+
if (node && node[utils.userPublicKey] && graph[soul]) {
|
|
445
|
+
oldGraphBytes[soul] = JSON.stringify(graph[soul]).length
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const update = await Ham.mix(
|
|
451
|
+
msg.put,
|
|
452
|
+
graph,
|
|
453
|
+
hasNode,
|
|
454
|
+
options.secure || false,
|
|
455
|
+
listen,
|
|
456
|
+
options.maxGraphSize,
|
|
457
|
+
)
|
|
324
458
|
if (Object.keys(update.now).length === 0) {
|
|
325
459
|
if (Object.keys(update.defer).length !== 0) {
|
|
326
|
-
setTimeout(
|
|
460
|
+
setTimeout(
|
|
461
|
+
() => put({put: update.defer, "#": msg["#"]}, send),
|
|
462
|
+
update.wait,
|
|
463
|
+
)
|
|
327
464
|
}
|
|
328
465
|
return
|
|
329
466
|
}
|
|
330
467
|
|
|
331
468
|
if (!(await check(update.now, send as never))) return
|
|
332
469
|
|
|
333
|
-
|
|
470
|
+
const finish = (err?: string | null): void => {
|
|
334
471
|
if (err) console.warn("store.put", err)
|
|
335
472
|
send(
|
|
336
473
|
JSON.stringify({
|
|
337
474
|
"#": dup.track(utils.text.random(9)),
|
|
338
475
|
"@": msg["#"],
|
|
339
476
|
err: err,
|
|
340
|
-
})
|
|
477
|
+
}),
|
|
341
478
|
)
|
|
342
479
|
update.listeners.forEach(cb => cb())
|
|
343
|
-
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!options.userLimit) {
|
|
483
|
+
store.put(update.now, finish)
|
|
484
|
+
} else {
|
|
485
|
+
const toStore: Graph = {}
|
|
486
|
+
for (const [soul, node] of Object.entries(update.now)) {
|
|
487
|
+
const pub = node![utils.userPublicKey] as string | undefined
|
|
488
|
+
if (!pub) {
|
|
489
|
+
toStore[soul] = node
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
492
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
493
|
+
if ((userStorage.get(pub) ?? 0) >= limit) continue
|
|
494
|
+
toStore[soul] = node
|
|
495
|
+
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
496
|
+
const newBytes = JSON.stringify(node).length
|
|
497
|
+
userStorage.set(
|
|
498
|
+
pub,
|
|
499
|
+
Math.max(0, (userStorage.get(pub) ?? 0) - oldBytes + newBytes),
|
|
500
|
+
)
|
|
501
|
+
saveUserStorage()
|
|
502
|
+
}
|
|
503
|
+
if (Object.keys(toStore).length > 0) {
|
|
504
|
+
store.put(toStore, finish)
|
|
505
|
+
} else {
|
|
506
|
+
finish(null)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
344
509
|
|
|
345
510
|
if (Object.keys(update.defer).length !== 0) {
|
|
346
|
-
setTimeout(
|
|
511
|
+
setTimeout(
|
|
512
|
+
() => put({put: update.defer, "#": msg["#"]}, send),
|
|
513
|
+
update.wait,
|
|
514
|
+
)
|
|
347
515
|
}
|
|
348
516
|
}
|
|
349
517
|
|
|
350
518
|
const getWithCallback = (
|
|
351
519
|
lex: Lex,
|
|
352
520
|
cb: (msg: WireMessage) => void,
|
|
353
|
-
send: (msg: string) => {
|
|
354
|
-
_opt?: WireOptions
|
|
521
|
+
send: (msg: string) => {err?: string} | void,
|
|
522
|
+
_opt?: WireOptions,
|
|
355
523
|
): void => {
|
|
356
524
|
if (!cb) return
|
|
357
525
|
|
|
@@ -377,7 +545,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
377
545
|
recentChecks.set(checkKey, now)
|
|
378
546
|
const sendResult = send(request)
|
|
379
547
|
if (sendResult && sendResult.err) {
|
|
380
|
-
cb({
|
|
548
|
+
cb({err: sendResult.err})
|
|
381
549
|
return
|
|
382
550
|
}
|
|
383
551
|
}
|
|
@@ -385,117 +553,115 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
385
553
|
// Don't skip checks for other types of lex queries.
|
|
386
554
|
const sendResult = send(request)
|
|
387
555
|
if (sendResult && sendResult.err) {
|
|
388
|
-
cb({
|
|
556
|
+
cb({err: sendResult.err})
|
|
389
557
|
return
|
|
390
558
|
}
|
|
391
559
|
}
|
|
392
560
|
}
|
|
393
|
-
cb({
|
|
561
|
+
cb({put: ack})
|
|
394
562
|
return
|
|
395
563
|
}
|
|
396
564
|
|
|
397
|
-
store.get(
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
gMeta["
|
|
420
|
-
|
|
421
|
-
if (!gMeta["s"]) gMeta["s"] = {}
|
|
422
|
-
gMeta["s"]![state] = meta["s"][state]
|
|
423
|
-
}
|
|
565
|
+
store.get(lex, (err, ack) => {
|
|
566
|
+
if (ack) {
|
|
567
|
+
// Populate in-memory graph from store data before sending the server
|
|
568
|
+
// request, so stale server responses with older state are correctly
|
|
569
|
+
// rejected by ham.mix.
|
|
570
|
+
for (const [soul, node] of Object.entries(ack)) {
|
|
571
|
+
if (!node) continue
|
|
572
|
+
if (!graph[soul]) {
|
|
573
|
+
graph[soul] = {...node} as unknown as Graph[string]
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
for (const [key, value] of Object.entries(node)) {
|
|
577
|
+
if (key === "_") continue
|
|
578
|
+
const gNode = graph[soul] as unknown as Record<string, unknown>
|
|
579
|
+
if (typeof gNode[key] !== "undefined") continue
|
|
580
|
+
gNode[key] = value
|
|
581
|
+
const meta = (node as unknown as GraphNode)._
|
|
582
|
+
if (meta?.[">"] && typeof meta[">"][key] !== "undefined") {
|
|
583
|
+
const state = meta[">"][key]
|
|
584
|
+
const gMeta = (graph[soul] as unknown as GraphNode)._
|
|
585
|
+
gMeta[">"][key] = state
|
|
586
|
+
if (meta["s"]?.[state]) {
|
|
587
|
+
if (!gMeta["s"]) gMeta["s"] = {}
|
|
588
|
+
gMeta["s"]![state] = meta["s"][state]
|
|
424
589
|
}
|
|
425
590
|
}
|
|
426
591
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (sendResult && sendResult.err) {
|
|
447
|
-
cb({ err: sendResult.err })
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
} else {
|
|
592
|
+
}
|
|
593
|
+
// Full-node fetch: the graph is now complete for this soul.
|
|
594
|
+
if (!lex["."] && ack[lex["#"]]) hasNode.add(lex["#"])
|
|
595
|
+
// Only serve from store if the requested property is present.
|
|
596
|
+
const node = ack[lex["#"]]
|
|
597
|
+
const hasProperty =
|
|
598
|
+
node &&
|
|
599
|
+
Object.keys(node).some(
|
|
600
|
+
key => key !== "_" && utils.match(lex["."], key),
|
|
601
|
+
)
|
|
602
|
+
if (hasProperty) {
|
|
603
|
+
// Public keys are immutable once written — never need a wire check.
|
|
604
|
+
if (!isNode && lex["."] !== utils.userPublicKey) {
|
|
605
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
606
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
607
|
+
const now = Date.now()
|
|
608
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
609
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
610
|
+
recentChecks.set(checkKey, now)
|
|
452
611
|
const sendResult = send(request)
|
|
453
612
|
if (sendResult && sendResult.err) {
|
|
454
|
-
cb({
|
|
613
|
+
cb({err: sendResult.err})
|
|
455
614
|
return
|
|
456
615
|
}
|
|
457
616
|
}
|
|
617
|
+
} else {
|
|
618
|
+
const sendResult = send(request)
|
|
619
|
+
if (sendResult && sendResult.err) {
|
|
620
|
+
cb({err: sendResult.err})
|
|
621
|
+
return
|
|
622
|
+
}
|
|
458
623
|
}
|
|
459
|
-
cb(err ? { put: ack, err: err } : { put: ack })
|
|
460
|
-
return
|
|
461
624
|
}
|
|
625
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
626
|
+
return
|
|
462
627
|
}
|
|
628
|
+
}
|
|
463
629
|
|
|
464
|
-
|
|
630
|
+
if (err) console.log(err)
|
|
465
631
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
cb({put: ack as never})
|
|
474
|
-
return
|
|
632
|
+
// Put requests only need a local answer — skip the wire round-trip.
|
|
633
|
+
if (opts && opts.put) {
|
|
634
|
+
const id = lex["#"]
|
|
635
|
+
const ack: Record<string, null | Record<string, null>> = {[id]: null}
|
|
636
|
+
if (typeof lex["."] === "string") {
|
|
637
|
+
ack[id] = {[lex["."]]: null}
|
|
475
638
|
}
|
|
639
|
+
cb({put: ack as never})
|
|
640
|
+
return
|
|
641
|
+
}
|
|
476
642
|
|
|
477
|
-
|
|
643
|
+
queue[track] = cb
|
|
478
644
|
|
|
479
|
-
|
|
645
|
+
pendingTimeouts.set(track, lex)
|
|
480
646
|
|
|
481
|
-
|
|
482
|
-
|
|
647
|
+
// Ensure the requested soul is stored when the response arrives.
|
|
648
|
+
pendingReferences.add(lex["#"])
|
|
483
649
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
650
|
+
const sendResult = send(request)
|
|
651
|
+
if (sendResult && sendResult.err) {
|
|
652
|
+
cb({err: sendResult.err})
|
|
653
|
+
delete queue[track]
|
|
654
|
+
pendingTimeouts.delete(track)
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
})
|
|
492
658
|
}
|
|
493
659
|
|
|
494
|
-
const api = (send: (msg: string) => {
|
|
660
|
+
const api = (send: (msg: string) => {err?: string} | void): WireAPI => {
|
|
495
661
|
return {
|
|
496
662
|
get: (lex, cb, _opt) => {
|
|
497
663
|
if (lex && typeof lex["."] === "number") {
|
|
498
|
-
lex = {
|
|
664
|
+
lex = {...lex, ".": String(lex["."])}
|
|
499
665
|
}
|
|
500
666
|
if (lex && lex["#"]) {
|
|
501
667
|
pendingReferences.add(lex["#"])
|
|
@@ -503,7 +669,22 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
503
669
|
getWithCallback(lex, cb, send, _opt)
|
|
504
670
|
},
|
|
505
671
|
put: async (data, cb) => {
|
|
506
|
-
const
|
|
672
|
+
const oldGraphBytes: Record<string, number> = {}
|
|
673
|
+
if (options.userLimit) {
|
|
674
|
+
for (const [soul, node] of Object.entries(data)) {
|
|
675
|
+
if (node && node[utils.userPublicKey] && graph[soul]) {
|
|
676
|
+
oldGraphBytes[soul] = JSON.stringify(graph[soul]).length
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const update = await Ham.mix(
|
|
681
|
+
data,
|
|
682
|
+
graph,
|
|
683
|
+
hasNode,
|
|
684
|
+
options.secure || false,
|
|
685
|
+
listen,
|
|
686
|
+
options.maxGraphSize,
|
|
687
|
+
)
|
|
507
688
|
if (Object.keys(update.now).length === 0) {
|
|
508
689
|
if (cb) cb(null)
|
|
509
690
|
return
|
|
@@ -513,6 +694,23 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
513
694
|
return
|
|
514
695
|
}
|
|
515
696
|
|
|
697
|
+
if (options.userLimit) {
|
|
698
|
+
for (const [soul, node] of Object.entries(update.now)) {
|
|
699
|
+
const pub = node![utils.userPublicKey] as string | undefined
|
|
700
|
+
if (!pub) continue
|
|
701
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
702
|
+
const current = userStorage.get(pub) ?? 0
|
|
703
|
+
if (current >= limit) {
|
|
704
|
+
if (cb) cb("storage limit exceeded")
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
708
|
+
const newBytes = JSON.stringify(node).length
|
|
709
|
+
userStorage.set(pub, Math.max(0, current - oldBytes + newBytes))
|
|
710
|
+
saveUserStorage()
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
516
714
|
for (const [_soul, node] of Object.entries(update.now)) {
|
|
517
715
|
if (node && typeof node === "object") {
|
|
518
716
|
for (const [_key, value] of Object.entries(node)) {
|
|
@@ -533,7 +731,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
533
731
|
JSON.stringify({
|
|
534
732
|
"#": dup.track(utils.text.random(9)),
|
|
535
733
|
put: data,
|
|
536
|
-
})
|
|
734
|
+
}),
|
|
537
735
|
)
|
|
538
736
|
if (sendResult && sendResult.err) {
|
|
539
737
|
if (cb) cb(sendResult.err)
|
|
@@ -542,15 +740,15 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
542
740
|
},
|
|
543
741
|
on: (lex, cb, _get, _opt) => {
|
|
544
742
|
if (lex && typeof lex["."] === "number") {
|
|
545
|
-
lex = {
|
|
743
|
+
lex = {...lex, ".": String(lex["."])}
|
|
546
744
|
}
|
|
547
745
|
const soul = lex && lex["#"]
|
|
548
746
|
if (!soul || !cb) return
|
|
549
747
|
|
|
550
748
|
if (listen[soul]) {
|
|
551
|
-
listen[soul]!.push({
|
|
749
|
+
listen[soul]!.push({".": lex["."], cb: cb as never})
|
|
552
750
|
} else {
|
|
553
|
-
listen[soul] = [{
|
|
751
|
+
listen[soul] = [{".": lex["."], cb: cb as never}]
|
|
554
752
|
}
|
|
555
753
|
if (_get) getWithCallback(lex, cb as never, send, _opt)
|
|
556
754
|
},
|
|
@@ -573,13 +771,14 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
573
771
|
if (isNode && (options.wss || options.server || options.port != null)) {
|
|
574
772
|
let wss = options.wss
|
|
575
773
|
let clients = (): WebSocket[] =>
|
|
576
|
-
(wss as {
|
|
774
|
+
(wss as {clients?: () => WebSocket[]}).clients?.() || []
|
|
577
775
|
if (!wss) {
|
|
578
776
|
const config = options.server
|
|
579
|
-
? {
|
|
580
|
-
: {
|
|
777
|
+
? {server: options.server}
|
|
778
|
+
: {port: options.port}
|
|
581
779
|
wss = new wsModule!.WebSocketServer(config)
|
|
582
|
-
clients = () =>
|
|
780
|
+
clients = () =>
|
|
781
|
+
Array.from((wss as {clients: Set<WebSocket>}).clients || [])
|
|
583
782
|
}
|
|
584
783
|
|
|
585
784
|
const send = (data: string, isBinary?: boolean): void => {
|
|
@@ -589,24 +788,27 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
589
788
|
const lex = pendingTimeouts.get(trackId)!
|
|
590
789
|
pendingTimeouts.delete(trackId)
|
|
591
790
|
|
|
592
|
-
setTimeout(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
791
|
+
setTimeout(
|
|
792
|
+
() => {
|
|
793
|
+
const cb = queue[trackId]
|
|
794
|
+
if (cb) {
|
|
795
|
+
const id = lex["#"]
|
|
796
|
+
const ack: Graph = {[id]: null as never}
|
|
797
|
+
if (typeof lex["."] === "string") {
|
|
798
|
+
ack[id] = {[lex["."]]: null} as never
|
|
799
|
+
}
|
|
800
|
+
cb({put: ack})
|
|
801
|
+
delete queue[trackId]
|
|
599
802
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}, isTestEnv ? 100 : 10000)
|
|
803
|
+
},
|
|
804
|
+
isTestEnv ? 100 : 10000,
|
|
805
|
+
)
|
|
604
806
|
}
|
|
605
807
|
|
|
606
808
|
clients().forEach(client => {
|
|
607
809
|
const ws = client as UnifiedWebSocket
|
|
608
810
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
609
|
-
ws.send(data, {
|
|
811
|
+
ws.send(data, {binary: isBinary})
|
|
610
812
|
} else if (ws) {
|
|
611
813
|
const retryHandler = ws._retryHandler || createRetryHandler()
|
|
612
814
|
ws._retryHandler = retryHandler
|
|
@@ -618,7 +820,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
618
820
|
setTimeout(() => {
|
|
619
821
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
620
822
|
retryHandler.reset()
|
|
621
|
-
ws.send(data, {
|
|
823
|
+
ws.send(data, {binary: isBinary})
|
|
622
824
|
}
|
|
623
825
|
}, delay)
|
|
624
826
|
}
|
|
@@ -626,100 +828,106 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
626
828
|
})
|
|
627
829
|
}
|
|
628
830
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
(
|
|
632
|
-
|
|
633
|
-
console.log("Connection limit reached, rejecting connection")
|
|
634
|
-
ws.close(1013, "Connection limit reached - try again later")
|
|
635
|
-
return
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const clientId = utils.text.random(9)
|
|
639
|
-
console.log(`New WebSocket client connected: ${clientId}`)
|
|
640
|
-
|
|
641
|
-
ws.on?.("error", ((...args: unknown[]) => {
|
|
642
|
-
const error = args[0] as Error
|
|
643
|
-
console.log("WebSocket error:", error)
|
|
644
|
-
connectionManager.remove(ws)
|
|
645
|
-
}) as (...args: unknown[]) => void)
|
|
831
|
+
if (options.userLimit) {
|
|
832
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
833
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
834
|
+
}
|
|
646
835
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
836
|
+
;(
|
|
837
|
+
wss as unknown as {
|
|
838
|
+
on: (event: string, cb: (ws: UnifiedWebSocket) => void) => void
|
|
839
|
+
}
|
|
840
|
+
).on("connection", (ws: UnifiedWebSocket) => {
|
|
841
|
+
if (!connectionManager.add(ws)) {
|
|
842
|
+
console.log("Connection limit reached, rejecting connection")
|
|
843
|
+
ws.close(1013, "Connection limit reached - try again later")
|
|
844
|
+
return
|
|
845
|
+
}
|
|
651
846
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
847
|
+
const clientId = utils.text.random(9)
|
|
848
|
+
console.log(`New WebSocket client connected: ${clientId}`)
|
|
849
|
+
|
|
850
|
+
ws.on?.("error", ((...args: unknown[]) => {
|
|
851
|
+
const error = args[0] as Error
|
|
852
|
+
console.log("WebSocket error:", error)
|
|
853
|
+
connectionManager.remove(ws)
|
|
854
|
+
}) as (...args: unknown[]) => void)
|
|
855
|
+
|
|
856
|
+
ws.on?.("close", (() => {
|
|
857
|
+
console.log(`WebSocket client disconnected: ${clientId}`)
|
|
858
|
+
connectionManager.remove(ws)
|
|
859
|
+
}) as (...args: unknown[]) => void)
|
|
860
|
+
|
|
861
|
+
ws.on?.("message", ((...args: unknown[]) => {
|
|
862
|
+
const data = args[0] as Buffer
|
|
863
|
+
const isBinary = args[1] as boolean
|
|
864
|
+
const validation = validateMessage(data, options.maxMessageSize)
|
|
865
|
+
if (!validation.valid) {
|
|
866
|
+
console.warn(`Invalid message: ${validation.error}`)
|
|
867
|
+
ws.send(JSON.stringify({error: validation.error}))
|
|
868
|
+
return
|
|
869
|
+
}
|
|
661
870
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
871
|
+
const parseResult = safeJSONParse(data, options.maxMessageSize)
|
|
872
|
+
if (!parseResult.success) {
|
|
873
|
+
console.warn(`JSON parse error: ${parseResult.error}`)
|
|
874
|
+
ws.send(JSON.stringify({error: "Invalid JSON"}))
|
|
875
|
+
return
|
|
876
|
+
}
|
|
668
877
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
878
|
+
const msg = parseResult.data!
|
|
879
|
+
if (!msg["#"]) return // Incoming messages must have '#'
|
|
880
|
+
if (dup.check(msg["#"])) return
|
|
672
881
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
882
|
+
const delay = rateLimiter.getDelay(clientId)
|
|
883
|
+
if (delay > 0) {
|
|
884
|
+
const throttleCount = rateLimiter.getThrottleCount(clientId)
|
|
885
|
+
console.log(
|
|
886
|
+
`Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message (throttle count: ${throttleCount})`,
|
|
887
|
+
)
|
|
888
|
+
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
676
889
|
console.log(
|
|
677
|
-
`Client ${clientId}:
|
|
678
|
-
)
|
|
679
|
-
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
680
|
-
console.log(
|
|
681
|
-
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`
|
|
682
|
-
)
|
|
683
|
-
ws.close(1008, "Rate limit violations")
|
|
684
|
-
return
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
ws.send(
|
|
688
|
-
JSON.stringify({
|
|
689
|
-
"#": dup.track(utils.text.random(9)),
|
|
690
|
-
"@": msg["#"],
|
|
691
|
-
err: `Rate limit exceeded. Slow down requests. Wait ${Math.ceil(delay / 1000)}s`,
|
|
692
|
-
throttle: delay,
|
|
693
|
-
})
|
|
890
|
+
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`,
|
|
694
891
|
)
|
|
892
|
+
ws.close(1008, "Rate limit violations")
|
|
695
893
|
return
|
|
696
894
|
}
|
|
697
895
|
|
|
698
|
-
|
|
699
|
-
|
|
896
|
+
ws.send(
|
|
897
|
+
JSON.stringify({
|
|
898
|
+
"#": dup.track(utils.text.random(9)),
|
|
899
|
+
"@": msg["#"],
|
|
900
|
+
err: `Rate limit exceeded. Slow down requests. Wait ${Math.ceil(delay / 1000)}s`,
|
|
901
|
+
throttle: delay,
|
|
902
|
+
}),
|
|
903
|
+
)
|
|
904
|
+
return
|
|
905
|
+
}
|
|
700
906
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
send(data.toString(), isBinary)
|
|
907
|
+
const processMessage = async (): Promise<void> => {
|
|
908
|
+
dup.track(msg["#"]!)
|
|
704
909
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
delete (msg as { "#"?: string })["#"]
|
|
709
|
-
delete (msg as { "@"?: string })["@"]
|
|
710
|
-
cb(msg)
|
|
711
|
-
delete queue[id!]
|
|
712
|
-
}
|
|
713
|
-
}
|
|
910
|
+
if (msg.get) get(msg as never, send)
|
|
911
|
+
if (msg.put) await put(msg as never, send)
|
|
912
|
+
send(data.toString(), isBinary)
|
|
714
913
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
914
|
+
const id = msg["@"]
|
|
915
|
+
const cb = queue[id!]
|
|
916
|
+
if (cb) {
|
|
917
|
+
delete (msg as {"#"?: string})["#"]
|
|
918
|
+
delete (msg as {"@"?: string})["@"]
|
|
919
|
+
cb(msg)
|
|
920
|
+
delete queue[id!]
|
|
719
921
|
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (delay > 0) {
|
|
925
|
+
setTimeout(processMessage, delay)
|
|
926
|
+
} else {
|
|
927
|
+
processMessage()
|
|
928
|
+
}
|
|
929
|
+
}) as (...args: unknown[]) => void)
|
|
930
|
+
})
|
|
723
931
|
return api(send)
|
|
724
932
|
}
|
|
725
933
|
|
|
@@ -732,18 +940,22 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
732
940
|
let queueProcessor: NodeJS.Timeout | null = null
|
|
733
941
|
const maxQueueLength = options.maxQueueLength || 10000
|
|
734
942
|
|
|
735
|
-
const startResponseTimeout = (
|
|
943
|
+
const startResponseTimeout = (
|
|
944
|
+
trackId: string,
|
|
945
|
+
lex: Lex,
|
|
946
|
+
delay = isTestEnv ? 100 : 10000,
|
|
947
|
+
): ReturnType<typeof setTimeout> => {
|
|
736
948
|
return setTimeout(() => {
|
|
737
949
|
offlineTimeouts.delete(trackId)
|
|
738
950
|
pendingTimeouts.delete(trackId)
|
|
739
951
|
const cb = queue[trackId]
|
|
740
952
|
if (cb) {
|
|
741
953
|
const id = lex["#"]
|
|
742
|
-
const ack: Graph = {
|
|
954
|
+
const ack: Graph = {[id]: null as never}
|
|
743
955
|
if (typeof lex["."] === "string") {
|
|
744
|
-
ack[id] = {
|
|
956
|
+
ack[id] = {[lex["."]]: null} as never
|
|
745
957
|
}
|
|
746
|
-
cb({
|
|
958
|
+
cb({put: ack})
|
|
747
959
|
delete queue[trackId]
|
|
748
960
|
}
|
|
749
961
|
}, delay)
|
|
@@ -757,7 +969,10 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
757
969
|
|
|
758
970
|
const now = Date.now()
|
|
759
971
|
if (clientThrottled && now < throttleUntil) {
|
|
760
|
-
queueProcessor = setTimeout(
|
|
972
|
+
queueProcessor = setTimeout(
|
|
973
|
+
processQueue,
|
|
974
|
+
Math.min(1000, throttleUntil - now),
|
|
975
|
+
)
|
|
761
976
|
return
|
|
762
977
|
}
|
|
763
978
|
|
|
@@ -775,7 +990,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
775
990
|
|
|
776
991
|
// Drop GET messages with no pending callback — these were already served
|
|
777
992
|
// from local store so no caller is waiting for the wire response.
|
|
778
|
-
const isStaleCheckMessage =
|
|
993
|
+
const isStaleCheckMessage =
|
|
994
|
+
!sent && !!msg.get && !pendingTimeouts.has(trackId!)
|
|
779
995
|
|
|
780
996
|
if (sent || isStaleCheckMessage) {
|
|
781
997
|
messageQueue.shift()
|
|
@@ -833,7 +1049,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
833
1049
|
return sentToAtLeastOne
|
|
834
1050
|
}
|
|
835
1051
|
|
|
836
|
-
const send = (data: string): {
|
|
1052
|
+
const send = (data: string): {err?: string} | void => {
|
|
837
1053
|
if (messageQueue.length >= maxQueueLength) {
|
|
838
1054
|
return {
|
|
839
1055
|
err: `Message queue exceeded maximum length (${maxQueueLength}). Update query logic to request less data.`,
|
|
@@ -866,7 +1082,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
866
1082
|
const delay = isTestEnv ? 50 : retryHandler.getDelay()
|
|
867
1083
|
retryHandler.increment()
|
|
868
1084
|
const t = setTimeout(start, delay)
|
|
869
|
-
if ((t as unknown as {
|
|
1085
|
+
if ((t as unknown as {unref?: () => void}).unref)
|
|
1086
|
+
(t as unknown as {unref: () => void}).unref()
|
|
870
1087
|
}
|
|
871
1088
|
}
|
|
872
1089
|
|
|
@@ -910,7 +1127,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
910
1127
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
911
1128
|
const known = await hasSoul(soul)
|
|
912
1129
|
hasSoulMap.set(soul, known)
|
|
913
|
-
if (
|
|
1130
|
+
if (
|
|
1131
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1132
|
+
!node ||
|
|
1133
|
+
typeof node !== "object"
|
|
1134
|
+
)
|
|
1135
|
+
continue
|
|
914
1136
|
for (const [, value] of Object.entries(node)) {
|
|
915
1137
|
const soulId = utils.rel.is(value as GraphValue)
|
|
916
1138
|
if (soulId) pendingReferences.add(soulId)
|
|
@@ -939,7 +1161,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
939
1161
|
// Store filtered data if any
|
|
940
1162
|
if (Object.keys(filteredPut).length > 0) {
|
|
941
1163
|
// Create filtered message for Ham.mix with original message ID
|
|
942
|
-
const filteredMsg = {
|
|
1164
|
+
const filteredMsg = {put: filteredPut, "#": msg["#"]!}
|
|
943
1165
|
await put(filteredMsg, send as never)
|
|
944
1166
|
}
|
|
945
1167
|
|
|
@@ -952,8 +1174,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
952
1174
|
const id = msg["@"]
|
|
953
1175
|
const cb = queue[id!]
|
|
954
1176
|
if (cb) {
|
|
955
|
-
delete (msg as {
|
|
956
|
-
delete (msg as {
|
|
1177
|
+
delete (msg as {"#"?: string})["#"]
|
|
1178
|
+
delete (msg as {"@"?: string})["@"]
|
|
957
1179
|
cb(msg)
|
|
958
1180
|
delete queue[id!]
|
|
959
1181
|
}
|
|
@@ -966,4 +1188,3 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
966
1188
|
}
|
|
967
1189
|
|
|
968
1190
|
export default Wire
|
|
969
|
-
|