@mblaney/holster 2.0.2 → 2.1.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/README.md +43 -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 +2 -2
- 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 +300 -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 +455 -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 = graph[soul] ? JSON.stringify(graph[soul]).length : 0
|
|
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,25 @@ 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 = graph[soul]
|
|
709
|
+
? JSON.stringify(graph[soul]).length
|
|
710
|
+
: 0
|
|
711
|
+
userStorage.set(pub, Math.max(0, current - oldBytes + newBytes))
|
|
712
|
+
saveUserStorage()
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
516
716
|
for (const [_soul, node] of Object.entries(update.now)) {
|
|
517
717
|
if (node && typeof node === "object") {
|
|
518
718
|
for (const [_key, value] of Object.entries(node)) {
|
|
@@ -533,7 +733,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
533
733
|
JSON.stringify({
|
|
534
734
|
"#": dup.track(utils.text.random(9)),
|
|
535
735
|
put: data,
|
|
536
|
-
})
|
|
736
|
+
}),
|
|
537
737
|
)
|
|
538
738
|
if (sendResult && sendResult.err) {
|
|
539
739
|
if (cb) cb(sendResult.err)
|
|
@@ -542,15 +742,15 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
542
742
|
},
|
|
543
743
|
on: (lex, cb, _get, _opt) => {
|
|
544
744
|
if (lex && typeof lex["."] === "number") {
|
|
545
|
-
lex = {
|
|
745
|
+
lex = {...lex, ".": String(lex["."])}
|
|
546
746
|
}
|
|
547
747
|
const soul = lex && lex["#"]
|
|
548
748
|
if (!soul || !cb) return
|
|
549
749
|
|
|
550
750
|
if (listen[soul]) {
|
|
551
|
-
listen[soul]!.push({
|
|
751
|
+
listen[soul]!.push({".": lex["."], cb: cb as never})
|
|
552
752
|
} else {
|
|
553
|
-
listen[soul] = [{
|
|
753
|
+
listen[soul] = [{".": lex["."], cb: cb as never}]
|
|
554
754
|
}
|
|
555
755
|
if (_get) getWithCallback(lex, cb as never, send, _opt)
|
|
556
756
|
},
|
|
@@ -573,13 +773,14 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
573
773
|
if (isNode && (options.wss || options.server || options.port != null)) {
|
|
574
774
|
let wss = options.wss
|
|
575
775
|
let clients = (): WebSocket[] =>
|
|
576
|
-
(wss as {
|
|
776
|
+
(wss as {clients?: () => WebSocket[]}).clients?.() || []
|
|
577
777
|
if (!wss) {
|
|
578
778
|
const config = options.server
|
|
579
|
-
? {
|
|
580
|
-
: {
|
|
779
|
+
? {server: options.server}
|
|
780
|
+
: {port: options.port}
|
|
581
781
|
wss = new wsModule!.WebSocketServer(config)
|
|
582
|
-
clients = () =>
|
|
782
|
+
clients = () =>
|
|
783
|
+
Array.from((wss as {clients: Set<WebSocket>}).clients || [])
|
|
583
784
|
}
|
|
584
785
|
|
|
585
786
|
const send = (data: string, isBinary?: boolean): void => {
|
|
@@ -589,24 +790,27 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
589
790
|
const lex = pendingTimeouts.get(trackId)!
|
|
590
791
|
pendingTimeouts.delete(trackId)
|
|
591
792
|
|
|
592
|
-
setTimeout(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
793
|
+
setTimeout(
|
|
794
|
+
() => {
|
|
795
|
+
const cb = queue[trackId]
|
|
796
|
+
if (cb) {
|
|
797
|
+
const id = lex["#"]
|
|
798
|
+
const ack: Graph = {[id]: null as never}
|
|
799
|
+
if (typeof lex["."] === "string") {
|
|
800
|
+
ack[id] = {[lex["."]]: null} as never
|
|
801
|
+
}
|
|
802
|
+
cb({put: ack})
|
|
803
|
+
delete queue[trackId]
|
|
599
804
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}, isTestEnv ? 100 : 10000)
|
|
805
|
+
},
|
|
806
|
+
isTestEnv ? 100 : 10000,
|
|
807
|
+
)
|
|
604
808
|
}
|
|
605
809
|
|
|
606
810
|
clients().forEach(client => {
|
|
607
811
|
const ws = client as UnifiedWebSocket
|
|
608
812
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
609
|
-
ws.send(data, {
|
|
813
|
+
ws.send(data, {binary: isBinary})
|
|
610
814
|
} else if (ws) {
|
|
611
815
|
const retryHandler = ws._retryHandler || createRetryHandler()
|
|
612
816
|
ws._retryHandler = retryHandler
|
|
@@ -618,7 +822,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
618
822
|
setTimeout(() => {
|
|
619
823
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
620
824
|
retryHandler.reset()
|
|
621
|
-
ws.send(data, {
|
|
825
|
+
ws.send(data, {binary: isBinary})
|
|
622
826
|
}
|
|
623
827
|
}, delay)
|
|
624
828
|
}
|
|
@@ -626,100 +830,106 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
626
830
|
})
|
|
627
831
|
}
|
|
628
832
|
|
|
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)
|
|
833
|
+
if (options.userLimit) {
|
|
834
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
835
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
836
|
+
}
|
|
646
837
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
838
|
+
;(
|
|
839
|
+
wss as unknown as {
|
|
840
|
+
on: (event: string, cb: (ws: UnifiedWebSocket) => void) => void
|
|
841
|
+
}
|
|
842
|
+
).on("connection", (ws: UnifiedWebSocket) => {
|
|
843
|
+
if (!connectionManager.add(ws)) {
|
|
844
|
+
console.log("Connection limit reached, rejecting connection")
|
|
845
|
+
ws.close(1013, "Connection limit reached - try again later")
|
|
846
|
+
return
|
|
847
|
+
}
|
|
651
848
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
849
|
+
const clientId = utils.text.random(9)
|
|
850
|
+
console.log(`New WebSocket client connected: ${clientId}`)
|
|
851
|
+
|
|
852
|
+
ws.on?.("error", ((...args: unknown[]) => {
|
|
853
|
+
const error = args[0] as Error
|
|
854
|
+
console.log("WebSocket error:", error)
|
|
855
|
+
connectionManager.remove(ws)
|
|
856
|
+
}) as (...args: unknown[]) => void)
|
|
857
|
+
|
|
858
|
+
ws.on?.("close", (() => {
|
|
859
|
+
console.log(`WebSocket client disconnected: ${clientId}`)
|
|
860
|
+
connectionManager.remove(ws)
|
|
861
|
+
}) as (...args: unknown[]) => void)
|
|
862
|
+
|
|
863
|
+
ws.on?.("message", ((...args: unknown[]) => {
|
|
864
|
+
const data = args[0] as Buffer
|
|
865
|
+
const isBinary = args[1] as boolean
|
|
866
|
+
const validation = validateMessage(data, options.maxMessageSize)
|
|
867
|
+
if (!validation.valid) {
|
|
868
|
+
console.warn(`Invalid message: ${validation.error}`)
|
|
869
|
+
ws.send(JSON.stringify({error: validation.error}))
|
|
870
|
+
return
|
|
871
|
+
}
|
|
661
872
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
873
|
+
const parseResult = safeJSONParse(data, options.maxMessageSize)
|
|
874
|
+
if (!parseResult.success) {
|
|
875
|
+
console.warn(`JSON parse error: ${parseResult.error}`)
|
|
876
|
+
ws.send(JSON.stringify({error: "Invalid JSON"}))
|
|
877
|
+
return
|
|
878
|
+
}
|
|
668
879
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
880
|
+
const msg = parseResult.data!
|
|
881
|
+
if (!msg["#"]) return // Incoming messages must have '#'
|
|
882
|
+
if (dup.check(msg["#"])) return
|
|
672
883
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
884
|
+
const delay = rateLimiter.getDelay(clientId)
|
|
885
|
+
if (delay > 0) {
|
|
886
|
+
const throttleCount = rateLimiter.getThrottleCount(clientId)
|
|
887
|
+
console.log(
|
|
888
|
+
`Client ${clientId}: rate limit exceeded, delay would be ${delay}ms, dropping message (throttle count: ${throttleCount})`,
|
|
889
|
+
)
|
|
890
|
+
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
676
891
|
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
|
-
})
|
|
892
|
+
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`,
|
|
694
893
|
)
|
|
894
|
+
ws.close(1008, "Rate limit violations")
|
|
695
895
|
return
|
|
696
896
|
}
|
|
697
897
|
|
|
698
|
-
|
|
699
|
-
|
|
898
|
+
ws.send(
|
|
899
|
+
JSON.stringify({
|
|
900
|
+
"#": dup.track(utils.text.random(9)),
|
|
901
|
+
"@": msg["#"],
|
|
902
|
+
err: `Rate limit exceeded. Slow down requests. Wait ${Math.ceil(delay / 1000)}s`,
|
|
903
|
+
throttle: delay,
|
|
904
|
+
}),
|
|
905
|
+
)
|
|
906
|
+
return
|
|
907
|
+
}
|
|
700
908
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
send(data.toString(), isBinary)
|
|
909
|
+
const processMessage = async (): Promise<void> => {
|
|
910
|
+
dup.track(msg["#"]!)
|
|
704
911
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
delete (msg as { "#"?: string })["#"]
|
|
709
|
-
delete (msg as { "@"?: string })["@"]
|
|
710
|
-
cb(msg)
|
|
711
|
-
delete queue[id!]
|
|
712
|
-
}
|
|
713
|
-
}
|
|
912
|
+
if (msg.get) get(msg as never, send)
|
|
913
|
+
if (msg.put) await put(msg as never, send)
|
|
914
|
+
send(data.toString(), isBinary)
|
|
714
915
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
916
|
+
const id = msg["@"]
|
|
917
|
+
const cb = queue[id!]
|
|
918
|
+
if (cb) {
|
|
919
|
+
delete (msg as {"#"?: string})["#"]
|
|
920
|
+
delete (msg as {"@"?: string})["@"]
|
|
921
|
+
cb(msg)
|
|
922
|
+
delete queue[id!]
|
|
719
923
|
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (delay > 0) {
|
|
927
|
+
setTimeout(processMessage, delay)
|
|
928
|
+
} else {
|
|
929
|
+
processMessage()
|
|
930
|
+
}
|
|
931
|
+
}) as (...args: unknown[]) => void)
|
|
932
|
+
})
|
|
723
933
|
return api(send)
|
|
724
934
|
}
|
|
725
935
|
|
|
@@ -732,18 +942,22 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
732
942
|
let queueProcessor: NodeJS.Timeout | null = null
|
|
733
943
|
const maxQueueLength = options.maxQueueLength || 10000
|
|
734
944
|
|
|
735
|
-
const startResponseTimeout = (
|
|
945
|
+
const startResponseTimeout = (
|
|
946
|
+
trackId: string,
|
|
947
|
+
lex: Lex,
|
|
948
|
+
delay = isTestEnv ? 100 : 10000,
|
|
949
|
+
): ReturnType<typeof setTimeout> => {
|
|
736
950
|
return setTimeout(() => {
|
|
737
951
|
offlineTimeouts.delete(trackId)
|
|
738
952
|
pendingTimeouts.delete(trackId)
|
|
739
953
|
const cb = queue[trackId]
|
|
740
954
|
if (cb) {
|
|
741
955
|
const id = lex["#"]
|
|
742
|
-
const ack: Graph = {
|
|
956
|
+
const ack: Graph = {[id]: null as never}
|
|
743
957
|
if (typeof lex["."] === "string") {
|
|
744
|
-
ack[id] = {
|
|
958
|
+
ack[id] = {[lex["."]]: null} as never
|
|
745
959
|
}
|
|
746
|
-
cb({
|
|
960
|
+
cb({put: ack})
|
|
747
961
|
delete queue[trackId]
|
|
748
962
|
}
|
|
749
963
|
}, delay)
|
|
@@ -757,7 +971,10 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
757
971
|
|
|
758
972
|
const now = Date.now()
|
|
759
973
|
if (clientThrottled && now < throttleUntil) {
|
|
760
|
-
queueProcessor = setTimeout(
|
|
974
|
+
queueProcessor = setTimeout(
|
|
975
|
+
processQueue,
|
|
976
|
+
Math.min(1000, throttleUntil - now),
|
|
977
|
+
)
|
|
761
978
|
return
|
|
762
979
|
}
|
|
763
980
|
|
|
@@ -775,7 +992,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
775
992
|
|
|
776
993
|
// Drop GET messages with no pending callback — these were already served
|
|
777
994
|
// from local store so no caller is waiting for the wire response.
|
|
778
|
-
const isStaleCheckMessage =
|
|
995
|
+
const isStaleCheckMessage =
|
|
996
|
+
!sent && !!msg.get && !pendingTimeouts.has(trackId!)
|
|
779
997
|
|
|
780
998
|
if (sent || isStaleCheckMessage) {
|
|
781
999
|
messageQueue.shift()
|
|
@@ -833,7 +1051,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
833
1051
|
return sentToAtLeastOne
|
|
834
1052
|
}
|
|
835
1053
|
|
|
836
|
-
const send = (data: string): {
|
|
1054
|
+
const send = (data: string): {err?: string} | void => {
|
|
837
1055
|
if (messageQueue.length >= maxQueueLength) {
|
|
838
1056
|
return {
|
|
839
1057
|
err: `Message queue exceeded maximum length (${maxQueueLength}). Update query logic to request less data.`,
|
|
@@ -866,7 +1084,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
866
1084
|
const delay = isTestEnv ? 50 : retryHandler.getDelay()
|
|
867
1085
|
retryHandler.increment()
|
|
868
1086
|
const t = setTimeout(start, delay)
|
|
869
|
-
if ((t as unknown as {
|
|
1087
|
+
if ((t as unknown as {unref?: () => void}).unref)
|
|
1088
|
+
(t as unknown as {unref: () => void}).unref()
|
|
870
1089
|
}
|
|
871
1090
|
}
|
|
872
1091
|
|
|
@@ -910,7 +1129,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
910
1129
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
911
1130
|
const known = await hasSoul(soul)
|
|
912
1131
|
hasSoulMap.set(soul, known)
|
|
913
|
-
if (
|
|
1132
|
+
if (
|
|
1133
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1134
|
+
!node ||
|
|
1135
|
+
typeof node !== "object"
|
|
1136
|
+
)
|
|
1137
|
+
continue
|
|
914
1138
|
for (const [, value] of Object.entries(node)) {
|
|
915
1139
|
const soulId = utils.rel.is(value as GraphValue)
|
|
916
1140
|
if (soulId) pendingReferences.add(soulId)
|
|
@@ -939,7 +1163,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
939
1163
|
// Store filtered data if any
|
|
940
1164
|
if (Object.keys(filteredPut).length > 0) {
|
|
941
1165
|
// Create filtered message for Ham.mix with original message ID
|
|
942
|
-
const filteredMsg = {
|
|
1166
|
+
const filteredMsg = {put: filteredPut, "#": msg["#"]!}
|
|
943
1167
|
await put(filteredMsg, send as never)
|
|
944
1168
|
}
|
|
945
1169
|
|
|
@@ -952,8 +1176,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
952
1176
|
const id = msg["@"]
|
|
953
1177
|
const cb = queue[id!]
|
|
954
1178
|
if (cb) {
|
|
955
|
-
delete (msg as {
|
|
956
|
-
delete (msg as {
|
|
1179
|
+
delete (msg as {"#"?: string})["#"]
|
|
1180
|
+
delete (msg as {"@"?: string})["@"]
|
|
957
1181
|
cb(msg)
|
|
958
1182
|
delete queue[id!]
|
|
959
1183
|
}
|
|
@@ -966,4 +1190,3 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
966
1190
|
}
|
|
967
1191
|
|
|
968
1192
|
export default Wire
|
|
969
|
-
|