@mblaney/holster 1.0.36 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "@mblaney/holster",
3
- "version": "1.0.36",
3
+ "version": "2.0.1",
4
4
  "description": "Holster is a service that allows synchronising data between devices using Node, Deno, Bun or the browser.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "scripts": {
8
8
  "test": "node --test",
9
- "test:ts": "node --test --import tsx/esm ts/test/**/*.test.ts",
10
- "test:ts:system": "node --test --import tsx/esm ts/test/system/**/*.test.ts",
9
+ "test:ts": "node --test --import tsx/esm ts/test/*.test.ts ts/test/system/*.test.ts",
11
10
  "build": "esbuild src/holster.js --bundle --format=esm --sourcemap \"--external:node:*\" \"--external:ws\" --minify --outdir=build",
12
11
  "build:ts": "esbuild ts/src/holster.ts --bundle --format=esm --sourcemap \"--external:node:*\" \"--external:ws\" --minify --outdir=dist",
13
12
  "build:all": "npm run build && npm run build:ts",
14
13
  "prepublishOnly": "npm run build:all",
15
- "type-check": "tsc --noEmit",
16
- "type-check:tests": "tsc --noEmit --project tsconfig.test.json"
14
+ "type-check": "tsc -p tsconfig.json",
15
+ "type-check:tests": "tsc -p tsconfig.test.json"
17
16
  },
18
17
  "files": [
19
18
  "src",
package/src/dup.js CHANGED
@@ -1,6 +1,5 @@
1
- const Dup = maxAge => {
2
- // Allow maxAge to be passed in as tests wait on the setTimeout.
3
- if (!maxAge) maxAge = 9000
1
+ const Dup = () => {
2
+ const maxAge = 9000
4
3
  const dup = {store: {}}
5
4
  dup.check = id => (dup.store[id] ? dup.track(id) : false)
6
5
  dup.track = id => {
@@ -8,14 +7,14 @@ const Dup = maxAge => {
8
7
  dup.store[id] = Date.now()
9
8
  if (!dup.expiry) {
10
9
  dup.expiry = setTimeout(() => {
11
- if (dup.expiry) return
12
-
13
10
  const now = Date.now()
14
11
  Object.keys(dup.store).forEach(id => {
15
12
  if (now - dup.store[id] > maxAge) delete dup.store[id]
16
13
  })
17
14
  dup.expiry = null
18
15
  }, maxAge)
16
+ // Don't block process exit — cleanup is best-effort.
17
+ if (dup.expiry.unref) dup.expiry.unref()
19
18
  }
20
19
  return id
21
20
  }
package/src/get.js CHANGED
@@ -43,17 +43,20 @@ const Get = (lex, graph, fast) => {
43
43
  // return undefined to ensure we get all matching properties from store.
44
44
  if (!fast) return
45
45
 
46
+ let found = false
46
47
  for (const key of Object.keys(graph[soul])) {
47
48
  // Always include userPublicKey for verification, regardless of filter
48
- if (key === userPublicKey || match(lex["."], key)) {
49
+ if (key !== "_" && (key === userPublicKey || match(lex["."], key))) {
49
50
  node[key] = graph[soul][key]
50
51
  node._[">"][key] = graph[soul]._[">"][key]
51
52
  const state = graph[soul]._[">"][key]
52
53
  if (graph[soul]._["s"] && graph[soul]._["s"][state]) {
53
54
  signatures[state] = graph[soul]._["s"][state]
54
55
  }
56
+ found = true
55
57
  }
56
58
  }
59
+ if (!found) return
57
60
  }
58
61
  if (Object.keys(signatures).length > 0) {
59
62
  node._["s"] = signatures
package/src/ham.js CHANGED
@@ -35,7 +35,7 @@ const Ham = (state, currentState, value, currentValue, signed = false) => {
35
35
  return {incoming: true}
36
36
  }
37
37
 
38
- Ham.mix = async (change, graph, secure, listen) => {
38
+ Ham.mix = async (change, graph, hasNode, secure, listen) => {
39
39
  if (!change || typeof change !== "object") {
40
40
  throw new TypeError("change must be an object")
41
41
  }
@@ -226,7 +226,10 @@ Ham.mix = async (change, graph, secure, listen) => {
226
226
  })
227
227
  .sort((a, b) => a.maxState - b.maxState)
228
228
  const remove = soulsByAge.slice(0, souls.length - MAX_GRAPH_SIZE)
229
- remove.forEach(({soul}) => delete graph[soul])
229
+ remove.forEach(({soul}) => {
230
+ delete graph[soul]
231
+ if (hasNode) hasNode.delete(soul)
232
+ })
230
233
  }
231
234
 
232
235
  return {now: now, defer: defer, wait: wait, listeners: listeners}
package/src/holster.js CHANGED
@@ -64,30 +64,28 @@ const Holster = opt => {
64
64
  delete msg.put[soul][utils.userPublicKey]
65
65
  delete msg.put[soul][utils.userSignature]
66
66
  // Resolve any rels on the node before returning to the user.
67
- for (const key of Object.keys(msg.put[soul])) {
67
+ await Promise.all(Object.keys(msg.put[soul]).map(async key => {
68
68
  const id = utils.rel.is(msg.put[soul][key])
69
- if (id) {
70
- // Retry with delays if we get null
71
- const attemptRead = async (retries = 0) => {
72
- const data = await new Promise(res => {
73
- const _ctxid = utils.text.random()
74
- const ctx = allctx.get(ctxid)
75
- allctx.set(_ctxid, {
76
- chain: [{item: null, soul: id}],
77
- user: ctx ? ctx.user : null,
78
- })
79
- api(_ctxid).next(null, res, _opt)
69
+ if (!id) return
70
+ const attemptRead = async (retries = 0) => {
71
+ const data = await new Promise(res => {
72
+ const _ctxid = utils.text.random()
73
+ const ctx = allctx.get(ctxid)
74
+ allctx.set(_ctxid, {
75
+ chain: [{item: null, soul: id}],
76
+ user: ctx ? ctx.user : null,
80
77
  })
81
- if (data !== null || retries >= 5) {
82
- return data
83
- }
84
- // Data not ready, retry after delay
85
- await new Promise(resolve => setTimeout(resolve, 50))
86
- return attemptRead(retries + 1)
78
+ api(_ctxid).next(null, res, _opt)
79
+ })
80
+ if (data !== null || retries >= 5) {
81
+ return data
87
82
  }
88
- msg.put[soul][key] = await attemptRead()
83
+ // Data not ready, retry after delay
84
+ await new Promise(resolve => setTimeout(resolve, 50))
85
+ return attemptRead(retries + 1)
89
86
  }
90
- }
87
+ msg.put[soul][key] = await attemptRead()
88
+ }))
91
89
  ack(msg.put[soul])
92
90
  } else {
93
91
  // No data callback.
@@ -132,11 +130,13 @@ const Holster = opt => {
132
130
  }
133
131
  }
134
132
 
135
- const watchForRel = (soul, item, ctx, i, request, cb) => {
133
+ const watchForRel = (soul, item, ctx, i, request, cb, on = true) => {
134
+ let timer = null
136
135
  const handler = () => {
137
136
  // Bail out if the context was removed by off().
138
137
  if (!allctx.has(ctxid)) {
139
138
  wire.off({"#": soul, ".": item}, handler)
139
+ if (timer) clearTimeout(timer)
140
140
  return
141
141
  }
142
142
  wire.get(
@@ -144,16 +144,39 @@ const Holster = opt => {
144
144
  msg => {
145
145
  const node = msg.put && msg.put[soul]
146
146
  const id = utils.rel.is(node && node[item])
147
- if (!id) return
147
+ if (!id) {
148
+ // If it's a plain value (not a rel) and this is a get, deliver it.
149
+ // node._ guards against timeout null-acks which lack metadata.
150
+ if (!on && node && node._ && typeof node[item] !== "undefined") {
151
+ if (timer) clearTimeout(timer)
152
+ wire.off({"#": soul, ".": item}, handler)
153
+ cb(node[item])
154
+ }
155
+ return
156
+ }
157
+ if (timer) clearTimeout(timer)
148
158
  wire.off({"#": soul, ".": item}, handler)
149
159
  ctx.chain[i].soul = id
150
160
  allctx.set(ctxid, {...ctx})
151
- api(ctxid).on(request.on, cb, request._get, request._opt)
161
+ if (on) {
162
+ api(ctxid).on(request.on, cb, request._get, request._opt)
163
+ } else {
164
+ api(ctxid).next(null, request.get, cb, request._opt)
165
+ }
152
166
  },
153
- {...request._opt, secure: ctx.user || opt.secure},
167
+ {...request._opt, secure: !!(ctx.user || opt.secure)},
154
168
  )
155
169
  }
156
170
  wire.on({"#": soul, ".": item}, handler, false, request._opt)
171
+ // Time out after the same total duration as the on() retry loop
172
+ // (1+2+4+8+16 = 31s) so callers are not blocked forever if the node
173
+ // genuinely doesn't exist.
174
+ if (!on) {
175
+ timer = setTimeout(() => {
176
+ wire.off({"#": soul, ".": item}, handler)
177
+ if (cb) cb(null)
178
+ }, 31000)
179
+ }
157
180
  }
158
181
 
159
182
  const resolve = (request, cb) => {
@@ -169,6 +192,7 @@ const Holster = opt => {
169
192
 
170
193
  let found = false
171
194
  const ctx = allctx.get(ctxid)
195
+ if (!ctx) return {item: null, soul: null}
172
196
  for (var i = 1; i < ctx.chain.length; i++) {
173
197
  if (ctx.chain[i].soul !== null) continue
174
198
 
@@ -255,13 +279,22 @@ const Holster = opt => {
255
279
  // Node doesn't exist yet — watch for it to appear.
256
280
  watchForRel(soul, item, ctx, i, request, cb)
257
281
  if (request._get) cb(null)
282
+ } else if (get && node) {
283
+ const sv = node._ && node._[">"]
284
+ if (sv && Object.keys(sv).length > 0 && typeof sv[item] === "undefined") {
285
+ // State vector has other properties but not this one — never written.
286
+ if (cb) cb(null)
287
+ } else {
288
+ // Empty state vector or item is tracked — may arrive via push.
289
+ watchForRel(soul, item, ctx, i, request, cb, false)
290
+ }
258
291
  } else {
259
- // Allow querying a node that doesn't exist.
292
+ // Soul doesn't exist or no get — return null.
260
293
  if (cb) cb(null)
261
294
  }
262
295
  }
263
296
  },
264
- {...request._opt, secure: ctx.user || opt.secure},
297
+ {...request._opt, secure: !!(ctx.user || opt.secure)},
265
298
  )
266
299
  // Callback has been passed to next soul lookup or called above, so
267
300
  // return false as the calling code should not continue.
@@ -387,9 +420,7 @@ const Holster = opt => {
387
420
  // ctx already removed by another chained callback is ok?
388
421
  if (!ctx) return
389
422
 
390
- if (!ctx.cb) {
391
- if (!cb) return
392
-
423
+ if (!ctx.cb && cb) {
393
424
  // This (and ack) allows nested objects to set their own callbacks.
394
425
  ctx.cb = cb
395
426
  cb = null
@@ -406,7 +437,7 @@ const Holster = opt => {
406
437
 
407
438
  // Resolve the current context before putting data. (Note that set is
408
439
  // not passed to resolve because it's already been applied above.)
409
- const {item, soul} = resolve({put: data}, _ack)
440
+ const {item, soul} = resolve({put: data, _opt: {put: true}}, _ack)
410
441
  if (!soul) return
411
442
 
412
443
  if (result === true) {
@@ -475,7 +506,7 @@ const Holster = opt => {
475
506
  wire.put(g, _ack)
476
507
  })
477
508
  },
478
- {secure: ctx.user || opt.secure},
509
+ {secure: !!(ctx.user || opt.secure), put: true},
479
510
  )
480
511
  return
481
512
  }
@@ -515,7 +546,7 @@ const Holster = opt => {
515
546
  } else {
516
547
  const _ctxid = utils.text.random()
517
548
  const chain = [{item: item, soul: soul}]
518
- // Pass on the previous context's callback and user flag here.
549
+ // Pass on the previous ctx's callback and user flag here.
519
550
  allctx.set(_ctxid, {
520
551
  chain: chain,
521
552
  user: ctx.user,
@@ -564,7 +595,7 @@ const Holster = opt => {
564
595
 
565
596
  wire.put(g, _ack)
566
597
  },
567
- {secure: ctx.user || opt.secure},
598
+ {secure: !!(ctx.user || opt.secure), put: true},
568
599
  )
569
600
  }
570
601
  attemptRead()
@@ -611,8 +642,8 @@ const Holster = opt => {
611
642
  map.set(cb, () => {
612
643
  // Bail out if off() has already cleaned up this context — the mapped
613
644
  // callback can fire twice (once from wire.on and once from the _get
614
- // immediate-read path) and the second call after async context cleanup
615
- // would crash in resolve(). Matches the guard in watchForRel.
645
+ // immediate-read path) and the second call after async context
646
+ // cleanup would crash in resolve(). Matches the guard in watchForRel.
616
647
  if (!allctx.has(ctxid)) return
617
648
  // Cancel any pending retry — the persistent listener firing means we
618
649
  // have an update and should not call cb twice.
@@ -635,7 +666,8 @@ const Holster = opt => {
635
666
  chain: [{item: null, soul: id}],
636
667
  user: ctx ? ctx.user : null,
637
668
  })
638
- api(_ctxid).next(null, res, retries === 0 ? utils.obj.put(_opt, "fast", true) : _opt)
669
+ api(_ctxid).next(null, res, retries === 0 ?
670
+ utils.obj.put(_opt, "fast", true) : _opt)
639
671
  })
640
672
  if (data !== null || retries >= 5) {
641
673
  retryCount = 0 // Reset retry counter on success
@@ -661,7 +693,8 @@ const Holster = opt => {
661
693
  // miss and never fire again if the data doesn't change while
662
694
  // the listener is attached, so we must poll for initial load.
663
695
  const retry = () => {
664
- const delay = Math.min(retryDelay * Math.pow(2, retryCount), 30000)
696
+ const delay =
697
+ Math.min(retryDelay * Math.pow(2, retryCount), 30000)
665
698
  retryCount++
666
699
  retryTimer = setTimeout(() => {
667
700
  wire.get(
@@ -680,7 +713,7 @@ const Holster = opt => {
680
713
  cb(null)
681
714
  }
682
715
  },
683
- {..._opt, secure: ctx.user || opt.secure},
716
+ {..._opt, secure: !!(ctx.user || opt.secure)},
684
717
  )
685
718
  }, delay)
686
719
  }
@@ -690,7 +723,7 @@ const Holster = opt => {
690
723
  }
691
724
  }
692
725
  },
693
- {..._opt, secure: ctx.user || opt.secure},
726
+ {..._opt, secure: !!(ctx.user || opt.secure)},
694
727
  )
695
728
  })
696
729
 
@@ -727,17 +760,14 @@ const Holster = opt => {
727
760
  // First remove the initial listener
728
761
  wire.off(initialLex, map.get(cb))
729
762
  // Then add listener on the related node
730
- let relLex
731
- if (lex) relLex = utils.obj.put(initialLex, "#", id)
732
- else relLex = {"#": id, ".": null}
733
- wire.on(relLex, map.get(cb), false, _opt)
763
+ wire.on({"#": id, ".": null}, map.get(cb), false, _opt)
734
764
  if (_get) map.get(cb)()
735
765
  } else if (_get) {
736
766
  // Not a rel, but _get was requested, so trigger callback.
737
767
  map.get(cb)()
738
768
  }
739
769
  },
740
- {..._opt, secure: ctx.user || opt.secure},
770
+ {..._opt, secure: !!(ctx.user || opt.secure)},
741
771
  )
742
772
  },
743
773
  off: cb => {
@@ -751,9 +781,10 @@ const Holster = opt => {
751
781
  const {item, soul} = resolve({off: true}, cb)
752
782
  if (!soul) return
753
783
 
754
- // Remove listener immediately from the original soul to avoid race condition.
755
- // The listener may be on a related soul if item is a rel, but we remove
756
- // from the original soul first since that's where it's initially registered.
784
+ // Remove listener immediately from the original soul to avoid a race
785
+ // condition. The listener may be on a related soul if item is a rel,
786
+ // but we remove from the original soul first since that's where it's
787
+ //initially registered.
757
788
  wire.off({"#": soul}, map.get(cb))
758
789
 
759
790
  // Check if item is a rel and remove from the related node as well.
package/src/index.js CHANGED
@@ -3,7 +3,7 @@ import path from "path"
3
3
  import {fileURLToPath} from "url"
4
4
  import Holster from "./holster.js"
5
5
 
6
- const holster = Holster()
6
+ const holster = Holster({port: 8765})
7
7
 
8
8
  // Note that express is only used here to serve the examples folder. It is not
9
9
  // required to run Holster itself.
package/src/radisk.js CHANGED
@@ -34,7 +34,6 @@ const Radisk = opt => {
34
34
  if (!opt.write) opt.write = 1 // Wait time before write in milliseconds
35
35
  if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB
36
36
  if (!opt.memoryLimit) opt.memoryLimit = 500 // Memory limit in MB
37
- if (!opt.readTimeout) opt.readTimeout = 1000 // Read timeout in milliseconds
38
37
  if (typeof opt.cache === "undefined") opt.cache = true
39
38
 
40
39
  if (!opt.store) {
@@ -59,7 +58,7 @@ const Radisk = opt => {
59
58
  // Performance logging
60
59
  const perfLog = (operation, startTime, key, size) => {
61
60
  const duration = Date.now() - startTime
62
- if (duration > 2000) {
61
+ if (duration > 3000) {
63
62
  console.log(
64
63
  `[RADISK-SLOW] ${operation}: ${duration}ms for ${key}${size ? ` (${size} bytes)` : ""}`,
65
64
  )
@@ -124,37 +123,29 @@ const Radisk = opt => {
124
123
  }
125
124
  }
126
125
 
127
- // Check if there's already a pending read for this key
128
126
  if (pendingReads.has(key)) {
129
- // Add callback to existing pending read
130
- pendingReads.get(key).callbacks.push(cb)
127
+ pendingReads.get(key).push(cb)
131
128
  return
132
129
  }
133
130
 
134
- // Start new read and create callback queue with fired flag
135
- const pending = {callbacks: [cb], fired: false, timeoutId: null}
136
- pendingReads.set(key, pending)
137
-
138
- // Set a timeout to fire callbacks if read takes too long
139
- pending.timeoutId = setTimeout(() => {
140
- if (!pending.fired) {
141
- pending.fired = true
142
- pendingReads.delete(key)
143
- const err = new Error("radisk read timeout")
144
- pending.callbacks.forEach(callback => callback(err, undefined))
145
- }
146
- }, opt.readTimeout)
147
-
131
+ pendingReads.set(key, [cb])
148
132
  const readStart = Date.now()
133
+ // If radisk.read genuinely hangs (fd leak, stuck I/O), recover
134
+ // callbacks as a cache miss so callers can fall through to the wire path.
135
+ // 30s is long enough to distinguish a hang from a slow-but-healthy read.
136
+ const timeout = setTimeout(() => {
137
+ const callbacks = pendingReads.get(key)
138
+ if (!callbacks) return
139
+ opt.log(`radisk read hang detected for key: ${key}`)
140
+ pendingReads.delete(key)
141
+ callbacks.forEach(callback => callback(null, null))
142
+ }, 30000)
149
143
  return radisk.read(key, (err, result) => {
150
- clearTimeout(pending.timeoutId)
144
+ clearTimeout(timeout)
151
145
  perfLog("read", readStart, key)
152
- // Execute all pending callbacks for this key if timeout hasn't fired
153
- if (!pending.fired) {
154
- pending.fired = true
155
- pendingReads.delete(key)
156
- pending.callbacks.forEach(callback => callback(err, result))
157
- }
146
+ const callbacks = pendingReads.get(key) || []
147
+ pendingReads.delete(key)
148
+ callbacks.forEach(callback => callback(err, result))
158
149
  })
159
150
  }
160
151
 
@@ -288,6 +279,8 @@ const Radisk = opt => {
288
279
  limit: "",
289
280
  done: false,
290
281
  count: 0,
282
+ halfKey: "", // soul ID at the ~opt.size/2 mark for a balanced split
283
+ halfOffset: 0, // write.text.length before that entry
291
284
  each: (value, key, k, pre) => {
292
285
  // each is called for all keys, but stop adding to write.text when
293
286
  // write.slice is called, then the current contents of write.text
@@ -298,17 +291,46 @@ const Radisk = opt => {
298
291
  value = typeof value === "undefined" ? "" : "=" + Radisk.encode(value)
299
292
  const enc =
300
293
  Radisk.encode(pre.length) + "#" + Radisk.encode(k) + value + "\n"
301
- // Cannot split the file if only have one entry to write. Also don't
302
- // start a split if in the middle of writing a node (pre > 0).
294
+ // Cannot split the file if only have one entry to write. Allow splitting
295
+ // at depth > 0 only when k itself contains enq, meaning k is a full
296
+ // "soulId\x05property" key, not merely a property suffix of a shared
297
+ // soul prefix. Use the full key (pre + k) to extract the soul boundary.
298
+ const validBoundary =
299
+ pre.length === 0 || (pre.length === 1 && k.includes(enq))
300
+
301
+ // Record the first valid split boundary past the halfway mark so a
302
+ // full overflow can split here instead of at the tail, keeping both
303
+ // halves near opt.size/2 rather than leaving the file always full.
304
+ if (
305
+ !write.halfKey &&
306
+ write.count > 1 &&
307
+ write.text.length > opt.size / 2 &&
308
+ validBoundary
309
+ ) {
310
+ const fullKey = pre.join("") + k
311
+ const end = fullKey.indexOf(enq)
312
+ write.halfKey =
313
+ end === -1 ? fullKey : fullKey.substring(0, end)
314
+ write.halfOffset = write.text.length
315
+ }
316
+
303
317
  if (
304
318
  write.count > 1 &&
305
- pre.length === 0 &&
306
- write.text.length + enc.length > opt.size
319
+ write.text.length + enc.length > opt.size &&
320
+ validBoundary
307
321
  ) {
308
- const end = k.indexOf(enq)
309
- write.limit = end === -1 ? k : k.substring(0, end)
310
- // Cannot split if they key is the same as the current file name.
322
+ const fullKey = pre.join("") + k
323
+ const end = fullKey.indexOf(enq)
324
+ write.limit =
325
+ write.halfKey ||
326
+ (end === -1 ? fullKey : fullKey.substring(0, end))
327
+ // Cannot split if the key is the same as the current file name.
311
328
  if (write.limit !== file) {
329
+ // Truncate to the balanced split point — text after halfOffset is
330
+ // handled by the write.slice call below, which reads from rad.
331
+ if (write.halfKey) {
332
+ write.text = write.text.substring(0, write.halfOffset)
333
+ }
312
334
  write.done = true
313
335
  write.sub = Radix()
314
336
  Radix.map(rad, write.slice)
@@ -330,6 +352,13 @@ const Radisk = opt => {
330
352
  // finished.
331
353
  const writeStart = Date.now()
332
354
  opt.store.put(file, write.text, err => {
355
+ // If a split occurred the cached radix tree still contains entries that
356
+ // were moved to the sub-file. Invalidate so the next save.mix re-reads
357
+ // from disk rather than re-writing those entries back to this file.
358
+ if (write.done) {
359
+ cache.delete(file)
360
+ fileListCache = null
361
+ }
333
362
  perfLog("file-write", writeStart, file, write.text.length)
334
363
  cb(err)
335
364
  })
@@ -340,71 +369,45 @@ const Radisk = opt => {
340
369
  const end = key.indexOf(enq)
341
370
  const soul = end === -1 ? key : key.substring(0, end)
342
371
 
343
- const read = {
344
- lex: file => {
345
- // store.list should call lex without a file last, which means all file
346
- // names were compared to soul, so the current read.file is ok to use.
347
- if (!file) {
348
- if (!read.file) {
349
- cb("no file found", u)
350
- return
351
- }
352
-
353
- // Check multi-file cache first
354
- if (opt.cache && cache.has(read.file)) {
355
- const cachedRadix = cache.get(read.file)
356
- read.value = cachedRadix(key)
357
- // Return cached result (defined or undefined) since in-memory
358
- // radix tree is authoritative
359
- return cb(u, read.value)
360
- }
361
-
362
- radisk.parse(read.file, read.it)
363
- return
364
- }
365
-
366
- // Want the filename closest to soul.
367
- if (file > soul || file < read.file) return
372
+ // Binary search for largest filename <= soul in a sorted array.
373
+ const findFile = files => {
374
+ let lo = 0, hi = files.length - 1, result = null
375
+ while (lo <= hi) {
376
+ const mid = (lo + hi) >> 1
377
+ if (files[mid] <= soul) { result = files[mid]; lo = mid + 1 }
378
+ else { hi = mid - 1 }
379
+ }
380
+ return result
381
+ }
368
382
 
369
- read.file = file
370
- },
371
- it: (err, disk) => {
383
+ const read = file => {
384
+ if (!file) { cb("no file found", u); return }
385
+ if (opt.cache && cache.has(file)) {
386
+ return cb(u, cache.get(file)(key))
387
+ }
388
+ radisk.parse(file, (err, disk) => {
372
389
  if (err) opt.log(err)
373
- if (disk) {
374
- if (opt.cache) {
375
- cache.set(read.file, disk)
376
- checkMemoryUsage() // Check memory usage after adding to cache
377
- }
378
- read.value = disk(key)
390
+ if (disk && opt.cache) {
391
+ cache.set(file, disk)
392
+ checkMemoryUsage()
379
393
  }
380
- cb(err, read.value)
381
- },
394
+ cb(err, disk ? disk(key) : u)
395
+ })
382
396
  }
397
+
383
398
  const now = Date.now()
384
- if (
385
- opt.cache &&
386
- fileListCache &&
387
- now - fileListCacheTime < FILE_LIST_CACHE_TTL
388
- ) {
389
- // Use cached file list
390
- fileListCache.forEach(file => read.lex(file))
391
- read.lex() // Signal end of list
399
+ if (opt.cache && fileListCache && now - fileListCacheTime < FILE_LIST_CACHE_TTL) {
400
+ read(findFile(fileListCache, soul))
392
401
  } else {
393
- // Refresh cache
394
402
  const files = []
395
- const originalLex = read.lex
396
- read.lex = file => {
397
- if (file) files.push(file)
398
- return originalLex(file)
399
- }
400
-
401
403
  opt.store.list(file => {
402
- read.lex(file)
403
- if (!file && opt.cache) {
404
- // End of list - cache the results
404
+ if (file) { files.push(file); return }
405
+ files.sort()
406
+ if (opt.cache) {
405
407
  fileListCache = files
406
408
  fileListCacheTime = now
407
409
  }
410
+ read(findFile(files, soul))
408
411
  })
409
412
  }
410
413
  }
package/src/sea-utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import SafeBuffer from "./buffer.js"
2
2
 
3
- const isNode = typeof document === "undefined"
3
+ const isNode = typeof process !== "undefined" && process.versions?.node != null
4
4
  const crypto = isNode
5
5
  ? (await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto
6
6
  : globalThis.crypto
package/src/store.js CHANGED
@@ -2,7 +2,7 @@ import Radisk from "./radisk.js"
2
2
  import Radix from "./radix.js"
3
3
  import * as utils from "./utils.js"
4
4
 
5
- const isNode = typeof document === "undefined"
5
+ const isNode = typeof process !== "undefined" && process.versions?.node != null
6
6
  const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
7
7
 
8
8
  // ASCII character for enquiry.
package/src/user.js CHANGED
@@ -5,7 +5,6 @@ import {userPublicKey, userSignature} from "./utils.js"
5
5
 
6
6
  const User = (opt, wire) => {
7
7
  if (!wire) wire = Wire(opt)
8
- const wait = opt?.wait ?? 5000
9
8
  let pubs = []
10
9
  let creating = false
11
10
  let authing = false
@@ -76,7 +75,6 @@ const User = (opt, wire) => {
76
75
 
77
76
  done(null)
78
77
  },
79
- {wait: wait},
80
78
  )
81
79
  }
82
80
 
@@ -103,7 +101,6 @@ const User = (opt, wire) => {
103
101
  pubs = Object.keys(data)
104
102
  next()
105
103
  },
106
- {wait: wait},
107
104
  )
108
105
  }
109
106
 
@@ -183,7 +180,6 @@ const User = (opt, wire) => {
183
180
  })
184
181
  })
185
182
  },
186
- {wait: wait},
187
183
  )
188
184
  },
189
185
  auth: (username, password, cb) => {