@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/package.json
CHANGED
package/src/holster.js
CHANGED
|
@@ -14,6 +14,11 @@ const Holster = opt => {
|
|
|
14
14
|
const map = new Map()
|
|
15
15
|
// Allow concurrent calls to the api by storing each context.
|
|
16
16
|
const allctx = new Map()
|
|
17
|
+
// Serializes concurrent creation of a missing rel for the same
|
|
18
|
+
// soul+item, so only the first caller creates it and the rest reuse
|
|
19
|
+
// its result rather than each minting a competing soul. Map<soul,
|
|
20
|
+
// Map<item, promise resolving to the soul id>>.
|
|
21
|
+
const pendingRel = new Map()
|
|
17
22
|
|
|
18
23
|
const ok = data => {
|
|
19
24
|
return (
|
|
@@ -181,6 +186,65 @@ const Holster = opt => {
|
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
// Creates a rel for item on soul, or reuses one already being created
|
|
190
|
+
// by a concurrent caller for the same soul+item (see pendingRel above).
|
|
191
|
+
// Calls cb(err, id) with a ready-to-display err, already prefixed where
|
|
192
|
+
// needed - callers should pass it straight through, not wrap it again.
|
|
193
|
+
const createRel = (soul, item, node, user, cb) => {
|
|
194
|
+
let bySoul = pendingRel.get(soul)
|
|
195
|
+
const pending = bySoul && bySoul.get(item)
|
|
196
|
+
if (pending) {
|
|
197
|
+
pending.then(
|
|
198
|
+
id => cb(null, id),
|
|
199
|
+
err => cb(err),
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const id = utils.text.random()
|
|
205
|
+
node[item] = utils.rel.ify(id)
|
|
206
|
+
let settle, fail
|
|
207
|
+
const promise = new Promise((res, rej) => {
|
|
208
|
+
settle = res
|
|
209
|
+
fail = rej
|
|
210
|
+
})
|
|
211
|
+
promise.catch(() => {})
|
|
212
|
+
if (!bySoul) {
|
|
213
|
+
bySoul = new Map()
|
|
214
|
+
pendingRel.set(soul, bySoul)
|
|
215
|
+
}
|
|
216
|
+
bySoul.set(item, promise)
|
|
217
|
+
const clear = () => {
|
|
218
|
+
bySoul.delete(item)
|
|
219
|
+
if (bySoul.size === 0) pendingRel.delete(soul)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
;(async () => {
|
|
223
|
+
// graph() itself calls cb with an already-formatted message when it
|
|
224
|
+
// fails (secure mode, no user) - pass cb straight through rather
|
|
225
|
+
// than wrapping, so that message isn't prefixed a second time.
|
|
226
|
+
const g = await graph(soul, node, user, cb)
|
|
227
|
+
if (g === null) {
|
|
228
|
+
clear()
|
|
229
|
+
fail(new Error("secure mode"))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
wire.put(g, err => {
|
|
234
|
+
clear()
|
|
235
|
+
if (err) {
|
|
236
|
+
const message = `error putting ${item} on ${soul}: ${err}`
|
|
237
|
+
fail(message)
|
|
238
|
+
cb(message)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
settle(id)
|
|
243
|
+
cb(null, id)
|
|
244
|
+
})
|
|
245
|
+
})()
|
|
246
|
+
}
|
|
247
|
+
|
|
184
248
|
const resolve = (request, cb) => {
|
|
185
249
|
if (!request) {
|
|
186
250
|
console.log("error resolve request parameter required")
|
|
@@ -236,18 +300,13 @@ const Holster = opt => {
|
|
|
236
300
|
cb(node[item])
|
|
237
301
|
} else if (put) {
|
|
238
302
|
// Request was chained before put, so rel doesn't exist yet.
|
|
239
|
-
|
|
240
|
-
node[item] = utils.rel.ify(id)
|
|
241
|
-
const g = await graph(soul, node, ctx.user, cb)
|
|
242
|
-
if (g === null) return
|
|
243
|
-
|
|
244
|
-
wire.put(g, err => {
|
|
303
|
+
createRel(soul, item, node, ctx.user, (err, relId) => {
|
|
245
304
|
if (err) {
|
|
246
|
-
cb(
|
|
305
|
+
cb(err)
|
|
247
306
|
return
|
|
248
307
|
}
|
|
249
308
|
|
|
250
|
-
ctx.chain[i].soul =
|
|
309
|
+
ctx.chain[i].soul = relId
|
|
251
310
|
api(ctxid).put(request.put, cb)
|
|
252
311
|
})
|
|
253
312
|
} else if (on) {
|
|
@@ -261,19 +320,13 @@ const Holster = opt => {
|
|
|
261
320
|
}
|
|
262
321
|
} else if (put) {
|
|
263
322
|
// Request was chained before put, so rel doesn't exist yet.
|
|
264
|
-
|
|
265
|
-
if (!node) node = {}
|
|
266
|
-
node[item] = utils.rel.ify(id)
|
|
267
|
-
const g = await graph(soul, node, ctx.user, cb)
|
|
268
|
-
if (g === null) return
|
|
269
|
-
|
|
270
|
-
wire.put(g, err => {
|
|
323
|
+
createRel(soul, item, node || {}, ctx.user, (err, relId) => {
|
|
271
324
|
if (err) {
|
|
272
|
-
cb(
|
|
325
|
+
cb(err)
|
|
273
326
|
return
|
|
274
327
|
}
|
|
275
328
|
|
|
276
|
-
ctx.chain[i].soul =
|
|
329
|
+
ctx.chain[i].soul = relId
|
|
277
330
|
api(ctxid).put(request.put, cb)
|
|
278
331
|
})
|
|
279
332
|
} else {
|
|
@@ -541,25 +594,21 @@ const Holster = opt => {
|
|
|
541
594
|
const id = utils.rel.is(current)
|
|
542
595
|
if (!id) {
|
|
543
596
|
// The current rel doesn't exist, so add it first.
|
|
544
|
-
|
|
545
|
-
node[item] = utils.rel.ify(utils.text.random())
|
|
546
|
-
const g = await graph(soul, node, ctx.user, _ack)
|
|
547
|
-
if (g === null) return
|
|
548
|
-
|
|
549
|
-
wire.put(g, err => {
|
|
597
|
+
createRel(soul, item, node || {}, ctx.user, err => {
|
|
550
598
|
if (err) {
|
|
551
|
-
_ack(
|
|
552
|
-
|
|
553
|
-
const _ctxid = utils.text.random()
|
|
554
|
-
const chain = [{item: item, soul: soul}]
|
|
555
|
-
// Pass on the previous ctx's callback and user flag here.
|
|
556
|
-
allctx.set(_ctxid, {
|
|
557
|
-
chain: chain,
|
|
558
|
-
user: ctx.user,
|
|
559
|
-
cb: ctx.cb,
|
|
560
|
-
})
|
|
561
|
-
api(_ctxid).put(data)
|
|
599
|
+
_ack(err)
|
|
600
|
+
return
|
|
562
601
|
}
|
|
602
|
+
|
|
603
|
+
const _ctxid = utils.text.random()
|
|
604
|
+
const chain = [{item: item, soul: soul}]
|
|
605
|
+
// Pass on the previous ctx's callback and user flag here.
|
|
606
|
+
allctx.set(_ctxid, {
|
|
607
|
+
chain: chain,
|
|
608
|
+
user: ctx.user,
|
|
609
|
+
cb: ctx.cb,
|
|
610
|
+
})
|
|
611
|
+
api(_ctxid).put(data)
|
|
563
612
|
})
|
|
564
613
|
return
|
|
565
614
|
}
|
package/src/radisk.js
CHANGED
|
@@ -171,13 +171,7 @@ const Radisk = opt => {
|
|
|
171
171
|
radisk.batch = Radix()
|
|
172
172
|
radisk.batch.acks = []
|
|
173
173
|
radisk.batch.ed = 0
|
|
174
|
-
let i = 0
|
|
175
174
|
radisk.save(batch, err => {
|
|
176
|
-
// This is to ignore multiple callbacks from radisk.save calling
|
|
177
|
-
// radisk.write? It looks like multiple callbacks will be made if a
|
|
178
|
-
// file needs to be split.
|
|
179
|
-
if (++i > 1) return
|
|
180
|
-
|
|
181
175
|
if (err) opt.log(err)
|
|
182
176
|
batch.acks.forEach(cb => cb(err))
|
|
183
177
|
radisk.thrash.at = null
|
|
@@ -199,23 +193,47 @@ const Radisk = opt => {
|
|
|
199
193
|
if (key < save.start) return
|
|
200
194
|
|
|
201
195
|
save.start = key
|
|
202
|
-
|
|
196
|
+
// store.list's own streamed order isn't guaranteed to be sorted -
|
|
197
|
+
// fs.readdir in particular makes no such promise, unlike
|
|
198
|
+
// radisk.read's own file lookup, which already sorts before
|
|
199
|
+
// searching. Collect the full list and sort it first, rather
|
|
200
|
+
// than scanning the raw stream as it arrives, since save.lex's
|
|
201
|
+
// own boundary search below only works correctly over genuinely
|
|
202
|
+
// sorted input - fed an unsorted stream, it can match the wrong
|
|
203
|
+
// file, or stop tracking new candidates the moment the stream
|
|
204
|
+
// stops looking monotonic, silently losing track of files that
|
|
205
|
+
// arrive afterward.
|
|
206
|
+
const files = []
|
|
207
|
+
opt.store.list(file => {
|
|
208
|
+
if (file) {
|
|
209
|
+
files.push(file)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
files.sort()
|
|
213
|
+
save.lex(files)
|
|
214
|
+
})
|
|
203
215
|
return true
|
|
204
216
|
},
|
|
205
|
-
lex:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
lex: files => {
|
|
218
|
+
// save.start can be set to undefined by a previous call to
|
|
219
|
+
// save.mix, so don't continue in this case.
|
|
220
|
+
if (!save.start) return
|
|
221
|
+
|
|
222
|
+
let candidate = null
|
|
223
|
+
for (const file of files) {
|
|
224
|
+
if (file > save.start) {
|
|
225
|
+
// ! is used as the first file name as it's the first
|
|
226
|
+
// printable character, so always matches as lexically less
|
|
227
|
+
// than any node.
|
|
228
|
+
save.mix(candidate || "!", save.start, file)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
candidate = file
|
|
215
232
|
}
|
|
233
|
+
save.mix(candidate || "!", save.start, u)
|
|
216
234
|
},
|
|
217
235
|
mix: (file, start, end) => {
|
|
218
|
-
save.start =
|
|
236
|
+
save.start = u
|
|
219
237
|
// Use cache if available, otherwise parse from disk
|
|
220
238
|
if (cache.has(file)) {
|
|
221
239
|
const disk = cache.get(file)
|
|
@@ -223,8 +241,17 @@ const Radisk = opt => {
|
|
|
223
241
|
if (key < start) return
|
|
224
242
|
|
|
225
243
|
if (end && end < key) {
|
|
244
|
+
// Found where the next file segment starts - stop here (the
|
|
245
|
+
// non-undefined return is what actually stops Radix.map's
|
|
246
|
+
// own iteration). Without it, the scan kept going through
|
|
247
|
+
// every remaining entry in the whole batch, each one
|
|
248
|
+
// overwriting save.start in turn, so save.next's own resume
|
|
249
|
+
// point ended up as the LAST key in the batch rather than
|
|
250
|
+
// the very next one - silently skipping every entry in
|
|
251
|
+
// between as "already processed" when none of them ever
|
|
252
|
+
// were.
|
|
226
253
|
save.start = key
|
|
227
|
-
return
|
|
254
|
+
return true
|
|
228
255
|
}
|
|
229
256
|
|
|
230
257
|
disk(key, value)
|
|
@@ -239,11 +266,21 @@ const Radisk = opt => {
|
|
|
239
266
|
|
|
240
267
|
if (end && end < key) {
|
|
241
268
|
save.start = key
|
|
242
|
-
return
|
|
269
|
+
return true
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
disk(key, value)
|
|
246
273
|
})
|
|
274
|
+
// Populate the cache with this merge synchronously, before the
|
|
275
|
+
// async store.put below even starts - a write's own merged
|
|
276
|
+
// result is always authoritative (it's this batch's actual
|
|
277
|
+
// content), unlike a plain read's parse, which can't tell
|
|
278
|
+
// whether it raced ahead of a write. Closes the window where a
|
|
279
|
+
// concurrent read's own parse-from-disk (started before this
|
|
280
|
+
// write, resolving after it) would otherwise be free to
|
|
281
|
+
// populate the cache with stale, pre-write data - see
|
|
282
|
+
// radisk.read's own matching change below.
|
|
283
|
+
if (opt.cache) cache.set(file, disk)
|
|
247
284
|
radisk.write(file, disk, save.next)
|
|
248
285
|
})
|
|
249
286
|
}
|
|
@@ -260,6 +297,21 @@ const Radisk = opt => {
|
|
|
260
297
|
}
|
|
261
298
|
|
|
262
299
|
radisk.write = (file, rad, cb) => {
|
|
300
|
+
// A split produces two independent opt.store.put calls - this file's
|
|
301
|
+
// own (truncated) remainder below, and the recursive call for the
|
|
302
|
+
// split-off sub-file. pending starts at 1 for this file's own write,
|
|
303
|
+
// and is bumped to 2 the moment a split is decided, so cb only fires
|
|
304
|
+
// once both halves are actually durable - calling it as soon as
|
|
305
|
+
// whichever one finishes first (the previous behavior) let a caller
|
|
306
|
+
// continue - including resolving the original put()'s own callback -
|
|
307
|
+
// before the other half's data had actually been written.
|
|
308
|
+
let pending = 1
|
|
309
|
+
let firstErr = null
|
|
310
|
+
const settle = err => {
|
|
311
|
+
if (err && !firstErr) firstErr = err
|
|
312
|
+
if (--pending > 0) return
|
|
313
|
+
cb(firstErr)
|
|
314
|
+
}
|
|
263
315
|
const write = {
|
|
264
316
|
text: "",
|
|
265
317
|
limit: "",
|
|
@@ -318,7 +370,8 @@ const Radisk = opt => {
|
|
|
318
370
|
write.done = true
|
|
319
371
|
write.sub = Radix()
|
|
320
372
|
Radix.map(rad, write.slice)
|
|
321
|
-
|
|
373
|
+
pending++
|
|
374
|
+
radisk.write(write.limit, write.sub, settle)
|
|
322
375
|
return
|
|
323
376
|
}
|
|
324
377
|
}
|
|
@@ -342,7 +395,7 @@ const Radisk = opt => {
|
|
|
342
395
|
cache.delete(file)
|
|
343
396
|
fileListCache = null
|
|
344
397
|
}
|
|
345
|
-
|
|
398
|
+
settle(err)
|
|
346
399
|
})
|
|
347
400
|
}
|
|
348
401
|
|
|
@@ -378,7 +431,13 @@ const Radisk = opt => {
|
|
|
378
431
|
}
|
|
379
432
|
radisk.parse(file, (err, disk) => {
|
|
380
433
|
if (err) opt.log(err)
|
|
381
|
-
|
|
434
|
+
// Never overwrite an existing cache entry from a plain read - it
|
|
435
|
+
// can't tell whether it raced ahead of a concurrent write (started
|
|
436
|
+
// before that write, but resolving after the write already
|
|
437
|
+
// installed its own authoritative, freshly-merged entry - see
|
|
438
|
+
// save.mix's own matching change). Only populate the cache when
|
|
439
|
+
// nothing beat this read to it.
|
|
440
|
+
if (disk && opt.cache && !cache.has(file)) {
|
|
382
441
|
cache.set(file, disk)
|
|
383
442
|
checkMemoryUsage()
|
|
384
443
|
}
|
package/src/radix.js
CHANGED
|
@@ -26,38 +26,68 @@ const Radix = () => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (!found) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
matchingKey
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
if (noValue) {
|
|
30
|
+
// If not found from the provided keys try matching with an existing
|
|
31
|
+
// key.
|
|
32
|
+
const result = utils.obj.map(tree, (hasValue, hasKey) => {
|
|
33
|
+
let j = 0
|
|
34
|
+
let matchingKey = ""
|
|
35
|
+
while (hasKey[j] === keys[j]) {
|
|
36
|
+
matchingKey += hasKey[j++]
|
|
37
|
+
}
|
|
38
|
+
if (matchingKey) {
|
|
39
|
+
// matchingKey has to be as long as the original keys when
|
|
40
|
+
// reading.
|
|
39
41
|
if (j <= max) return
|
|
40
42
|
|
|
41
43
|
tmp[hasKey.slice(j)] = hasValue
|
|
42
44
|
return hasValue
|
|
43
45
|
}
|
|
46
|
+
})
|
|
47
|
+
if (!result) return
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
return tmp
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Writing: find the EXISTING key with the longest shared prefix,
|
|
53
|
+
// not just the first one with any overlap at all (obj.map stops at
|
|
54
|
+
// the first non-undefined result, so a plain find here would settle
|
|
55
|
+
// for whichever candidate happens to come first in Object.keys'
|
|
56
|
+
// own iteration order). A radix tree's own invariant - no two
|
|
57
|
+
// sibling keys share a prefix - only holds if every insertion
|
|
58
|
+
// merges with its one true match; with many keys sharing this tree
|
|
59
|
+
// at once (many concurrent writers batched together), a
|
|
60
|
+
// coincidentally shorter, unrelated key can otherwise "win" over
|
|
61
|
+
// the real, much longer match - silently misplacing the new key
|
|
62
|
+
// under the wrong group, while its actual sibling is never touched
|
|
63
|
+
// at all and stays wherever it already was, invisible to any read
|
|
64
|
+
// that expects it inside this new group.
|
|
65
|
+
let bestKey = null
|
|
66
|
+
let bestMatch = ""
|
|
67
|
+
for (const hasKey of Object.keys(tree)) {
|
|
68
|
+
let j = 0
|
|
69
|
+
let matchingKey = ""
|
|
70
|
+
while (hasKey[j] === keys[j]) {
|
|
71
|
+
matchingKey += hasKey[j++]
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
if (matchingKey.length > bestMatch.length) {
|
|
74
|
+
bestKey = hasKey
|
|
75
|
+
bestMatch = matchingKey
|
|
76
|
+
}
|
|
77
|
+
}
|
|
56
78
|
|
|
79
|
+
if (bestKey) {
|
|
80
|
+
const hasValue = tree[bestKey]
|
|
81
|
+
const j = bestMatch.length
|
|
82
|
+
let replace = {
|
|
83
|
+
[bestKey.slice(j)]: hasValue,
|
|
84
|
+
[keys.slice(j)]: {[record]: value},
|
|
85
|
+
}
|
|
86
|
+
tree[bestMatch] = {[group]: replace}
|
|
87
|
+
delete tree[bestKey]
|
|
88
|
+
} else {
|
|
57
89
|
if (!tree[key]) tree[key] = {}
|
|
58
90
|
tree[key][record] = value
|
|
59
|
-
} else if (noValue) {
|
|
60
|
-
return tmp
|
|
61
91
|
}
|
|
62
92
|
} else if (i === max) {
|
|
63
93
|
// If no value use the key provided to return a whole group or record.
|
package/src/store.js
CHANGED
package/src/wire.js
CHANGED
|
@@ -319,6 +319,9 @@ const Wire = opt => {
|
|
|
319
319
|
// signature but not actually match the user under which the data is being
|
|
320
320
|
// stored. To avoid this, the current data on a soul needs to be checked to
|
|
321
321
|
// make sure the stored public key matches the one provided with the update.
|
|
322
|
+
// This must run before Ham.mix, which updates graph in place — checking
|
|
323
|
+
// afterwards would compare the incoming update's public key against itself
|
|
324
|
+
// instead of the previously stored one.
|
|
322
325
|
const check = async (data, send, cb) => {
|
|
323
326
|
const key = utils.userPublicKey
|
|
324
327
|
|
|
@@ -328,8 +331,12 @@ const Wire = opt => {
|
|
|
328
331
|
// allow it - the update is not trying to change the public key.
|
|
329
332
|
if (node[key] === undefined) continue
|
|
330
333
|
|
|
334
|
+
// A full-node lookup is required (rather than a targeted property
|
|
335
|
+
// lookup) so that a soul which exists but has never had a public key
|
|
336
|
+
// can be told apart from a soul that doesn't exist yet — both would
|
|
337
|
+
// otherwise resolve to the same "not found" shape for this one key.
|
|
331
338
|
const msg = await new Promise(res => {
|
|
332
|
-
getWithCallback({"#": soul
|
|
339
|
+
getWithCallback({"#": soul}, res, send, {put: true})
|
|
333
340
|
})
|
|
334
341
|
if (msg.err) {
|
|
335
342
|
if (cb) cb(msg.err)
|
|
@@ -340,7 +347,12 @@ const Wire = opt => {
|
|
|
340
347
|
// matching public keys, as the provided soul also needs a rel on the
|
|
341
348
|
// parent node which then also requires checking. Otherwise public keys
|
|
342
349
|
// need to match for existing data.
|
|
343
|
-
|
|
350
|
+
const existing = msg.put && msg.put[soul]
|
|
351
|
+
if (
|
|
352
|
+
!existing ||
|
|
353
|
+
existing[key] === undefined ||
|
|
354
|
+
existing[key] === node[key]
|
|
355
|
+
) {
|
|
344
356
|
continue
|
|
345
357
|
}
|
|
346
358
|
|
|
@@ -394,6 +406,8 @@ const Wire = opt => {
|
|
|
394
406
|
}
|
|
395
407
|
}
|
|
396
408
|
|
|
409
|
+
if (!(await check(msg.put, send))) return
|
|
410
|
+
|
|
397
411
|
// Store updates returned from Ham.mix and defer updates if required.
|
|
398
412
|
const update = await Ham.mix(
|
|
399
413
|
msg.put,
|
|
@@ -414,8 +428,6 @@ const Wire = opt => {
|
|
|
414
428
|
return
|
|
415
429
|
}
|
|
416
430
|
|
|
417
|
-
if (!(await check(update.now, send))) return
|
|
418
|
-
|
|
419
431
|
const finish = err => {
|
|
420
432
|
if (err) console.warn("store.put", err)
|
|
421
433
|
send(
|
|
@@ -636,6 +648,10 @@ const Wire = opt => {
|
|
|
636
648
|
}
|
|
637
649
|
}
|
|
638
650
|
}
|
|
651
|
+
if (!(await check(data, send, cb))) {
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
639
655
|
const update = await Ham.mix(
|
|
640
656
|
data,
|
|
641
657
|
graph,
|
|
@@ -650,8 +666,15 @@ const Wire = opt => {
|
|
|
650
666
|
return
|
|
651
667
|
}
|
|
652
668
|
|
|
653
|
-
|
|
654
|
-
|
|
669
|
+
// Ham.mix has already merged this locally-authored update into
|
|
670
|
+
// graph[soul], synchronously, before store.put below even starts
|
|
671
|
+
// persisting it - mark it as fully known now rather than waiting
|
|
672
|
+
// for a future disk fetch to do so, so a read landing in that
|
|
673
|
+
// window doesn't get forced through a disk round-trip that could
|
|
674
|
+
// itself race ahead of this same write and find nothing, even
|
|
675
|
+
// though the correct answer was already sitting in memory.
|
|
676
|
+
for (const soul of Object.keys(update.now)) {
|
|
677
|
+
hasNode.add(soul)
|
|
655
678
|
}
|
|
656
679
|
|
|
657
680
|
if (opt.userLimit) {
|