@bananapus/nana-sdk-core 2.35.0 → 2.36.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/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +42 -24
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/cjs/review/tabState.d.ts +23 -0
- package/dist/cjs/review/tabState.d.ts.map +1 -0
- package/dist/cjs/review/tabState.js +30 -0
- package/dist/cjs/review/tabState.js.map +1 -0
- package/dist/cjs/review/writeHold.d.ts +31 -5
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +260 -42
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +42 -3
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +168 -14
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/cjs/testing.d.ts +7 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +30 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +42 -24
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/tabState.d.ts +23 -0
- package/dist/esm/review/tabState.d.ts.map +1 -0
- package/dist/esm/review/tabState.js +26 -0
- package/dist/esm/review/tabState.js.map +1 -0
- package/dist/esm/review/writeHold.d.ts +31 -5
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +261 -43
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +42 -3
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +167 -16
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/dist/esm/testing.d.ts +7 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +25 -0
- package/dist/esm/testing.js.map +1 -0
- package/package.json +6 -1
|
@@ -8,6 +8,18 @@ const messages_js_1 = require("./messages.js");
|
|
|
8
8
|
const writeRecovery_js_1 = require("./writeRecovery.js");
|
|
9
9
|
const untrusted_js_1 = require("../untrusted.js");
|
|
10
10
|
const relayr_js_1 = require("./relayr.js");
|
|
11
|
+
const tabState_js_1 = require("./tabState.js");
|
|
12
|
+
/** `sent` is the transaction at `hash`, exactly `record`'s call, from its account, on its chain. */
|
|
13
|
+
function sentIsCall(sent, hash, record) {
|
|
14
|
+
return (sent.hash?.toLowerCase() === hash.toLowerCase() &&
|
|
15
|
+
sent.from?.toLowerCase() === record.account &&
|
|
16
|
+
(sent.chainId === undefined || sent.chainId === record.chainId) &&
|
|
17
|
+
sent.to?.toLowerCase() === record.call.to &&
|
|
18
|
+
sent.input?.toLowerCase() === record.call.data &&
|
|
19
|
+
BigInt(sent.value) === BigInt(record.call.value));
|
|
20
|
+
}
|
|
21
|
+
/** The stop of every controller with a follower or tab listener running, for the test-only reset. */
|
|
22
|
+
const liveHolds = (0, tabState_js_1.tabShared)(tabState_js_1.TAB.holds, () => new Set());
|
|
11
23
|
/**
|
|
12
24
|
* A state read without the chain (peek, a replay, another tab's change, a
|
|
13
25
|
* send's progress): its status and line, never the exit. Only the gate in
|
|
@@ -67,6 +79,8 @@ const SAFE_EXECUTION_TOPICS = [
|
|
|
67
79
|
];
|
|
68
80
|
/** A hash the network still hasn't seen this long after the wallet returned it was almost certainly never broadcast. */
|
|
69
81
|
const UNSEEN_HASH_MS = 30 * 60_000;
|
|
82
|
+
/** The longest one request to Safe's service may take inside the hold. */
|
|
83
|
+
const SAFE_SERVICE_READ_MS = 8_000;
|
|
70
84
|
/** The 30 minutes have passed since the wallet returned the hash, or since it was first read. A clock that couldn't be saved (NaN) never runs out. */
|
|
71
85
|
function unseenTooLong(record) {
|
|
72
86
|
return (Date.now() - Number(record.submittedAt ?? record.firstSeenAt) >=
|
|
@@ -183,6 +197,30 @@ function classify(record) {
|
|
|
183
197
|
*/
|
|
184
198
|
function createWriteHold(options = {}) {
|
|
185
199
|
const storage = options.storage ?? (0, writeRecovery_js_1.createHeldWriteStorage)();
|
|
200
|
+
/**
|
|
201
|
+
* Safe's service, every request and its body bounded together to
|
|
202
|
+
* {@link SAFE_SERVICE_READ_MS} whatever its fetch does with the signal: a read that times out is
|
|
203
|
+
* unknown, which never releases or settles, so a hung service can't hold
|
|
204
|
+
* a send or the call's lock.
|
|
205
|
+
*/
|
|
206
|
+
const service = {
|
|
207
|
+
...options.safeService,
|
|
208
|
+
fetch: (url, init) => {
|
|
209
|
+
const request = options.safeService?.fetch ?? globalThis.fetch;
|
|
210
|
+
let timer;
|
|
211
|
+
return Promise.race([
|
|
212
|
+
// The whole read, its body included, under the one deadline.
|
|
213
|
+
request(url, init).then(async (response) => new Response(await response.arrayBuffer(), {
|
|
214
|
+
status: response.status,
|
|
215
|
+
statusText: response.statusText,
|
|
216
|
+
headers: response.headers,
|
|
217
|
+
})),
|
|
218
|
+
new Promise((_, reject) => {
|
|
219
|
+
timer = setTimeout(() => reject(new Error("Safe's service did not answer in time.")), SAFE_SERVICE_READ_MS);
|
|
220
|
+
}),
|
|
221
|
+
]).finally(() => clearTimeout(timer));
|
|
222
|
+
},
|
|
223
|
+
};
|
|
186
224
|
const interval = options.settle?.intervalMs ?? SETTLE_POLL_MS;
|
|
187
225
|
const attempts = options.settle?.attempts ?? SETTLE_POLL_ATTEMPTS;
|
|
188
226
|
const listeners = new Map();
|
|
@@ -226,11 +264,25 @@ function createWriteHold(options = {}) {
|
|
|
226
264
|
}
|
|
227
265
|
};
|
|
228
266
|
globalThis.addEventListener("storage", onStorage);
|
|
267
|
+
liveHolds.add(dispose);
|
|
268
|
+
}
|
|
269
|
+
/** Stop every settle poll and the tab listener. */
|
|
270
|
+
function dispose() {
|
|
271
|
+
for (const timer of timers.values())
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
timers.clear();
|
|
274
|
+
if (onStorage)
|
|
275
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
276
|
+
onStorage = undefined;
|
|
277
|
+
liveHolds.delete(dispose);
|
|
229
278
|
}
|
|
230
|
-
function openOne(call, hooks = {}
|
|
279
|
+
function openOne(call, hooks = {},
|
|
280
|
+
/** The key a found write was saved under, when it isn't its call's own. */
|
|
281
|
+
at) {
|
|
231
282
|
const recovery = (0, writeRecovery_js_1.createBrowserWriteRecovery)(call, {
|
|
232
283
|
storage,
|
|
233
284
|
locks: options.locks,
|
|
285
|
+
at,
|
|
234
286
|
});
|
|
235
287
|
// The canonical recovery scope: a namespace and an exact call never share it.
|
|
236
288
|
const key = recovery.scopeKey;
|
|
@@ -287,6 +339,7 @@ function createWriteHold(options = {}) {
|
|
|
287
339
|
function follow(client, id, left = attempts) {
|
|
288
340
|
if (timers.has(id) || left <= 0)
|
|
289
341
|
return;
|
|
342
|
+
liveHolds.add(dispose);
|
|
290
343
|
timers.set(id, setTimeout(() => {
|
|
291
344
|
void state(client)
|
|
292
345
|
.catch(() => null)
|
|
@@ -328,10 +381,37 @@ function createWriteHold(options = {}) {
|
|
|
328
381
|
follow(client, record.id);
|
|
329
382
|
return classify(recovery.read());
|
|
330
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Whether a previous deploy's `hash` may be this call's: a valid hash
|
|
386
|
+
* read on the call's chain that isn't shown to be another write. A
|
|
387
|
+
* transaction must be the exact call from the account; a Safe proposal,
|
|
388
|
+
* exactly this call. One that can't be read is unknown, not refused.
|
|
389
|
+
*/
|
|
390
|
+
async function adoptable(hash, client) {
|
|
391
|
+
if (!(0, untrusted_js_1.isBytes32)(hash) ||
|
|
392
|
+
!client ||
|
|
393
|
+
(await client.getChainId().catch(() => null)) !== call.chainId)
|
|
394
|
+
return false;
|
|
395
|
+
const reviewed = {
|
|
396
|
+
account: call.account.toLowerCase(),
|
|
397
|
+
chainId: call.chainId,
|
|
398
|
+
call: {
|
|
399
|
+
to: call.call.to.toLowerCase(),
|
|
400
|
+
data: call.call.data.toLowerCase(),
|
|
401
|
+
value: String(call.call.value ?? 0),
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
if (call.safe) {
|
|
405
|
+
const proposal = await (0, safeService_js_1.readSafeTransaction)(call.chainId, call.account, hash, service).catch(() => null);
|
|
406
|
+
return !proposal || (0, writeRecovery_js_1.safeProposalIsHeldCall)(proposal, call);
|
|
407
|
+
}
|
|
408
|
+
const sent = await client.getTransaction({ hash }).catch(() => null);
|
|
409
|
+
return !sent || sentIsCall(sent, hash, reviewed);
|
|
410
|
+
}
|
|
331
411
|
/** The authenticated nonce of a Safe proposal, from Safe's service. */
|
|
332
412
|
async function proposalNonceOf(record) {
|
|
333
413
|
try {
|
|
334
|
-
const proposal = await (0, safeService_js_1.readSafeTransaction)(record.chainId, record.account, record.hash,
|
|
414
|
+
const proposal = await (0, safeService_js_1.readSafeTransaction)(record.chainId, record.account, record.hash, service);
|
|
335
415
|
return (0, safeService_js_1.safeTransactionMessage)(proposal).nonce;
|
|
336
416
|
}
|
|
337
417
|
catch {
|
|
@@ -536,13 +616,13 @@ function createWriteHold(options = {}) {
|
|
|
536
616
|
}
|
|
537
617
|
const end = (record.safeNonce !== undefined &&
|
|
538
618
|
(await (0, safeService_js_1.provenSafeProposalEnd)(client, record.account, hash, BigInt(record.safeNonce)))) ||
|
|
539
|
-
(await (0, safeService_js_1.lookAtSafeProposalEnd)(client, record.chainId, record.account, hash,
|
|
619
|
+
(await (0, safeService_js_1.lookAtSafeProposalEnd)(client, record.chainId, record.account, hash, service));
|
|
540
620
|
// Another Safe transaction took its nonce, even after a saved execution
|
|
541
621
|
// was reorganized away. It may have run the call too: the exit, never a release.
|
|
542
622
|
if (end.look === "replaced")
|
|
543
623
|
return safeReplaced(recovery.replacedOnChain(record));
|
|
544
624
|
if (end.look === "expired" && !record.executed) {
|
|
545
|
-
const expired = await (0, writeRecovery_js_1.verifyReviewedWriteExpiry)(client, record,
|
|
625
|
+
const expired = await (0, writeRecovery_js_1.verifyReviewedWriteExpiry)(client, record, service).catch(() => false);
|
|
546
626
|
if (expired && release(record))
|
|
547
627
|
return clear({ result: "expired" }, record.id);
|
|
548
628
|
}
|
|
@@ -673,9 +753,7 @@ function createWriteHold(options = {}) {
|
|
|
673
753
|
}
|
|
674
754
|
if (record.hash &&
|
|
675
755
|
record.sentNonce === undefined &&
|
|
676
|
-
(record
|
|
677
|
-
record.fromBlock === undefined ||
|
|
678
|
-
unseenTooLong(record)) &&
|
|
756
|
+
unseenTooLong(record) &&
|
|
679
757
|
(await client.getTransaction({ hash: record.hash }).then(() => false, (error) => error?.name === "TransactionNotFoundError")))
|
|
680
758
|
return unchecked(record);
|
|
681
759
|
return classify(record);
|
|
@@ -687,7 +765,7 @@ function createWriteHold(options = {}) {
|
|
|
687
765
|
async function state(client, checks = {}) {
|
|
688
766
|
lastEffect = checks.effect;
|
|
689
767
|
const result = await locked(() => inspect(client, checks.effect), false);
|
|
690
|
-
return shown(result ? result.value : peek());
|
|
768
|
+
return { ...shown(result ? result.value : peek()), checked: !!result };
|
|
691
769
|
}
|
|
692
770
|
return {
|
|
693
771
|
state,
|
|
@@ -873,7 +951,10 @@ function createWriteHold(options = {}) {
|
|
|
873
951
|
// what the saved record says, with no exit.
|
|
874
952
|
}, false);
|
|
875
953
|
const ended = result?.value ?? { released: false, state: peek() };
|
|
876
|
-
return {
|
|
954
|
+
return {
|
|
955
|
+
...ended,
|
|
956
|
+
state: { ...shown(ended.state), checked: !!result },
|
|
957
|
+
};
|
|
877
958
|
},
|
|
878
959
|
/**
|
|
879
960
|
* A receipt for the held write: an ordinary write's own hash, or a Safe
|
|
@@ -917,6 +998,94 @@ function createWriteHold(options = {}) {
|
|
|
917
998
|
* one lock, without releasing it. The hold then follows that hash: no
|
|
918
999
|
* exit while it is pending, and its receipt settles it.
|
|
919
1000
|
*/
|
|
1001
|
+
/**
|
|
1002
|
+
* Move a write a previous deploy kept (outside these holds) into this
|
|
1003
|
+
* hold. `snapshot` is the state the app read before deciding. Under
|
|
1004
|
+
* the call's lock, it reserves a held record with `evidence` (the
|
|
1005
|
+
* account's next nonce, or the nonce it was sent at, with the block it
|
|
1006
|
+
* was read at; when it was sent; and its `hash`, or for a Safe write
|
|
1007
|
+
* `safe.safeTxHash`) only while nothing is held and no reservation
|
|
1008
|
+
* came since `snapshot`: another tab's hold or settlement wins, and
|
|
1009
|
+
* the current state is returned unchanged. A hash is checked first
|
|
1010
|
+
* with `client`: a transaction that is another call, from another
|
|
1011
|
+
* account or on another chain (or a Safe proposal of another call) is
|
|
1012
|
+
* refused, adopting nothing. One the chain can't show is held, with
|
|
1013
|
+
* the warned exit 30 minutes after it was sent (or adopted). Then the
|
|
1014
|
+
* gate reads it: a mined one settles on its verified receipt. It never
|
|
1015
|
+
* sends or releases; adopting twice holds once.
|
|
1016
|
+
*/
|
|
1017
|
+
async adopt(snapshot, evidence = {}, client) {
|
|
1018
|
+
const nonce = evidence.nonce ?? evidence.sentNonce;
|
|
1019
|
+
const hash = (call.safe ? evidence.safe?.safeTxHash : evidence.hash)?.toLowerCase();
|
|
1020
|
+
/** Nothing held, and no reservation since the app's read. */
|
|
1021
|
+
const free = () => snapshot.status === "clear" &&
|
|
1022
|
+
!recovery.read() &&
|
|
1023
|
+
recovery.latestReservation() === snapshot.reservation;
|
|
1024
|
+
const reserve = () => recovery.reserve(undefined, nonce !== undefined && evidence.fromBlock !== undefined
|
|
1025
|
+
? { nonce, blockNumber: evidence.fromBlock }
|
|
1026
|
+
: undefined, {
|
|
1027
|
+
hash,
|
|
1028
|
+
submittedAt: evidence.submittedAt === undefined
|
|
1029
|
+
? undefined
|
|
1030
|
+
: String(evidence.submittedAt),
|
|
1031
|
+
});
|
|
1032
|
+
if (hooks.across) {
|
|
1033
|
+
// Under an intent: the same atomic reserve as a send, under every
|
|
1034
|
+
// legacy key's lock, mirrored at each so older versions see it.
|
|
1035
|
+
if (!free() ||
|
|
1036
|
+
(hash !== undefined && !(await adoptable(hash, client))))
|
|
1037
|
+
return shown(peek());
|
|
1038
|
+
let adopted = false;
|
|
1039
|
+
try {
|
|
1040
|
+
const { released } = await hooks.across((work) => recovery.withLock(work), () => {
|
|
1041
|
+
if (!free())
|
|
1042
|
+
throw new Error("Held since the app's read.");
|
|
1043
|
+
reserve();
|
|
1044
|
+
adopted = true;
|
|
1045
|
+
}, recovery.rejected, Promise.resolve(), hooks.generations());
|
|
1046
|
+
await released;
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
/* Held elsewhere, busy, or no longer free: nothing adopted. */
|
|
1050
|
+
}
|
|
1051
|
+
// A mined hash settles on its verified receipt, through the gate.
|
|
1052
|
+
return adopted && hash && client ? state(client) : shown(peek());
|
|
1053
|
+
}
|
|
1054
|
+
const result = await locked(async () => {
|
|
1055
|
+
if (!free())
|
|
1056
|
+
return peek();
|
|
1057
|
+
if (hash !== undefined && !(await adoptable(hash, client)))
|
|
1058
|
+
return peek();
|
|
1059
|
+
reserve();
|
|
1060
|
+
return hash && client ? inspect(client) : peek();
|
|
1061
|
+
}, true);
|
|
1062
|
+
return shown(result?.value ?? peek());
|
|
1063
|
+
},
|
|
1064
|
+
/**
|
|
1065
|
+
* Save the Safe nonce of a held Safe proposal adopted before it was
|
|
1066
|
+
* known, so a replacement can be proven later. Under the call's lock,
|
|
1067
|
+
* only while the held proposal is `safeTxHash` with no nonce yet, and
|
|
1068
|
+
* only when the call's proposal at `nonce` hashes to it: the hash is
|
|
1069
|
+
* the proof. Then the gate reads it. Never sends or releases.
|
|
1070
|
+
*/
|
|
1071
|
+
async attachSafeNonce(client, { safeTxHash, nonce }) {
|
|
1072
|
+
const result = await locked(async () => {
|
|
1073
|
+
const record = recovery.read();
|
|
1074
|
+
if (record?.safe &&
|
|
1075
|
+
!record.mirrorOf &&
|
|
1076
|
+
record.safeNonce === undefined &&
|
|
1077
|
+
record.hash === safeTxHash.toLowerCase())
|
|
1078
|
+
try {
|
|
1079
|
+
(0, safeService_js_1.canonicalSafeTxHash)(call.chainId, call.account, (0, writeRecovery_js_1.heldSafeProposal)(call, nonce), safeTxHash);
|
|
1080
|
+
recovery.proposalNonce(record, BigInt(nonce));
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
/* Not this proposal at that nonce: nothing saved. */
|
|
1084
|
+
}
|
|
1085
|
+
return inspect(client);
|
|
1086
|
+
}, true);
|
|
1087
|
+
return shown(result?.value ?? peek());
|
|
1088
|
+
},
|
|
920
1089
|
async attachHash(client, hash) {
|
|
921
1090
|
const result = await locked(async () => {
|
|
922
1091
|
const record = recovery.read();
|
|
@@ -929,12 +1098,8 @@ function createWriteHold(options = {}) {
|
|
|
929
1098
|
const sent = await client
|
|
930
1099
|
.getTransaction({ hash })
|
|
931
1100
|
.catch(() => null);
|
|
932
|
-
if (sent
|
|
933
|
-
sent
|
|
934
|
-
(sent.chainId === undefined || sent.chainId === record.chainId) &&
|
|
935
|
-
sent.to?.toLowerCase() === record.call.to &&
|
|
936
|
-
sent.input?.toLowerCase() === record.call.data &&
|
|
937
|
-
BigInt(sent.value) === BigInt(record.call.value) &&
|
|
1101
|
+
if (sent &&
|
|
1102
|
+
sentIsCall(sent, hash, record) &&
|
|
938
1103
|
// This attempt, not an earlier identical call: never below the
|
|
939
1104
|
// account's next nonce or mined before it was reserved.
|
|
940
1105
|
// With no such evidence, an identical earlier call can't be
|
|
@@ -990,7 +1155,13 @@ function createWriteHold(options = {}) {
|
|
|
990
1155
|
/**
|
|
991
1156
|
* `legacy`: for a write held under an intent namespace, the keys an earlier
|
|
992
1157
|
* version held it under (an old namespace, or `{ call }` for an exact
|
|
993
|
-
* call); the current exact call is always checked.
|
|
1158
|
+
* call); the current exact call is always checked. `{ match: { to,
|
|
1159
|
+
* selector } }` finds them instead: every exact-call write this account
|
|
1160
|
+
* saved on this chain to `to` with that 4-byte selector, for a hold saved
|
|
1161
|
+
* before the intent key existed under calldata the retry can't rebuild (an
|
|
1162
|
+
* old minimum, expiration or deadline). Pass one only for a write whose
|
|
1163
|
+
* calldata is volatile: it holds every such call. It is found when the
|
|
1164
|
+
* handle opens, and again right before a send reserves. While any of them holds
|
|
994
1165
|
* an unresolved write, the intent handle *is* that hold: its state, its
|
|
995
1166
|
* refusal and its own exit, in place. Each is checked alone, under its own
|
|
996
1167
|
* lock only, never two at once. Only when all are clear does the intent's
|
|
@@ -1000,6 +1171,9 @@ function createWriteHold(options = {}) {
|
|
|
1000
1171
|
function open(call, { legacy = [], } = {}) {
|
|
1001
1172
|
if (call.namespace === undefined)
|
|
1002
1173
|
return openExact(call);
|
|
1174
|
+
const matches = legacy.flatMap((source) => typeof source === "object" && "match" in source ? [source.match] : []);
|
|
1175
|
+
/** Every exact-call write the matches find in storage now. */
|
|
1176
|
+
const matched = () => matches.flatMap((match) => (0, writeRecovery_js_1.findReviewedWrites)({ chainId: call.chainId, account: call.account, ...match }, { storage }));
|
|
1003
1177
|
const namespace = call.namespace;
|
|
1004
1178
|
const own = openOne(call, {
|
|
1005
1179
|
across: reserveAcross,
|
|
@@ -1011,19 +1185,54 @@ function createWriteHold(options = {}) {
|
|
|
1011
1185
|
const { namespace: _, ...exact } = call;
|
|
1012
1186
|
const scopeOf = (input) => (0, writeRecovery_js_1.createBrowserWriteRecovery)(input, { storage }).scopeKey;
|
|
1013
1187
|
const scopes = new Set([scopeOf(call)]);
|
|
1014
|
-
const inputs = [{ call: call.call }, ...legacy]
|
|
1015
|
-
.map((source) => typeof source === "string"
|
|
1016
|
-
? { ...call, namespace: source }
|
|
1017
|
-
: { ...exact, call: source.call })
|
|
1018
|
-
.filter((input) => {
|
|
1019
|
-
const scope = scopeOf(input);
|
|
1020
|
-
return !scopes.has(scope) && !!scopes.add(scope);
|
|
1021
|
-
})
|
|
1022
|
-
// One canonical order, by scope, for every writer's locks.
|
|
1023
|
-
.sort((a, b) => Number(scopeOf(a) > scopeOf(b)) - Number(scopeOf(a) < scopeOf(b)));
|
|
1024
1188
|
// Mirror-aware: evidence and exits always act on the source attempt.
|
|
1025
|
-
const delegates =
|
|
1026
|
-
const journals =
|
|
1189
|
+
const delegates = [];
|
|
1190
|
+
const journals = [];
|
|
1191
|
+
/** Hold `inputs` too, each once, in one canonical order by scope for every writer's locks. */
|
|
1192
|
+
function include(inputs) {
|
|
1193
|
+
const added = inputs.filter(({ input, at }) => {
|
|
1194
|
+
const scope = at ?? scopeOf(input);
|
|
1195
|
+
return !scopes.has(scope) && !!scopes.add(scope);
|
|
1196
|
+
});
|
|
1197
|
+
if (!added.length)
|
|
1198
|
+
return;
|
|
1199
|
+
const keyed = [
|
|
1200
|
+
...delegates.map((delegate, index) => ({
|
|
1201
|
+
delegate,
|
|
1202
|
+
journal: journals[index],
|
|
1203
|
+
})),
|
|
1204
|
+
...added.map(({ input, at }) => ({
|
|
1205
|
+
delegate: openExact(input, at),
|
|
1206
|
+
journal: (0, writeRecovery_js_1.createBrowserWriteRecovery)(input, {
|
|
1207
|
+
storage,
|
|
1208
|
+
locks: options.locks,
|
|
1209
|
+
at,
|
|
1210
|
+
}),
|
|
1211
|
+
})),
|
|
1212
|
+
].sort((a, b) => Number(a.journal.scopeKey > b.journal.scopeKey) -
|
|
1213
|
+
Number(a.journal.scopeKey < b.journal.scopeKey));
|
|
1214
|
+
delegates.splice(0, delegates.length, ...keyed.map((k) => k.delegate));
|
|
1215
|
+
journals.splice(0, journals.length, ...keyed.map((k) => k.journal));
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Look for matching writes again (on every state(), send() and exit());
|
|
1219
|
+
* those found since are held too, each at the exact key it was found
|
|
1220
|
+
* under, with its own Safe flag. Returns the ones that are new.
|
|
1221
|
+
*/
|
|
1222
|
+
function rescan() {
|
|
1223
|
+
const found = matched().filter(({ key }) => !scopes.has(key));
|
|
1224
|
+
include(found.map(({ key, record }) => ({
|
|
1225
|
+
input: { ...exact, safe: record.safe, call: record.call },
|
|
1226
|
+
at: key,
|
|
1227
|
+
})));
|
|
1228
|
+
return found.map(({ record }) => record);
|
|
1229
|
+
}
|
|
1230
|
+
include([{ call: call.call }, ...legacy].flatMap((source) => typeof source === "string"
|
|
1231
|
+
? [{ input: { ...call, namespace: source } }]
|
|
1232
|
+
: "call" in source
|
|
1233
|
+
? [{ input: { ...exact, call: source.call } }]
|
|
1234
|
+
: []));
|
|
1235
|
+
rescan();
|
|
1027
1236
|
/** A legacy key holding anything but this intent's own mirror. */
|
|
1028
1237
|
const foreign = (index) => journals[index].read()?.mirrorOf !== namespace &&
|
|
1029
1238
|
delegates[index].peek().status !== "clear";
|
|
@@ -1031,6 +1240,7 @@ function createWriteHold(options = {}) {
|
|
|
1031
1240
|
const active = () => delegates.find((_, index) => foreign(index));
|
|
1032
1241
|
/** The first legacy key holding an unresolved write, through its own gate. */
|
|
1033
1242
|
async function delegated(client) {
|
|
1243
|
+
rescan();
|
|
1034
1244
|
for (const [index, handle] of delegates.entries()) {
|
|
1035
1245
|
if (!foreign(index))
|
|
1036
1246
|
continue;
|
|
@@ -1066,14 +1276,20 @@ function createWriteHold(options = {}) {
|
|
|
1066
1276
|
entered += 1;
|
|
1067
1277
|
return acquire(index + 1);
|
|
1068
1278
|
});
|
|
1279
|
+
const refuse = (held) => new WriteHeldError(noticed({
|
|
1280
|
+
...ungated(classify(held)),
|
|
1281
|
+
reservation: held.id,
|
|
1282
|
+
}));
|
|
1069
1283
|
for (const journal of journals) {
|
|
1070
1284
|
const held = journal.read();
|
|
1071
1285
|
if (held && held.mirrorOf !== namespace)
|
|
1072
|
-
throw
|
|
1073
|
-
...ungated(classify(held)),
|
|
1074
|
-
reservation: held.id,
|
|
1075
|
-
}));
|
|
1286
|
+
throw refuse(held);
|
|
1076
1287
|
}
|
|
1288
|
+
// A matching write saved since the last look holds it too, and
|
|
1289
|
+
// its own state() and exit() are reached from now on.
|
|
1290
|
+
const [late] = rescan();
|
|
1291
|
+
if (late)
|
|
1292
|
+
throw refuse(late);
|
|
1077
1293
|
// Another attempt since this review opened: review it again.
|
|
1078
1294
|
if (generations() !== reviewed)
|
|
1079
1295
|
throw new WriteHoldStaleReviewError(ownJournal.settled());
|
|
@@ -1161,6 +1377,7 @@ function createWriteHold(options = {}) {
|
|
|
1161
1377
|
return own.send(client, run);
|
|
1162
1378
|
},
|
|
1163
1379
|
async exit(snapshot, acknowledgement, client) {
|
|
1380
|
+
rescan();
|
|
1164
1381
|
const owner = snapshot.status === "clear"
|
|
1165
1382
|
? undefined
|
|
1166
1383
|
: delegates.find((handle, index) => {
|
|
@@ -1182,6 +1399,10 @@ function createWriteHold(options = {}) {
|
|
|
1182
1399
|
onUnproven: async (...args) => active()?.onUnproven(...args) ?? after(await own.onUnproven(...args)),
|
|
1183
1400
|
onExecuted: async (...args) => active()?.onExecuted(...args) ?? after(await own.onExecuted(...args)),
|
|
1184
1401
|
attachHash: async (...args) => active()?.attachHash(...args) ?? after(await own.attachHash(...args)),
|
|
1402
|
+
attachSafeNonce: async (...args) => active()?.attachSafeNonce(...args) ??
|
|
1403
|
+
after(await own.attachSafeNonce(...args)),
|
|
1404
|
+
// A legacy hold already holds it: nothing is adopted.
|
|
1405
|
+
adopt: async (...args) => active()?.peek() ?? after(await own.adopt(...args)),
|
|
1185
1406
|
};
|
|
1186
1407
|
}
|
|
1187
1408
|
/**
|
|
@@ -1189,11 +1410,12 @@ function createWriteHold(options = {}) {
|
|
|
1189
1410
|
* it is that intent's hold: it never sends or releases on its own, and its
|
|
1190
1411
|
* exit is the intent's. A mirror left after the intent ended is removed.
|
|
1191
1412
|
*/
|
|
1192
|
-
function openExact(call) {
|
|
1193
|
-
const own = openOne(call);
|
|
1413
|
+
function openExact(call, at) {
|
|
1414
|
+
const own = openOne(call, {}, at);
|
|
1194
1415
|
const journal = (0, writeRecovery_js_1.createBrowserWriteRecovery)(call, {
|
|
1195
1416
|
storage,
|
|
1196
1417
|
locks: options.locks,
|
|
1418
|
+
at,
|
|
1197
1419
|
});
|
|
1198
1420
|
/** The intent this key mirrors, from storage alone. */
|
|
1199
1421
|
const mirrored = () => {
|
|
@@ -1257,6 +1479,9 @@ function createWriteHold(options = {}) {
|
|
|
1257
1479
|
onUnproven: (...args) => (route() ?? own).onUnproven(...args),
|
|
1258
1480
|
onExecuted: (...args) => (route() ?? own).onExecuted(...args),
|
|
1259
1481
|
attachHash: (...args) => (route() ?? own).attachHash(...args),
|
|
1482
|
+
attachSafeNonce: (...args) => (route() ?? own).attachSafeNonce(...args),
|
|
1483
|
+
// An intent's mirror already holds it: nothing is adopted.
|
|
1484
|
+
adopt: async (...args) => route()?.peek() ?? own.adopt(...args),
|
|
1260
1485
|
};
|
|
1261
1486
|
}
|
|
1262
1487
|
return {
|
|
@@ -1266,14 +1491,7 @@ function createWriteHold(options = {}) {
|
|
|
1266
1491
|
/** The review's line while holds last only in this tab, or null. */
|
|
1267
1492
|
reviewLine: () => (storage.durable() ? null : messages_js_1.WRITE_HOLD_IN_TAB_LINE),
|
|
1268
1493
|
/** Stop every settle poll and the tab listener (tests, teardown). */
|
|
1269
|
-
dispose
|
|
1270
|
-
for (const timer of timers.values())
|
|
1271
|
-
clearTimeout(timer);
|
|
1272
|
-
timers.clear();
|
|
1273
|
-
if (onStorage)
|
|
1274
|
-
globalThis.removeEventListener?.("storage", onStorage);
|
|
1275
|
-
onStorage = undefined;
|
|
1276
|
-
},
|
|
1494
|
+
dispose,
|
|
1277
1495
|
};
|
|
1278
1496
|
}
|
|
1279
1497
|
//# sourceMappingURL=writeHold.js.map
|