@mblaney/holster 2.1.3 → 2.1.4
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 +22 -9
- 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/holster.js +83 -34
- package/src/radisk.js +82 -23
- package/src/radix.js +52 -22
- package/src/store.js +1 -0
- package/src/wire.js +29 -6
- package/ts/src/holster.ts +96 -48
- package/ts/src/radisk.ts +73 -18
- package/ts/src/radix.ts +58 -28
- package/ts/src/store.ts +1 -0
- package/ts/src/wire.ts +34 -6
package/ts/src/holster.ts
CHANGED
|
@@ -78,6 +78,11 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
78
78
|
const user = User(options, wire as never)
|
|
79
79
|
const map = new Map<(data: unknown) => void, () => void>()
|
|
80
80
|
const allctx = new Map<string, ApiContext>()
|
|
81
|
+
// Serializes concurrent creation of a missing rel for the same
|
|
82
|
+
// soul+item, so only the first caller creates it and the rest reuse
|
|
83
|
+
// its result rather than each minting a competing soul. Map<soul,
|
|
84
|
+
// Map<item, promise resolving to the soul id>>.
|
|
85
|
+
const pendingRel = new Map<string, Map<string, Promise<string>>>()
|
|
81
86
|
|
|
82
87
|
const ok = (data: GraphValue): boolean => {
|
|
83
88
|
return (
|
|
@@ -275,6 +280,72 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
275
280
|
}
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
// Creates a rel for item on soul, or reuses one already being created
|
|
284
|
+
// by a concurrent caller for the same soul+item (see pendingRel above).
|
|
285
|
+
// Calls cb(err, id) with a ready-to-display err, already prefixed where
|
|
286
|
+
// needed - callers should pass it straight through, not wrap it again.
|
|
287
|
+
const createRel = (
|
|
288
|
+
soul: string,
|
|
289
|
+
item: string,
|
|
290
|
+
node: Record<string, GraphValue>,
|
|
291
|
+
user: UserIdentity | null | undefined,
|
|
292
|
+
cb: (err?: string | null, id?: string) => void,
|
|
293
|
+
): void => {
|
|
294
|
+
let bySoul = pendingRel.get(soul)
|
|
295
|
+
const pending = bySoul && bySoul.get(item)
|
|
296
|
+
if (pending) {
|
|
297
|
+
pending.then(
|
|
298
|
+
id => cb(undefined, id),
|
|
299
|
+
err => cb(err as string),
|
|
300
|
+
)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const id = utils.text.random()
|
|
305
|
+
node[item] = utils.rel.ify(id)
|
|
306
|
+
let settle!: (id: string) => void
|
|
307
|
+
let fail!: (err: unknown) => void
|
|
308
|
+
const promise = new Promise<string>((res, rej) => {
|
|
309
|
+
settle = res
|
|
310
|
+
fail = rej
|
|
311
|
+
})
|
|
312
|
+
promise.catch(() => {})
|
|
313
|
+
if (!bySoul) {
|
|
314
|
+
bySoul = new Map()
|
|
315
|
+
pendingRel.set(soul, bySoul)
|
|
316
|
+
}
|
|
317
|
+
bySoul.set(item, promise)
|
|
318
|
+
const clear = () => {
|
|
319
|
+
bySoul!.delete(item)
|
|
320
|
+
if (bySoul!.size === 0) pendingRel.delete(soul)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
void (async () => {
|
|
324
|
+
// graph() itself calls cb with an already-formatted message when it
|
|
325
|
+
// fails (secure mode, no user) - pass cb straight through rather
|
|
326
|
+
// than wrapping, so that message isn't prefixed a second time.
|
|
327
|
+
const g = await graph(soul, node, user, cb)
|
|
328
|
+
if (g === null) {
|
|
329
|
+
clear()
|
|
330
|
+
fail(new Error("secure mode"))
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
wire.put(g, err => {
|
|
335
|
+
clear()
|
|
336
|
+
if (err) {
|
|
337
|
+
const message = `error putting ${item} on ${soul}: ${err}`
|
|
338
|
+
fail(message)
|
|
339
|
+
cb(message)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
settle(id)
|
|
344
|
+
cb(undefined, id)
|
|
345
|
+
})
|
|
346
|
+
})()
|
|
347
|
+
}
|
|
348
|
+
|
|
278
349
|
const resolve = (
|
|
279
350
|
request: {
|
|
280
351
|
get?: LexFilter
|
|
@@ -341,28 +412,20 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
341
412
|
} else if (get) {
|
|
342
413
|
cb!(node[item!])
|
|
343
414
|
} else if (put) {
|
|
344
|
-
|
|
345
|
-
node[item!] = utils.rel.ify(id)
|
|
346
|
-
const g = await graph(
|
|
415
|
+
createRel(
|
|
347
416
|
soul!,
|
|
417
|
+
item!,
|
|
348
418
|
node as never,
|
|
349
419
|
ctx.user,
|
|
350
|
-
|
|
420
|
+
(err, relId) => {
|
|
421
|
+
if (err) {
|
|
422
|
+
;(cb as (err: string) => void)(err)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
ctx.chain[i]!.soul = relId!
|
|
426
|
+
api(ctxid).put(request.put!, cb as never)
|
|
427
|
+
},
|
|
351
428
|
)
|
|
352
|
-
if (g === null) return
|
|
353
|
-
|
|
354
|
-
wire.put(g, err => {
|
|
355
|
-
if (err) {
|
|
356
|
-
;(cb as (err: string) => void)(
|
|
357
|
-
`error putting ${item} on ${soul}: ${err}`,
|
|
358
|
-
)
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
if (id) {
|
|
362
|
-
ctx.chain[i]!.soul = id
|
|
363
|
-
}
|
|
364
|
-
api(ctxid).put(request.put!, cb as never)
|
|
365
|
-
})
|
|
366
429
|
} else if (on) {
|
|
367
430
|
// Item is not a rel yet — watch the parent soul for when it
|
|
368
431
|
// becomes one, then retry chain resolution.
|
|
@@ -372,20 +435,13 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
372
435
|
if (cb) cb(null)
|
|
373
436
|
}
|
|
374
437
|
} else if (put) {
|
|
375
|
-
const id = utils.text.random()
|
|
376
438
|
if (!node) node = {} as never
|
|
377
|
-
|
|
378
|
-
const g = await graph(soul!, node as never, ctx.user, cb as never)
|
|
379
|
-
if (g === null) return
|
|
380
|
-
|
|
381
|
-
wire.put(g, err => {
|
|
439
|
+
createRel(soul!, item!, node as never, ctx.user, (err, relId) => {
|
|
382
440
|
if (err) {
|
|
383
|
-
;(cb as (err: string) => void)(
|
|
384
|
-
`error putting ${item} on ${soul}: ${err}`,
|
|
385
|
-
)
|
|
441
|
+
;(cb as (err: string) => void)(err)
|
|
386
442
|
return
|
|
387
443
|
}
|
|
388
|
-
ctx.chain[i]!.soul =
|
|
444
|
+
ctx.chain[i]!.soul = relId!
|
|
389
445
|
api(ctxid).put(request.put!, cb as never)
|
|
390
446
|
})
|
|
391
447
|
} else {
|
|
@@ -708,28 +764,20 @@ const Holster = (opt?: HolsterOptions | string | string[]): HolsterAPI => {
|
|
|
708
764
|
const id = utils.rel.is(current as GraphValue)
|
|
709
765
|
if (!id) {
|
|
710
766
|
if (!node) node = {} as never
|
|
711
|
-
|
|
712
|
-
const g = await graph(
|
|
713
|
-
soul,
|
|
714
|
-
node as never,
|
|
715
|
-
ctx.user,
|
|
716
|
-
_ack as never,
|
|
717
|
-
)
|
|
718
|
-
if (g === null) return
|
|
719
|
-
|
|
720
|
-
wire.put(g, err => {
|
|
767
|
+
createRel(soul, item!, node as never, ctx.user, err => {
|
|
721
768
|
if (err) {
|
|
722
|
-
_ack(
|
|
723
|
-
|
|
724
|
-
const _ctxid = utils.text.random()
|
|
725
|
-
const chain: ChainItem[] = [{item: item!, soul: soul}]
|
|
726
|
-
allctx.set(_ctxid, {
|
|
727
|
-
chain: chain,
|
|
728
|
-
user: ctx.user,
|
|
729
|
-
cb: ctx.cb,
|
|
730
|
-
})
|
|
731
|
-
api(_ctxid).put(data)
|
|
769
|
+
_ack(err)
|
|
770
|
+
return
|
|
732
771
|
}
|
|
772
|
+
|
|
773
|
+
const _ctxid = utils.text.random()
|
|
774
|
+
const chain: ChainItem[] = [{item: item!, soul: soul}]
|
|
775
|
+
allctx.set(_ctxid, {
|
|
776
|
+
chain: chain,
|
|
777
|
+
user: ctx.user,
|
|
778
|
+
cb: ctx.cb,
|
|
779
|
+
})
|
|
780
|
+
api(_ctxid).put(data)
|
|
733
781
|
})
|
|
734
782
|
return
|
|
735
783
|
}
|
package/ts/src/radisk.ts
CHANGED
|
@@ -229,10 +229,7 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
229
229
|
radisk.batch = Radix() as RadiskInterface["batch"]
|
|
230
230
|
radisk.batch.acks = []
|
|
231
231
|
radisk.batch.ed = 0
|
|
232
|
-
let i = 0
|
|
233
232
|
radisk.save(batch, err => {
|
|
234
|
-
if (++i > 1) return
|
|
235
|
-
|
|
236
233
|
if (err) options.log(err)
|
|
237
234
|
batch.acks.forEach(cb => cb(err))
|
|
238
235
|
radisk.thrash.at = undefined
|
|
@@ -246,26 +243,54 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
246
243
|
find: (_tree: unknown, key: string): boolean | undefined => {
|
|
247
244
|
if (key < (save.start || "")) return undefined
|
|
248
245
|
save.start = key
|
|
249
|
-
store.list
|
|
246
|
+
// store.list's own streamed order isn't guaranteed to be sorted -
|
|
247
|
+
// fs.readdir in particular makes no such promise, unlike
|
|
248
|
+
// radisk.read's own file lookup, which already sorts before
|
|
249
|
+
// searching. Collect the full list and sort it first, rather
|
|
250
|
+
// than scanning the raw stream as it arrives, since save.lex's
|
|
251
|
+
// own boundary search below only works correctly over genuinely
|
|
252
|
+
// sorted input - fed an unsorted stream, it can match the wrong
|
|
253
|
+
// file, or stop tracking new candidates the moment the stream
|
|
254
|
+
// stops looking monotonic, silently losing track of files that
|
|
255
|
+
// arrive afterward.
|
|
256
|
+
const files: string[] = []
|
|
257
|
+
store.list(file => {
|
|
258
|
+
if (file) {
|
|
259
|
+
files.push(file)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
files.sort()
|
|
263
|
+
save.lex(files)
|
|
264
|
+
})
|
|
250
265
|
return true
|
|
251
266
|
},
|
|
252
|
-
lex: (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
lex: (files: string[]): void => {
|
|
268
|
+
// save.start can be set to undefined by a previous call to
|
|
269
|
+
// save.mix, so don't continue in this case.
|
|
270
|
+
if (!save.start) return
|
|
271
|
+
|
|
272
|
+
let candidate: string | null = null
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
if (file > save.start) {
|
|
275
|
+
// ! is used as the first file name as it's the first
|
|
276
|
+
// printable character, so always matches as lexically less
|
|
277
|
+
// than any node.
|
|
278
|
+
save.mix(candidate || "!", save.start, file)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
candidate = file
|
|
258
282
|
}
|
|
283
|
+
save.mix(candidate || "!", save.start, undefined)
|
|
259
284
|
},
|
|
260
285
|
mix: (file: string, start: string, end?: string): void => {
|
|
261
|
-
save.start =
|
|
286
|
+
save.start = undefined
|
|
262
287
|
if (cache.has(file)) {
|
|
263
288
|
const disk = cache.get(file)!
|
|
264
289
|
Radix.map(rad, (value, key) => {
|
|
265
290
|
if (key < start) return undefined
|
|
266
291
|
if (end && end < key) {
|
|
267
292
|
save.start = key
|
|
268
|
-
return
|
|
293
|
+
return true
|
|
269
294
|
}
|
|
270
295
|
disk(key, value)
|
|
271
296
|
return undefined
|
|
@@ -278,11 +303,21 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
278
303
|
if (key < start) return undefined
|
|
279
304
|
if (end && end < key) {
|
|
280
305
|
save.start = key
|
|
281
|
-
return
|
|
306
|
+
return true
|
|
282
307
|
}
|
|
283
308
|
disk!(key, value)
|
|
284
309
|
return undefined
|
|
285
310
|
})
|
|
311
|
+
// Populate the cache with this merge synchronously, before the
|
|
312
|
+
// async store.put below even starts - a write's own merged
|
|
313
|
+
// result is always authoritative (it's this batch's actual
|
|
314
|
+
// content), unlike a plain read's parse, which can't tell
|
|
315
|
+
// whether it raced ahead of a write. Closes the window where a
|
|
316
|
+
// concurrent read's own parse-from-disk (started before this
|
|
317
|
+
// write, resolving after it) would otherwise be free to
|
|
318
|
+
// populate the cache with stale, pre-write data - see
|
|
319
|
+
// radisk.read's own matching change below.
|
|
320
|
+
if (options.cache) cache.set(file, disk!)
|
|
286
321
|
radisk.write(file, disk!, save.next)
|
|
287
322
|
})
|
|
288
323
|
}
|
|
@@ -296,13 +331,26 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
296
331
|
cb(err)
|
|
297
332
|
},
|
|
298
333
|
start: undefined as string | undefined,
|
|
299
|
-
end: undefined as string | undefined,
|
|
300
|
-
file: undefined as string | undefined,
|
|
301
334
|
}
|
|
302
335
|
Radix.map(rad, save.find)
|
|
303
336
|
}
|
|
304
337
|
|
|
305
338
|
radisk.write = (file, rad, cb) => {
|
|
339
|
+
// A split produces two independent store.put calls - this file's own
|
|
340
|
+
// (truncated) remainder below, and the recursive call for the
|
|
341
|
+
// split-off sub-file. pending starts at 1 for this file's own write,
|
|
342
|
+
// and is bumped to 2 the moment a split is decided, so cb only fires
|
|
343
|
+
// once both halves are actually durable - calling it as soon as
|
|
344
|
+
// whichever one finishes first (the previous behavior) let a caller
|
|
345
|
+
// continue - including resolving the original put()'s own callback -
|
|
346
|
+
// before the other half's data had actually been written.
|
|
347
|
+
let pending = 1
|
|
348
|
+
let firstErr: string | null = null
|
|
349
|
+
const settle = (err?: string | null): void => {
|
|
350
|
+
if (err && !firstErr) firstErr = err
|
|
351
|
+
if (--pending > 0) return
|
|
352
|
+
cb(firstErr)
|
|
353
|
+
}
|
|
306
354
|
const write = {
|
|
307
355
|
text: "",
|
|
308
356
|
limit: "",
|
|
@@ -360,7 +408,8 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
360
408
|
write.done = true
|
|
361
409
|
write.sub = Radix()
|
|
362
410
|
Radix.map(rad, write.slice)
|
|
363
|
-
|
|
411
|
+
pending++
|
|
412
|
+
radisk.write(write.limit, write.sub, settle)
|
|
364
413
|
return undefined
|
|
365
414
|
}
|
|
366
415
|
}
|
|
@@ -380,7 +429,7 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
380
429
|
cache.delete(file)
|
|
381
430
|
fileListCache = null
|
|
382
431
|
}
|
|
383
|
-
|
|
432
|
+
settle(err)
|
|
384
433
|
})
|
|
385
434
|
}
|
|
386
435
|
|
|
@@ -421,7 +470,13 @@ const Radisk = (opt: RadiskOptions) => {
|
|
|
421
470
|
}
|
|
422
471
|
radisk.parse(file, (err, disk) => {
|
|
423
472
|
if (err) options.log(err)
|
|
424
|
-
|
|
473
|
+
// Never overwrite an existing cache entry from a plain read - it
|
|
474
|
+
// can't tell whether it raced ahead of a concurrent write (started
|
|
475
|
+
// before that write, but resolving after the write already
|
|
476
|
+
// installed its own authoritative, freshly-merged entry - see
|
|
477
|
+
// save.mix's own matching change). Only populate the cache when
|
|
478
|
+
// nothing beat this read to it.
|
|
479
|
+
if (disk && options.cache && !cache.has(file)) {
|
|
425
480
|
cache.set(file, disk)
|
|
426
481
|
checkMemoryUsage()
|
|
427
482
|
}
|
package/ts/src/radix.ts
CHANGED
|
@@ -57,44 +57,74 @@ const Radix = (): RadixFunction => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (!found) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
matchingKey
|
|
68
|
-
j
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
if (noValue) {
|
|
61
|
+
// If not found from the provided keys try matching with an
|
|
62
|
+
// existing key.
|
|
63
|
+
const result = utils.obj.map(
|
|
64
|
+
tree!,
|
|
65
|
+
(hasValue: RadixValue, hasKey: string) => {
|
|
66
|
+
let j = 0
|
|
67
|
+
let matchingKey = ""
|
|
68
|
+
while (hasKey[j] === keys[j]) {
|
|
69
|
+
matchingKey += hasKey[j]
|
|
70
|
+
j++
|
|
71
|
+
}
|
|
72
|
+
if (matchingKey) {
|
|
73
|
+
// matchingKey has to be as long as the original keys when
|
|
74
|
+
// reading.
|
|
73
75
|
if (j <= max) return undefined
|
|
74
76
|
|
|
75
77
|
tmp[hasKey.slice(j)] = hasValue
|
|
76
78
|
return hasValue
|
|
77
79
|
}
|
|
80
|
+
return undefined
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
if (!result) return undefined
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[keys.slice(j)]: {[record]: value!},
|
|
82
|
-
}
|
|
83
|
-
tree![matchingKey] = {[group]: replace}
|
|
84
|
-
delete tree![hasKey]
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
return undefined
|
|
88
|
-
},
|
|
89
|
-
)
|
|
85
|
+
return tmp
|
|
86
|
+
}
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
// Writing: find the EXISTING key with the longest shared prefix,
|
|
89
|
+
// not just the first one with any overlap at all (obj.map stops at
|
|
90
|
+
// the first non-undefined result, so a plain find here would settle
|
|
91
|
+
// for whichever candidate happens to come first in Object.keys'
|
|
92
|
+
// own iteration order). A radix tree's own invariant - no two
|
|
93
|
+
// sibling keys share a prefix - only holds if every insertion
|
|
94
|
+
// merges with its one true match; with many keys sharing this tree
|
|
95
|
+
// at once (many concurrent writers batched together), a
|
|
96
|
+
// coincidentally shorter, unrelated key can otherwise "win" over
|
|
97
|
+
// the real, much longer match - silently misplacing the new key
|
|
98
|
+
// under the wrong group, while its actual sibling is never touched
|
|
99
|
+
// at all and stays wherever it already was, invisible to any read
|
|
100
|
+
// that expects it inside this new group.
|
|
101
|
+
let bestKey: string | null = null
|
|
102
|
+
let bestMatch = ""
|
|
103
|
+
for (const hasKey of Object.keys(tree!)) {
|
|
104
|
+
let j = 0
|
|
105
|
+
let matchingKey = ""
|
|
106
|
+
while (hasKey[j] === keys[j]) {
|
|
107
|
+
matchingKey += hasKey[j]
|
|
108
|
+
j++
|
|
109
|
+
}
|
|
110
|
+
if (matchingKey.length > bestMatch.length) {
|
|
111
|
+
bestKey = hasKey
|
|
112
|
+
bestMatch = matchingKey
|
|
113
|
+
}
|
|
114
|
+
}
|
|
93
115
|
|
|
116
|
+
if (bestKey) {
|
|
117
|
+
const hasValue = tree![bestKey]!
|
|
118
|
+
const j = bestMatch.length
|
|
119
|
+
const replace: RadixTree = {
|
|
120
|
+
[bestKey.slice(j)]: hasValue,
|
|
121
|
+
[keys.slice(j)]: {[record]: value!},
|
|
122
|
+
}
|
|
123
|
+
tree![bestMatch] = {[group]: replace}
|
|
124
|
+
delete tree![bestKey]
|
|
125
|
+
} else {
|
|
94
126
|
if (!tree![key]) tree![key] = {}
|
|
95
127
|
tree![key]![record] = value!
|
|
96
|
-
} else if (noValue) {
|
|
97
|
-
return tmp
|
|
98
128
|
}
|
|
99
129
|
} else if (i === max) {
|
|
100
130
|
// If no value use the key provided to return a whole group or record
|
package/ts/src/store.ts
CHANGED
package/ts/src/wire.ts
CHANGED
|
@@ -370,6 +370,14 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
370
370
|
console.log("Run npx holster-user-storage to view full keys and all users")
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// The check function is required because user data must provide a public key
|
|
374
|
+
// so that it can be verified. The public key might verify the provided
|
|
375
|
+
// signature but not actually match the user under which the data is being
|
|
376
|
+
// stored. To avoid this, the current data on a soul needs to be checked to
|
|
377
|
+
// make sure the stored public key matches the one provided with the update.
|
|
378
|
+
// This must run before Ham.mix, which updates graph in place — checking
|
|
379
|
+
// afterwards would compare the incoming update's public key against itself
|
|
380
|
+
// instead of the previously stored one.
|
|
373
381
|
const check = async (
|
|
374
382
|
data: Graph,
|
|
375
383
|
send: (msg: string) => {err?: string} | void,
|
|
@@ -383,8 +391,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
383
391
|
// allow it - the update is not trying to change the public key.
|
|
384
392
|
if (node[key] === undefined) continue
|
|
385
393
|
|
|
394
|
+
// A full-node lookup is required (rather than a targeted property
|
|
395
|
+
// lookup) so that a soul which exists but has never had a public key
|
|
396
|
+
// can be told apart from a soul that doesn't exist yet — both would
|
|
397
|
+
// otherwise resolve to the same "not found" shape for this one key.
|
|
386
398
|
const msg = await new Promise<WireMessage>(res => {
|
|
387
|
-
getWithCallback({"#": soul
|
|
399
|
+
getWithCallback({"#": soul}, res, send, {put: true})
|
|
388
400
|
})
|
|
389
401
|
if (msg.err) {
|
|
390
402
|
if (cb) cb(msg.err)
|
|
@@ -395,7 +407,12 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
395
407
|
// matching public keys, as the provided soul also needs a rel on the
|
|
396
408
|
// parent node which then also requires checking. Otherwise public keys
|
|
397
409
|
// need to match for existing data.
|
|
398
|
-
|
|
410
|
+
const existing = msg.put && msg.put[soul]
|
|
411
|
+
if (
|
|
412
|
+
!existing ||
|
|
413
|
+
existing[key] === undefined ||
|
|
414
|
+
existing[key] === node[key]
|
|
415
|
+
) {
|
|
399
416
|
continue
|
|
400
417
|
}
|
|
401
418
|
|
|
@@ -449,6 +466,8 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
449
466
|
}
|
|
450
467
|
}
|
|
451
468
|
|
|
469
|
+
if (!(await check(msg.put, send as never))) return
|
|
470
|
+
|
|
452
471
|
const update = await Ham.mix(
|
|
453
472
|
msg.put,
|
|
454
473
|
graph,
|
|
@@ -467,8 +486,6 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
467
486
|
return
|
|
468
487
|
}
|
|
469
488
|
|
|
470
|
-
if (!(await check(update.now, send as never))) return
|
|
471
|
-
|
|
472
489
|
const finish = (err?: string | null): void => {
|
|
473
490
|
if (err) console.warn("store.put", err)
|
|
474
491
|
send(
|
|
@@ -679,6 +696,10 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
679
696
|
}
|
|
680
697
|
}
|
|
681
698
|
}
|
|
699
|
+
if (!(await check(data, send as never, cb))) {
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
|
|
682
703
|
const update = await Ham.mix(
|
|
683
704
|
data,
|
|
684
705
|
graph,
|
|
@@ -692,8 +713,15 @@ const Wire = (opt: HolsterOptions): WireAPI => {
|
|
|
692
713
|
return
|
|
693
714
|
}
|
|
694
715
|
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
// Ham.mix has already merged this locally-authored update into
|
|
717
|
+
// graph[soul], synchronously, before store.put below even starts
|
|
718
|
+
// persisting it - mark it as fully known now rather than waiting
|
|
719
|
+
// for a future disk fetch to do so, so a read landing in that
|
|
720
|
+
// window doesn't get forced through a disk round-trip that could
|
|
721
|
+
// itself race ahead of this same write and find nothing, even
|
|
722
|
+
// though the correct answer was already sitting in memory.
|
|
723
|
+
for (const soul of Object.keys(update.now)) {
|
|
724
|
+
hasNode.add(soul)
|
|
697
725
|
}
|
|
698
726
|
|
|
699
727
|
if (options.userLimit) {
|