@mblaney/holster 2.0.1 → 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 -28
- package/src/store.js +30 -17
- package/src/user.js +92 -101
- package/src/wire.js +325 -125
- 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 -51
- 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 +76 -58
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +477 -248
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,62 +417,109 @@ 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
|
-
store.get(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
)
|
|
317
|
-
},
|
|
318
|
-
{ secure: true }
|
|
319
|
-
)
|
|
423
|
+
store.get(msg.get, (err, ack) => {
|
|
424
|
+
send(
|
|
425
|
+
JSON.stringify({
|
|
426
|
+
"#": dup.track(utils.text.random(9)),
|
|
427
|
+
"@": msg["#"],
|
|
428
|
+
put: ack,
|
|
429
|
+
err: err,
|
|
430
|
+
}),
|
|
431
|
+
)
|
|
432
|
+
})
|
|
320
433
|
}
|
|
321
434
|
}
|
|
322
435
|
|
|
323
436
|
const put = async (
|
|
324
|
-
msg: {
|
|
325
|
-
send: (msg: string) => void
|
|
437
|
+
msg: {put: Graph; "#": string},
|
|
438
|
+
send: (msg: string) => void,
|
|
326
439
|
): Promise<void> => {
|
|
327
|
-
|
|
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
|
+
)
|
|
328
458
|
if (Object.keys(update.now).length === 0) {
|
|
329
459
|
if (Object.keys(update.defer).length !== 0) {
|
|
330
|
-
setTimeout(
|
|
460
|
+
setTimeout(
|
|
461
|
+
() => put({put: update.defer, "#": msg["#"]}, send),
|
|
462
|
+
update.wait,
|
|
463
|
+
)
|
|
331
464
|
}
|
|
332
465
|
return
|
|
333
466
|
}
|
|
334
467
|
|
|
335
468
|
if (!(await check(update.now, send as never))) return
|
|
336
469
|
|
|
337
|
-
|
|
470
|
+
const finish = (err?: string | null): void => {
|
|
338
471
|
if (err) console.warn("store.put", err)
|
|
339
472
|
send(
|
|
340
473
|
JSON.stringify({
|
|
341
474
|
"#": dup.track(utils.text.random(9)),
|
|
342
475
|
"@": msg["#"],
|
|
343
476
|
err: err,
|
|
344
|
-
})
|
|
477
|
+
}),
|
|
345
478
|
)
|
|
346
479
|
update.listeners.forEach(cb => cb())
|
|
347
|
-
}
|
|
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
|
+
}
|
|
348
509
|
|
|
349
510
|
if (Object.keys(update.defer).length !== 0) {
|
|
350
|
-
setTimeout(
|
|
511
|
+
setTimeout(
|
|
512
|
+
() => put({put: update.defer, "#": msg["#"]}, send),
|
|
513
|
+
update.wait,
|
|
514
|
+
)
|
|
351
515
|
}
|
|
352
516
|
}
|
|
353
517
|
|
|
354
518
|
const getWithCallback = (
|
|
355
519
|
lex: Lex,
|
|
356
520
|
cb: (msg: WireMessage) => void,
|
|
357
|
-
send: (msg: string) => {
|
|
358
|
-
_opt?: WireOptions
|
|
521
|
+
send: (msg: string) => {err?: string} | void,
|
|
522
|
+
_opt?: WireOptions,
|
|
359
523
|
): void => {
|
|
360
524
|
if (!cb) return
|
|
361
525
|
|
|
@@ -381,7 +545,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
381
545
|
recentChecks.set(checkKey, now)
|
|
382
546
|
const sendResult = send(request)
|
|
383
547
|
if (sendResult && sendResult.err) {
|
|
384
|
-
cb({
|
|
548
|
+
cb({err: sendResult.err})
|
|
385
549
|
return
|
|
386
550
|
}
|
|
387
551
|
}
|
|
@@ -389,119 +553,115 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
389
553
|
// Don't skip checks for other types of lex queries.
|
|
390
554
|
const sendResult = send(request)
|
|
391
555
|
if (sendResult && sendResult.err) {
|
|
392
|
-
cb({
|
|
556
|
+
cb({err: sendResult.err})
|
|
393
557
|
return
|
|
394
558
|
}
|
|
395
559
|
}
|
|
396
560
|
}
|
|
397
|
-
cb({
|
|
561
|
+
cb({put: ack})
|
|
398
562
|
return
|
|
399
563
|
}
|
|
400
564
|
|
|
401
|
-
store.get(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
gMeta["
|
|
424
|
-
|
|
425
|
-
if (!gMeta["s"]) gMeta["s"] = {}
|
|
426
|
-
gMeta["s"]![state] = meta["s"][state]
|
|
427
|
-
}
|
|
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]
|
|
428
589
|
}
|
|
429
590
|
}
|
|
430
591
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (sendResult && sendResult.err) {
|
|
451
|
-
cb({ err: sendResult.err })
|
|
452
|
-
return
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
} 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)
|
|
456
611
|
const sendResult = send(request)
|
|
457
612
|
if (sendResult && sendResult.err) {
|
|
458
|
-
cb({
|
|
613
|
+
cb({err: sendResult.err})
|
|
459
614
|
return
|
|
460
615
|
}
|
|
461
616
|
}
|
|
617
|
+
} else {
|
|
618
|
+
const sendResult = send(request)
|
|
619
|
+
if (sendResult && sendResult.err) {
|
|
620
|
+
cb({err: sendResult.err})
|
|
621
|
+
return
|
|
622
|
+
}
|
|
462
623
|
}
|
|
463
|
-
cb(err ? { put: ack, err: err } : { put: ack })
|
|
464
|
-
return
|
|
465
624
|
}
|
|
625
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
626
|
+
return
|
|
466
627
|
}
|
|
628
|
+
}
|
|
467
629
|
|
|
468
|
-
|
|
630
|
+
if (err) console.log(err)
|
|
469
631
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
cb({put: ack as never})
|
|
478
|
-
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}
|
|
479
638
|
}
|
|
639
|
+
cb({put: ack as never})
|
|
640
|
+
return
|
|
641
|
+
}
|
|
480
642
|
|
|
481
|
-
|
|
643
|
+
queue[track] = cb
|
|
482
644
|
|
|
483
|
-
|
|
645
|
+
pendingTimeouts.set(track, lex)
|
|
484
646
|
|
|
485
|
-
|
|
486
|
-
|
|
647
|
+
// Ensure the requested soul is stored when the response arrives.
|
|
648
|
+
pendingReferences.add(lex["#"])
|
|
487
649
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
opts
|
|
497
|
-
)
|
|
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
|
+
})
|
|
498
658
|
}
|
|
499
659
|
|
|
500
|
-
const api = (send: (msg: string) => {
|
|
660
|
+
const api = (send: (msg: string) => {err?: string} | void): WireAPI => {
|
|
501
661
|
return {
|
|
502
662
|
get: (lex, cb, _opt) => {
|
|
503
663
|
if (lex && typeof lex["."] === "number") {
|
|
504
|
-
lex = {
|
|
664
|
+
lex = {...lex, ".": String(lex["."])}
|
|
505
665
|
}
|
|
506
666
|
if (lex && lex["#"]) {
|
|
507
667
|
pendingReferences.add(lex["#"])
|
|
@@ -509,7 +669,22 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
509
669
|
getWithCallback(lex, cb, send, _opt)
|
|
510
670
|
},
|
|
511
671
|
put: async (data, cb) => {
|
|
512
|
-
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
|
+
)
|
|
513
688
|
if (Object.keys(update.now).length === 0) {
|
|
514
689
|
if (cb) cb(null)
|
|
515
690
|
return
|
|
@@ -519,6 +694,23 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
519
694
|
return
|
|
520
695
|
}
|
|
521
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
|
+
|
|
522
714
|
for (const [_soul, node] of Object.entries(update.now)) {
|
|
523
715
|
if (node && typeof node === "object") {
|
|
524
716
|
for (const [_key, value] of Object.entries(node)) {
|
|
@@ -539,7 +731,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
539
731
|
JSON.stringify({
|
|
540
732
|
"#": dup.track(utils.text.random(9)),
|
|
541
733
|
put: data,
|
|
542
|
-
})
|
|
734
|
+
}),
|
|
543
735
|
)
|
|
544
736
|
if (sendResult && sendResult.err) {
|
|
545
737
|
if (cb) cb(sendResult.err)
|
|
@@ -548,15 +740,15 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
548
740
|
},
|
|
549
741
|
on: (lex, cb, _get, _opt) => {
|
|
550
742
|
if (lex && typeof lex["."] === "number") {
|
|
551
|
-
lex = {
|
|
743
|
+
lex = {...lex, ".": String(lex["."])}
|
|
552
744
|
}
|
|
553
745
|
const soul = lex && lex["#"]
|
|
554
746
|
if (!soul || !cb) return
|
|
555
747
|
|
|
556
748
|
if (listen[soul]) {
|
|
557
|
-
listen[soul]!.push({
|
|
749
|
+
listen[soul]!.push({".": lex["."], cb: cb as never})
|
|
558
750
|
} else {
|
|
559
|
-
listen[soul] = [{
|
|
751
|
+
listen[soul] = [{".": lex["."], cb: cb as never}]
|
|
560
752
|
}
|
|
561
753
|
if (_get) getWithCallback(lex, cb as never, send, _opt)
|
|
562
754
|
},
|
|
@@ -579,13 +771,14 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
579
771
|
if (isNode && (options.wss || options.server || options.port != null)) {
|
|
580
772
|
let wss = options.wss
|
|
581
773
|
let clients = (): WebSocket[] =>
|
|
582
|
-
(wss as {
|
|
774
|
+
(wss as {clients?: () => WebSocket[]}).clients?.() || []
|
|
583
775
|
if (!wss) {
|
|
584
776
|
const config = options.server
|
|
585
|
-
? {
|
|
586
|
-
: {
|
|
777
|
+
? {server: options.server}
|
|
778
|
+
: {port: options.port}
|
|
587
779
|
wss = new wsModule!.WebSocketServer(config)
|
|
588
|
-
clients = () =>
|
|
780
|
+
clients = () =>
|
|
781
|
+
Array.from((wss as {clients: Set<WebSocket>}).clients || [])
|
|
589
782
|
}
|
|
590
783
|
|
|
591
784
|
const send = (data: string, isBinary?: boolean): void => {
|
|
@@ -595,24 +788,27 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
595
788
|
const lex = pendingTimeouts.get(trackId)!
|
|
596
789
|
pendingTimeouts.delete(trackId)
|
|
597
790
|
|
|
598
|
-
setTimeout(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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]
|
|
605
802
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}, isTestEnv ? 100 : 10000)
|
|
803
|
+
},
|
|
804
|
+
isTestEnv ? 100 : 10000,
|
|
805
|
+
)
|
|
610
806
|
}
|
|
611
807
|
|
|
612
808
|
clients().forEach(client => {
|
|
613
809
|
const ws = client as UnifiedWebSocket
|
|
614
810
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
615
|
-
ws.send(data, {
|
|
811
|
+
ws.send(data, {binary: isBinary})
|
|
616
812
|
} else if (ws) {
|
|
617
813
|
const retryHandler = ws._retryHandler || createRetryHandler()
|
|
618
814
|
ws._retryHandler = retryHandler
|
|
@@ -624,7 +820,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
624
820
|
setTimeout(() => {
|
|
625
821
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
626
822
|
retryHandler.reset()
|
|
627
|
-
ws.send(data, {
|
|
823
|
+
ws.send(data, {binary: isBinary})
|
|
628
824
|
}
|
|
629
825
|
}, delay)
|
|
630
826
|
}
|
|
@@ -632,100 +828,106 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
632
828
|
})
|
|
633
829
|
}
|
|
634
830
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
(
|
|
638
|
-
|
|
639
|
-
console.log("Connection limit reached, rejecting connection")
|
|
640
|
-
ws.close(1013, "Connection limit reached - try again later")
|
|
641
|
-
return
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const clientId = utils.text.random(9)
|
|
645
|
-
console.log(`New WebSocket client connected: ${clientId}`)
|
|
646
|
-
|
|
647
|
-
ws.on?.("error", ((...args: unknown[]) => {
|
|
648
|
-
const error = args[0] as Error
|
|
649
|
-
console.log("WebSocket error:", error)
|
|
650
|
-
connectionManager.remove(ws)
|
|
651
|
-
}) as (...args: unknown[]) => void)
|
|
831
|
+
if (options.userLimit) {
|
|
832
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
833
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
834
|
+
}
|
|
652
835
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
+
}
|
|
657
846
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
}
|
|
667
870
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
}
|
|
674
877
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
878
|
+
const msg = parseResult.data!
|
|
879
|
+
if (!msg["#"]) return // Incoming messages must have '#'
|
|
880
|
+
if (dup.check(msg["#"])) return
|
|
678
881
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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)) {
|
|
682
889
|
console.log(
|
|
683
|
-
`Client ${clientId}:
|
|
684
|
-
)
|
|
685
|
-
if (rateLimiter.shouldDisconnect(clientId)) {
|
|
686
|
-
console.log(
|
|
687
|
-
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`
|
|
688
|
-
)
|
|
689
|
-
ws.close(1008, "Rate limit violations")
|
|
690
|
-
return
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
ws.send(
|
|
694
|
-
JSON.stringify({
|
|
695
|
-
"#": dup.track(utils.text.random(9)),
|
|
696
|
-
"@": msg["#"],
|
|
697
|
-
err: `Rate limit exceeded. Slow down requests. Wait ${Math.ceil(delay / 1000)}s`,
|
|
698
|
-
throttle: delay,
|
|
699
|
-
})
|
|
890
|
+
`Client ${clientId}: Disconnecting after ${throttleCount} throttle violations`,
|
|
700
891
|
)
|
|
892
|
+
ws.close(1008, "Rate limit violations")
|
|
701
893
|
return
|
|
702
894
|
}
|
|
703
895
|
|
|
704
|
-
|
|
705
|
-
|
|
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
|
+
}
|
|
706
906
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
send(data.toString(), isBinary)
|
|
907
|
+
const processMessage = async (): Promise<void> => {
|
|
908
|
+
dup.track(msg["#"]!)
|
|
710
909
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
delete (msg as { "#"?: string })["#"]
|
|
715
|
-
delete (msg as { "@"?: string })["@"]
|
|
716
|
-
cb(msg)
|
|
717
|
-
delete queue[id!]
|
|
718
|
-
}
|
|
719
|
-
}
|
|
910
|
+
if (msg.get) get(msg as never, send)
|
|
911
|
+
if (msg.put) await put(msg as never, send)
|
|
912
|
+
send(data.toString(), isBinary)
|
|
720
913
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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!]
|
|
725
921
|
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (delay > 0) {
|
|
925
|
+
setTimeout(processMessage, delay)
|
|
926
|
+
} else {
|
|
927
|
+
processMessage()
|
|
928
|
+
}
|
|
929
|
+
}) as (...args: unknown[]) => void)
|
|
930
|
+
})
|
|
729
931
|
return api(send)
|
|
730
932
|
}
|
|
731
933
|
|
|
@@ -734,21 +936,26 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
734
936
|
let clientThrottled = false
|
|
735
937
|
let throttleUntil = 0
|
|
736
938
|
let messageQueue: string[] = []
|
|
939
|
+
let offlinePuts: string[] = []
|
|
737
940
|
let queueProcessor: NodeJS.Timeout | null = null
|
|
738
941
|
const maxQueueLength = options.maxQueueLength || 10000
|
|
739
942
|
|
|
740
|
-
const startResponseTimeout = (
|
|
943
|
+
const startResponseTimeout = (
|
|
944
|
+
trackId: string,
|
|
945
|
+
lex: Lex,
|
|
946
|
+
delay = isTestEnv ? 100 : 10000,
|
|
947
|
+
): ReturnType<typeof setTimeout> => {
|
|
741
948
|
return setTimeout(() => {
|
|
742
949
|
offlineTimeouts.delete(trackId)
|
|
743
950
|
pendingTimeouts.delete(trackId)
|
|
744
951
|
const cb = queue[trackId]
|
|
745
952
|
if (cb) {
|
|
746
953
|
const id = lex["#"]
|
|
747
|
-
const ack: Graph = {
|
|
954
|
+
const ack: Graph = {[id]: null as never}
|
|
748
955
|
if (typeof lex["."] === "string") {
|
|
749
|
-
ack[id] = {
|
|
956
|
+
ack[id] = {[lex["."]]: null} as never
|
|
750
957
|
}
|
|
751
|
-
cb({
|
|
958
|
+
cb({put: ack})
|
|
752
959
|
delete queue[trackId]
|
|
753
960
|
}
|
|
754
961
|
}, delay)
|
|
@@ -762,7 +969,10 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
762
969
|
|
|
763
970
|
const now = Date.now()
|
|
764
971
|
if (clientThrottled && now < throttleUntil) {
|
|
765
|
-
queueProcessor = setTimeout(
|
|
972
|
+
queueProcessor = setTimeout(
|
|
973
|
+
processQueue,
|
|
974
|
+
Math.min(1000, throttleUntil - now),
|
|
975
|
+
)
|
|
766
976
|
return
|
|
767
977
|
}
|
|
768
978
|
|
|
@@ -780,10 +990,16 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
780
990
|
|
|
781
991
|
// Drop GET messages with no pending callback — these were already served
|
|
782
992
|
// from local store so no caller is waiting for the wire response.
|
|
783
|
-
const isStaleCheckMessage =
|
|
993
|
+
const isStaleCheckMessage =
|
|
994
|
+
!sent && !!msg.get && !pendingTimeouts.has(trackId!)
|
|
784
995
|
|
|
785
996
|
if (sent || isStaleCheckMessage) {
|
|
786
997
|
messageQueue.shift()
|
|
998
|
+
} else if (!sent && msg.put) {
|
|
999
|
+
// Hold offline PUTs separately so they don't block GET messages behind
|
|
1000
|
+
// them. They'll be flushed back to the front of the queue on reconnect.
|
|
1001
|
+
messageQueue.shift()
|
|
1002
|
+
offlinePuts.push(data)
|
|
787
1003
|
}
|
|
788
1004
|
|
|
789
1005
|
if (trackId && pendingTimeouts.has(trackId)) {
|
|
@@ -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.`,
|
|
@@ -863,14 +1079,23 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
863
1079
|
ws = null
|
|
864
1080
|
|
|
865
1081
|
if (retryHandler.shouldRetry()) {
|
|
866
|
-
const delay = retryHandler.getDelay()
|
|
1082
|
+
const delay = isTestEnv ? 50 : retryHandler.getDelay()
|
|
867
1083
|
retryHandler.increment()
|
|
868
|
-
setTimeout(start, delay)
|
|
1084
|
+
const t = setTimeout(start, delay)
|
|
1085
|
+
if ((t as unknown as {unref?: () => void}).unref)
|
|
1086
|
+
(t as unknown as {unref: () => void}).unref()
|
|
869
1087
|
}
|
|
870
1088
|
}
|
|
871
1089
|
|
|
872
1090
|
ws.onopen = () => {
|
|
873
1091
|
retryHandler.reset()
|
|
1092
|
+
if (offlinePuts.length > 0) {
|
|
1093
|
+
messageQueue = offlinePuts.concat(messageQueue)
|
|
1094
|
+
offlinePuts = []
|
|
1095
|
+
if (!queueProcessor) {
|
|
1096
|
+
queueProcessor = setTimeout(processQueue, 50)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
874
1099
|
}
|
|
875
1100
|
|
|
876
1101
|
ws.onerror = (e: Event) => {
|
|
@@ -902,7 +1127,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
902
1127
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
903
1128
|
const known = await hasSoul(soul)
|
|
904
1129
|
hasSoulMap.set(soul, known)
|
|
905
|
-
if (
|
|
1130
|
+
if (
|
|
1131
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1132
|
+
!node ||
|
|
1133
|
+
typeof node !== "object"
|
|
1134
|
+
)
|
|
1135
|
+
continue
|
|
906
1136
|
for (const [, value] of Object.entries(node)) {
|
|
907
1137
|
const soulId = utils.rel.is(value as GraphValue)
|
|
908
1138
|
if (soulId) pendingReferences.add(soulId)
|
|
@@ -931,7 +1161,7 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
931
1161
|
// Store filtered data if any
|
|
932
1162
|
if (Object.keys(filteredPut).length > 0) {
|
|
933
1163
|
// Create filtered message for Ham.mix with original message ID
|
|
934
|
-
const filteredMsg = {
|
|
1164
|
+
const filteredMsg = {put: filteredPut, "#": msg["#"]!}
|
|
935
1165
|
await put(filteredMsg, send as never)
|
|
936
1166
|
}
|
|
937
1167
|
|
|
@@ -944,8 +1174,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
944
1174
|
const id = msg["@"]
|
|
945
1175
|
const cb = queue[id!]
|
|
946
1176
|
if (cb) {
|
|
947
|
-
delete (msg as {
|
|
948
|
-
delete (msg as {
|
|
1177
|
+
delete (msg as {"#"?: string})["#"]
|
|
1178
|
+
delete (msg as {"@"?: string})["@"]
|
|
949
1179
|
cb(msg)
|
|
950
1180
|
delete queue[id!]
|
|
951
1181
|
}
|
|
@@ -958,4 +1188,3 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
958
1188
|
}
|
|
959
1189
|
|
|
960
1190
|
export default Wire
|
|
961
|
-
|