@metalntwx/metal-pay 0.2.0 → 0.3.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/README.md +104 -38
- package/dist/clearing-abi.d.ts +1836 -0
- package/dist/clearing-abi.js +2361 -0
- package/dist/clearing-client.d.ts +96 -159
- package/dist/clearing-client.js +412 -553
- package/dist/clearing-codec.d.ts +57 -0
- package/dist/clearing-codec.js +173 -0
- package/dist/clearing-custody.d.ts +202 -0
- package/dist/clearing-custody.js +552 -0
- package/dist/clearing-server.d.ts +30 -49
- package/dist/clearing-server.js +87 -67
- package/dist/clearing-x402.d.ts +38 -0
- package/dist/clearing-x402.js +249 -0
- package/dist/client.d.ts +14 -15
- package/dist/client.js +53 -22
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/mandates/account.d.ts +9 -4
- package/dist/mandates/account.js +15 -11
- package/dist/mandates/binding.d.ts +4 -27
- package/dist/mandates/binding.js +5 -9
- package/dist/mandates/checkout.js +5 -5
- package/dist/mandates/client.js +4 -3
- package/dist/mandates/l3a.d.ts +2 -2
- package/dist/mandates/l3a.js +5 -4
- package/dist/mandates/seller.d.ts +1 -1
- package/dist/mandates/seller.js +4 -4
- package/dist/mandates/verify.d.ts +4 -6
- package/dist/mandates/verify.js +147 -82
- package/dist/mandates/vi/credential.js +4 -4
- package/dist/networks.d.ts +9 -1
- package/dist/networks.js +105 -4
- package/dist/node.d.ts +11 -0
- package/dist/node.js +97 -0
- package/package.json +18 -11
- package/src/clearing-abi.ts +2361 -0
- package/src/clearing-client.test.ts +850 -0
- package/src/clearing-client.ts +612 -0
- package/src/clearing-codec.test.ts +57 -0
- package/src/clearing-codec.ts +251 -0
- package/src/clearing-custody.test.ts +763 -0
- package/src/clearing-custody.ts +743 -0
- package/src/clearing-fixture.json +37 -0
- package/src/clearing-server.test.ts +450 -0
- package/src/clearing-server.ts +123 -0
- package/src/clearing-x402.test.ts +181 -0
- package/src/clearing-x402.ts +328 -0
- package/src/client.test.ts +333 -0
- package/src/client.ts +195 -0
- package/src/index.ts +33 -0
- package/src/mandates/account.ts +95 -0
- package/src/mandates/binding.ts +280 -0
- package/src/mandates/chain.ts +68 -0
- package/src/mandates/checkout.ts +368 -0
- package/src/mandates/client.ts +159 -0
- package/src/mandates/extension.ts +48 -0
- package/src/mandates/index.ts +121 -0
- package/src/mandates/ingest.ts +42 -0
- package/src/mandates/jcs.ts +16 -0
- package/src/mandates/l3a.ts +216 -0
- package/src/mandates/seller.ts +91 -0
- package/src/mandates/verify.ts +625 -0
- package/src/mandates/vi/credential.ts +477 -0
- package/src/mandates/vi/issue.ts +118 -0
- package/src/mandates/vi/sd-jwt.ts +189 -0
- package/src/networks.test.ts +177 -0
- package/src/networks.ts +242 -0
- package/src/node.test.ts +24 -0
- package/src/node.ts +96 -0
- package/src/server.test.ts +223 -0
- package/src/server.ts +77 -0
- package/dist/clearing.d.ts +0 -204
- package/dist/clearing.js +0 -151
- package/dist/file-storage.d.ts +0 -39
- package/dist/file-storage.js +0 -195
package/dist/clearing.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// Metal clearing's signed messages: the EIP-712 types and digests that a
|
|
2
|
-
// payer, a seller, the facilitator and the clearing operator must agree on
|
|
3
|
-
// field for field. They mirror contracts/src/clearing/ClearingTypes.sol and
|
|
4
|
-
// the operator's local API (apps/clearing-operator).
|
|
5
|
-
//
|
|
6
|
-
// - A Debit is a payer's signed running total, naming who it pays.
|
|
7
|
-
// - A Receipt is the operator's signature crediting a recipient's shard.
|
|
8
|
-
// - An Exit is an account's signed withdrawal, naming where funds go.
|
|
9
|
-
// - An AccountRequest is an account asking the facilitator about itself.
|
|
10
|
-
//
|
|
11
|
-
// All are signed under the Clearing contract's domain ("MetalClearing", "1",
|
|
12
|
-
// chainId, contract). Pure: no I/O.
|
|
13
|
-
import { hashTypedData, recoverTypedDataAddress, } from "viem";
|
|
14
|
-
/** The x402 scheme name of Metal's clearing lane. */
|
|
15
|
-
export const CLEARING_DEBIT = "clearing-debit";
|
|
16
|
-
export const CLEARING_DOMAIN_NAME = "MetalClearing";
|
|
17
|
-
export const DEBIT_TYPES = {
|
|
18
|
-
Debit: [
|
|
19
|
-
{ name: "payer", type: "address" },
|
|
20
|
-
{ name: "recipient", type: "address" },
|
|
21
|
-
{ name: "epoch", type: "uint64" },
|
|
22
|
-
{ name: "index", type: "uint64" },
|
|
23
|
-
{ name: "cumulativeDebit", type: "uint256" },
|
|
24
|
-
{ name: "ref", type: "bytes32" },
|
|
25
|
-
],
|
|
26
|
-
};
|
|
27
|
-
export const RECEIPT_TYPES = {
|
|
28
|
-
Receipt: [
|
|
29
|
-
{ name: "recipient", type: "address" },
|
|
30
|
-
{ name: "epoch", type: "uint64" },
|
|
31
|
-
{ name: "shard", type: "uint64" },
|
|
32
|
-
{ name: "index", type: "uint64" },
|
|
33
|
-
{ name: "cumulativeCredit", type: "uint256" },
|
|
34
|
-
{ name: "prevReceiptDigest", type: "bytes32" },
|
|
35
|
-
{ name: "debitDigest", type: "bytes32" },
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
export function clearingDomain(chainId, contract) {
|
|
39
|
-
return {
|
|
40
|
-
name: CLEARING_DOMAIN_NAME,
|
|
41
|
-
version: "1",
|
|
42
|
-
chainId,
|
|
43
|
-
verifyingContract: contract,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export function debitFromWire(wire) {
|
|
47
|
-
return {
|
|
48
|
-
payer: wire.payer,
|
|
49
|
-
recipient: wire.recipient,
|
|
50
|
-
epoch: BigInt(wire.epoch),
|
|
51
|
-
index: BigInt(wire.index),
|
|
52
|
-
cumulativeDebit: BigInt(wire.cumulativeDebit),
|
|
53
|
-
ref: wire.ref,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export function debitTypedData(domain, debit) {
|
|
57
|
-
return {
|
|
58
|
-
domain,
|
|
59
|
-
types: DEBIT_TYPES,
|
|
60
|
-
primaryType: "Debit",
|
|
61
|
-
message: debit,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export function receiptTypedData(domain, receipt) {
|
|
65
|
-
return {
|
|
66
|
-
domain,
|
|
67
|
-
types: RECEIPT_TYPES,
|
|
68
|
-
primaryType: "Receipt",
|
|
69
|
-
message: receipt,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
export function debitDigest(domain, debit) {
|
|
73
|
-
return hashTypedData(debitTypedData(domain, debit));
|
|
74
|
-
}
|
|
75
|
-
export function receiptDigest(domain, receipt) {
|
|
76
|
-
return hashTypedData(receiptTypedData(domain, receipt));
|
|
77
|
-
}
|
|
78
|
-
/** An account's request to take funds out of clearing custody. Only the
|
|
79
|
-
* account can sign it, and it names where the funds go. */
|
|
80
|
-
export const EXIT_TYPES = {
|
|
81
|
-
Exit: [
|
|
82
|
-
{ name: "account", type: "address" },
|
|
83
|
-
{ name: "destination", type: "address" },
|
|
84
|
-
{ name: "amount", type: "uint256" },
|
|
85
|
-
{ name: "deadline", type: "uint64" },
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
export function exitTypedData(domain, exit) {
|
|
89
|
-
return {
|
|
90
|
-
domain,
|
|
91
|
-
types: EXIT_TYPES,
|
|
92
|
-
primaryType: "Exit",
|
|
93
|
-
message: exit,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export function exitDigest(domain, exit) {
|
|
97
|
-
return hashTypedData(exitTypedData(domain, exit));
|
|
98
|
-
}
|
|
99
|
-
export async function recoverExitSigner(domain, exit, signature) {
|
|
100
|
-
try {
|
|
101
|
-
return await recoverTypedDataAddress({
|
|
102
|
-
...exitTypedData(domain, exit),
|
|
103
|
-
signature,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/** An account asking the facilitator about itself: where its next Debit
|
|
111
|
-
* starts, or what it can withdraw. Signed under the clearing domain so that
|
|
112
|
-
* only the account can read its own position; the deadline keeps a request
|
|
113
|
-
* someone else has seen from being used for long. Never sent on chain. */
|
|
114
|
-
export const ACCOUNT_REQUEST_TYPES = {
|
|
115
|
-
AccountRequest: [
|
|
116
|
-
{ name: "account", type: "address" },
|
|
117
|
-
{ name: "deadline", type: "uint64" },
|
|
118
|
-
],
|
|
119
|
-
};
|
|
120
|
-
export async function recoverAccountRequestSigner(domain, request, signature) {
|
|
121
|
-
try {
|
|
122
|
-
return await recoverTypedDataAddress({
|
|
123
|
-
domain,
|
|
124
|
-
types: ACCOUNT_REQUEST_TYPES,
|
|
125
|
-
primaryType: "AccountRequest",
|
|
126
|
-
message: request,
|
|
127
|
-
signature,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
export async function recoverDebitSigner(domain, debit, signature) {
|
|
135
|
-
try {
|
|
136
|
-
return await recoverTypedDataAddress({
|
|
137
|
-
...debitTypedData(domain, debit),
|
|
138
|
-
signature,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/** Receive shards spread a hot recipient over parallel credit lanes. The
|
|
146
|
-
* shard is a pure function of `ref`, so a replayed debit lands on the same
|
|
147
|
-
* one. Same formula as the café's. */
|
|
148
|
-
export const REVENUE_SHARDS = 8n;
|
|
149
|
-
export function shardFor(ref) {
|
|
150
|
-
return BigInt(`0x${ref.slice(-2)}`) % REVENUE_SHARDS;
|
|
151
|
-
}
|
package/dist/file-storage.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type ClearingPositionStorage, type StoredClearingPosition } from "./clearing-client.js";
|
|
2
|
-
export interface FileClearingPositionStorageOptions {
|
|
3
|
-
/** How long a writer waits for the lock before giving up. Default 5s. */
|
|
4
|
-
lockWaitMs?: number;
|
|
5
|
-
/** A lock this old was left by a writer that died or hung. Default 30s;
|
|
6
|
-
* a write holds the lock for milliseconds. */
|
|
7
|
-
lockStaleMs?: number;
|
|
8
|
-
}
|
|
9
|
-
export declare class FileClearingPositionStorage implements ClearingPositionStorage {
|
|
10
|
-
private readonly path;
|
|
11
|
-
private readonly lockPath;
|
|
12
|
-
private readonly takeoverPath;
|
|
13
|
-
private readonly lockWaitMs;
|
|
14
|
-
private readonly lockStaleMs;
|
|
15
|
-
constructor(path: string, options?: FileClearingPositionStorageOptions);
|
|
16
|
-
private read;
|
|
17
|
-
/** Hold the lock file while `job` runs. The whole file is read, changed
|
|
18
|
-
* and rewritten, so two writers (two instances, or two processes) must
|
|
19
|
-
* not overlap or one would drop the other's record.
|
|
20
|
-
*
|
|
21
|
-
* The lock is a file created exclusively, holding a token unique to its
|
|
22
|
-
* holder. A lock left by a writer that died is taken over, and that is
|
|
23
|
-
* the delicate part: two waiters must not both conclude it is abandoned
|
|
24
|
-
* and both remove it, or the second would delete the first one's fresh
|
|
25
|
-
* lock. So removal itself is guarded (see `takeOver`), and a writer whose
|
|
26
|
-
* lock was taken over finds out before it writes (`stillHeld`). */
|
|
27
|
-
private locked;
|
|
28
|
-
private inspect;
|
|
29
|
-
/** Remove `path` only while it still holds `token`. */
|
|
30
|
-
private removeIfHolds;
|
|
31
|
-
/** Remove an abandoned lock, at most one waiter at a time. The takeover
|
|
32
|
-
* file is itself created exclusively, so only one waiter is ever inside;
|
|
33
|
-
* it removes the lock only if it is still the abandoned one it saw. A
|
|
34
|
-
* waiter that arrives later finds either no lock or a fresh one from a
|
|
35
|
-
* new holder, and leaves it alone. */
|
|
36
|
-
private takeOver;
|
|
37
|
-
get(key: string): Promise<StoredClearingPosition | undefined>;
|
|
38
|
-
set(key: string, position: StoredClearingPosition): Promise<void>;
|
|
39
|
-
}
|
package/dist/file-storage.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
// Durable storage for the clearing client's position: one JSON file. Give
|
|
2
|
-
// every process that signs with the same key the same file, so each sees
|
|
3
|
-
// what the others signed.
|
|
4
|
-
//
|
|
5
|
-
// Kept apart from `clearing-client.ts` so the plug-in itself has no
|
|
6
|
-
// filesystem dependency, the way the official batch-settlement client ships
|
|
7
|
-
// its file storage as a separate entry point.
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
-
import { dirname } from "node:path";
|
|
11
|
-
import { keepHighest, } from "./clearing-client.js";
|
|
12
|
-
/** Thrown inside a write whose lock was taken over while it was working. */
|
|
13
|
-
class LostLock extends Error {
|
|
14
|
-
}
|
|
15
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
-
/** Whether the process that wrote a lock is gone. Only meaningful for a
|
|
17
|
-
* holder on this machine; an unparsable pid is not treated as dead. */
|
|
18
|
-
function holderIsDead(token) {
|
|
19
|
-
const pid = Number(token.split(":")[0]);
|
|
20
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
21
|
-
return false;
|
|
22
|
-
try {
|
|
23
|
-
process.kill(pid, 0);
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
return error.code === "ESRCH";
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export class FileClearingPositionStorage {
|
|
31
|
-
path;
|
|
32
|
-
lockPath;
|
|
33
|
-
takeoverPath;
|
|
34
|
-
lockWaitMs;
|
|
35
|
-
lockStaleMs;
|
|
36
|
-
constructor(path, options = {}) {
|
|
37
|
-
this.path = path;
|
|
38
|
-
this.lockPath = `${path}.lock`;
|
|
39
|
-
this.takeoverPath = `${path}.lock.takeover`;
|
|
40
|
-
this.lockWaitMs = options.lockWaitMs ?? 5_000;
|
|
41
|
-
this.lockStaleMs = options.lockStaleMs ?? 30_000;
|
|
42
|
-
}
|
|
43
|
-
async read() {
|
|
44
|
-
let text;
|
|
45
|
-
try {
|
|
46
|
-
text = await readFile(this.path, "utf8");
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (error.code === "ENOENT")
|
|
50
|
-
return {};
|
|
51
|
-
throw new Error(`clearing position file ${this.path} is unreadable`, {
|
|
52
|
-
cause: error,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// A file that exists but does not parse is not "no record": treating it
|
|
56
|
-
// as empty would drop the protection the record gives.
|
|
57
|
-
try {
|
|
58
|
-
const parsed = JSON.parse(text);
|
|
59
|
-
if (typeof parsed !== "object" ||
|
|
60
|
-
parsed === null ||
|
|
61
|
-
Array.isArray(parsed))
|
|
62
|
-
throw new Error("not an object");
|
|
63
|
-
return parsed;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
throw new Error(`clearing position file ${this.path} is unreadable`, {
|
|
67
|
-
cause: error,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/** Hold the lock file while `job` runs. The whole file is read, changed
|
|
72
|
-
* and rewritten, so two writers (two instances, or two processes) must
|
|
73
|
-
* not overlap or one would drop the other's record.
|
|
74
|
-
*
|
|
75
|
-
* The lock is a file created exclusively, holding a token unique to its
|
|
76
|
-
* holder. A lock left by a writer that died is taken over, and that is
|
|
77
|
-
* the delicate part: two waiters must not both conclude it is abandoned
|
|
78
|
-
* and both remove it, or the second would delete the first one's fresh
|
|
79
|
-
* lock. So removal itself is guarded (see `takeOver`), and a writer whose
|
|
80
|
-
* lock was taken over finds out before it writes (`stillHeld`). */
|
|
81
|
-
async locked(job) {
|
|
82
|
-
await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
83
|
-
const deadline = Date.now() + this.lockWaitMs;
|
|
84
|
-
const token = `${process.pid}:${randomUUID()}`;
|
|
85
|
-
for (;;) {
|
|
86
|
-
try {
|
|
87
|
-
await writeFile(this.lockPath, token, { flag: "wx" });
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
if (error.code !== "EEXIST")
|
|
92
|
-
throw error;
|
|
93
|
-
// Every way round this loop is bounded by the wait.
|
|
94
|
-
if (Date.now() > deadline)
|
|
95
|
-
throw new Error(`timed out waiting for the clearing position lock ${this.lockPath}`, { cause: error });
|
|
96
|
-
const seen = await this.inspect(this.lockPath);
|
|
97
|
-
if (seen && (holderIsDead(seen.token) || seen.age > this.lockStaleMs))
|
|
98
|
-
await this.takeOver(seen.token);
|
|
99
|
-
// Held by a live writer, unreadable, or already gone: wait and retry.
|
|
100
|
-
else
|
|
101
|
-
await sleep(10);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const stillHeld = async () => {
|
|
105
|
-
if ((await this.inspect(this.lockPath))?.token !== token)
|
|
106
|
-
throw new LostLock(`the clearing position lock ${this.lockPath} was taken over mid-write`);
|
|
107
|
-
};
|
|
108
|
-
try {
|
|
109
|
-
return await job(stillHeld);
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
// Release only our own lock: if it was taken over, it is not ours.
|
|
113
|
-
if ((await this.inspect(this.lockPath))?.token === token)
|
|
114
|
-
await rm(this.lockPath, { force: true });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async inspect(path) {
|
|
118
|
-
// One open file for both reads, so the token and the age always belong
|
|
119
|
-
// to the same lock even if it is replaced meanwhile.
|
|
120
|
-
let handle;
|
|
121
|
-
try {
|
|
122
|
-
handle = await open(path, "r");
|
|
123
|
-
const info = await handle.stat();
|
|
124
|
-
const token = await handle.readFile("utf8");
|
|
125
|
-
return { token, age: Date.now() - info.mtimeMs };
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
finally {
|
|
131
|
-
await handle?.close();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Remove `path` only while it still holds `token`. */
|
|
135
|
-
async removeIfHolds(path, token) {
|
|
136
|
-
if ((await this.inspect(path))?.token === token)
|
|
137
|
-
await rm(path, { force: true });
|
|
138
|
-
}
|
|
139
|
-
/** Remove an abandoned lock, at most one waiter at a time. The takeover
|
|
140
|
-
* file is itself created exclusively, so only one waiter is ever inside;
|
|
141
|
-
* it removes the lock only if it is still the abandoned one it saw. A
|
|
142
|
-
* waiter that arrives later finds either no lock or a fresh one from a
|
|
143
|
-
* new holder, and leaves it alone. */
|
|
144
|
-
async takeOver(abandoned) {
|
|
145
|
-
const mine = `${process.pid}:${randomUUID()}`;
|
|
146
|
-
try {
|
|
147
|
-
await writeFile(this.takeoverPath, mine, { flag: "wx" });
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
if (error.code !== "EEXIST")
|
|
151
|
-
throw error;
|
|
152
|
-
// Another waiter is doing it. If that waiter itself died here, clear
|
|
153
|
-
// its marker so takeovers are not blocked for good.
|
|
154
|
-
const other = await this.inspect(this.takeoverPath);
|
|
155
|
-
if (other && (holderIsDead(other.token) || other.age > this.lockStaleMs))
|
|
156
|
-
await this.removeIfHolds(this.takeoverPath, other.token);
|
|
157
|
-
await sleep(10);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
if ((await this.inspect(this.lockPath))?.token === abandoned)
|
|
162
|
-
await rm(this.lockPath, { force: true });
|
|
163
|
-
}
|
|
164
|
-
finally {
|
|
165
|
-
await this.removeIfHolds(this.takeoverPath, mine);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async get(key) {
|
|
169
|
-
return (await this.read())[key];
|
|
170
|
-
}
|
|
171
|
-
set(key, position) {
|
|
172
|
-
return this.locked(async (stillHeld) => {
|
|
173
|
-
const all = await this.read();
|
|
174
|
-
// Merged under the lock: another client may have signed since the
|
|
175
|
-
// caller read its position, and the high-water mark must not go down.
|
|
176
|
-
all[key] = keepHighest(all[key], position);
|
|
177
|
-
// Write beside the file under a name no other writer shares, then swap
|
|
178
|
-
// it in, so a crash mid-write cannot leave a truncated record.
|
|
179
|
-
const next = `${this.path}.${randomUUID()}.tmp`;
|
|
180
|
-
try {
|
|
181
|
-
await writeFile(next, `${JSON.stringify(all, null, 2)}\n`, {
|
|
182
|
-
mode: 0o600,
|
|
183
|
-
});
|
|
184
|
-
// A writer that hung long enough to be taken over must not swap in
|
|
185
|
-
// what it read before the newer writer's change.
|
|
186
|
-
await stillHeld();
|
|
187
|
-
await rename(next, this.path);
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
await rm(next, { force: true });
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|