@agoric/internal 0.3.3-dev-3dacdc3.0.3dacdc3 → 0.3.3-dev-2db363c.0.2db363c

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/internal",
3
- "version": "0.3.3-dev-3dacdc3.0.3dacdc3",
3
+ "version": "0.3.3-dev-2db363c.0.2db363c",
4
4
  "description": "Externally unsupported utilities internal to agoric-sdk",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,7 +20,7 @@
20
20
  "lint:types": "yarn run -T tsc"
21
21
  },
22
22
  "dependencies": {
23
- "@agoric/base-zone": "0.1.1-dev-3dacdc3.0.3dacdc3",
23
+ "@agoric/base-zone": "0.1.1-dev-2db363c.0.2db363c",
24
24
  "@endo/cache-map": "^1.1.0",
25
25
  "@endo/common": "^1.2.13",
26
26
  "@endo/compartment-mapper": "^1.6.3",
@@ -38,7 +38,7 @@
38
38
  "jessie.js": "^0.3.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@agoric/cosmic-proto": "0.4.1-dev-3dacdc3.0.3dacdc3",
41
+ "@agoric/cosmic-proto": "0.4.1-dev-2db363c.0.2db363c",
42
42
  "@endo/exo": "^1.5.12",
43
43
  "@endo/init": "^1.1.12",
44
44
  "@endo/ses-ava": "^1.3.2",
@@ -66,5 +66,5 @@
66
66
  "typeCoverage": {
67
67
  "atLeast": 92.84
68
68
  },
69
- "gitHead": "3dacdc33b578ee52258e5771192bf8e57beb603b"
69
+ "gitHead": "2db363cfdc60b99cc644823e99bced301cf748b5"
70
70
  }
@@ -0,0 +1,33 @@
1
+ export interface LockLifecycleEvent {
2
+ key: string;
3
+ lockPath: string;
4
+ type: 'lock-acquired' | 'lock-released';
5
+ }
6
+ export interface LockWaitingEvent {
7
+ key: string;
8
+ lockPath: string;
9
+ type: 'lock-waiting';
10
+ waitedMs: number;
11
+ }
12
+ export interface LockBrokenEvent {
13
+ ageMs?: number;
14
+ key: string;
15
+ lockPath: string;
16
+ ownerPid?: number;
17
+ reason: 'dead-owner' | 'stale-age';
18
+ staleLockMs?: number;
19
+ type: 'lock-broken';
20
+ }
21
+ export type BuildCacheEvent = LockLifecycleEvent | LockWaitingEvent | LockBrokenEvent;
22
+ export interface DirectoryLockPowers {
23
+ acquireTimeoutMs: number;
24
+ delayMs: (ms: number) => Promise<unknown>;
25
+ fs: Pick<typeof import('node:fs/promises'), 'mkdir' | 'readFile' | 'rm' | 'stat' | 'writeFile'>;
26
+ isPidAlive: (pid: number) => boolean;
27
+ lockRoot: string;
28
+ now: () => number;
29
+ onEvent?: (event: BuildCacheEvent) => void;
30
+ pid: number;
31
+ staleLockMs: number;
32
+ }
33
+ //# sourceMappingURL=build-cache-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-cache-types.d.ts","sourceRoot":"","sources":["build-cache-types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,eAAe,GAAG,eAAe,CAAC;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,WAAW,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,EAAE,EAAE,IAAI,CACN,cAAc,kBAAkB,CAAC,EACjC,OAAO,GAAG,UAAU,GAAG,IAAI,GAAG,MAAM,GAAG,WAAW,CACnD,CAAC;IACF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,12 @@
1
+ export function makeDirectoryLock(powers: DirectoryLockPowers): {
2
+ withLock: (key: string, body: () => Promise<any>) => Promise<any>;
3
+ };
4
+ export function writeFileAtomic({ fs, filePath, data, now, pid }: {
5
+ fs: Pick<typeof import("node:fs/promises"), "rename" | "writeFile">;
6
+ filePath: string;
7
+ data: string;
8
+ now: () => number;
9
+ pid: number;
10
+ }): Promise<void>;
11
+ import type { DirectoryLockPowers } from './build-cache-types.js';
12
+ //# sourceMappingURL=build-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-cache.d.ts","sourceRoot":"","sources":["build-cache.js"],"names":[],"mappings":"AAUO,0CAFI,mBAAmB;oBAkEjB,MAAM,QACN,MAAM,OAAO,CAAC,GAAG,CAAC;EAsF9B;AAgBM,kEAVI;IACN,EAAE,EAAE,IAAI,CAAC,iCAA0B,EAAE,QAAQ,GAAG,WAAW,CAAC,CAAC;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,iBASH;yCAlLsD,wBAAwB"}
@@ -0,0 +1,184 @@
1
+ /* eslint-env node */
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * @import {BuildCacheEvent, DirectoryLockPowers} from './build-cache-types.js';
6
+ */
7
+
8
+ /**
9
+ * @param {DirectoryLockPowers} powers
10
+ */
11
+ export const makeDirectoryLock = powers => {
12
+ const {
13
+ fs,
14
+ delayMs,
15
+ now,
16
+ pid,
17
+ isPidAlive,
18
+ lockRoot,
19
+ staleLockMs,
20
+ acquireTimeoutMs,
21
+ onEvent = () => {},
22
+ } = powers;
23
+ const safeEmit = event => {
24
+ try {
25
+ onEvent(event);
26
+ } catch {
27
+ // Event sinks must not interfere with cache lock correctness.
28
+ }
29
+ };
30
+
31
+ /** @param {string} dpath */
32
+ const mkdirUnlessExists = async dpath => {
33
+ await null; // avoid accidentally catching synchronous exceptions
34
+ try {
35
+ await fs.mkdir(dpath);
36
+ return true;
37
+ } catch (err) {
38
+ const e = /** @type {NodeJS.ErrnoException} */ (err);
39
+ if (e.code === 'EEXIST') {
40
+ return false;
41
+ }
42
+ throw err;
43
+ }
44
+ };
45
+
46
+ /** @param {string} fpath */
47
+ const statUnlessMissing = async fpath => {
48
+ await null; // avoid accidentally catching synchronous exceptions
49
+ try {
50
+ return await fs.stat(fpath);
51
+ } catch (err) {
52
+ const e = /** @type {NodeJS.ErrnoException} */ (err);
53
+ if (e.code === 'ENOENT') {
54
+ return undefined;
55
+ }
56
+ throw err;
57
+ }
58
+ };
59
+
60
+ /** @param {string} fpath */
61
+ const readUnlessMissing = async fpath => {
62
+ await null; // avoid accidentally catching synchronous exceptions
63
+ try {
64
+ return await fs.readFile(fpath, 'utf8');
65
+ } catch (err) {
66
+ const e = /** @type {NodeJS.ErrnoException} */ (err);
67
+ if (e.code === 'ENOENT') {
68
+ return undefined;
69
+ }
70
+ throw err;
71
+ }
72
+ };
73
+
74
+ /**
75
+ * @param {string} key
76
+ * @param {() => Promise<any>} body
77
+ */
78
+ const withLock = async (key, body) => {
79
+ const lockPath = path.join(lockRoot, `${encodeURIComponent(key)}.lock`);
80
+ const ownerPath = path.join(lockPath, 'owner.json');
81
+ await fs.mkdir(lockRoot, { recursive: true });
82
+ const started = now();
83
+
84
+ const maybeBreakStaleLock = async () => {
85
+ const ownerText = await readUnlessMissing(ownerPath);
86
+ if (ownerText) {
87
+ try {
88
+ const ownerInfo = JSON.parse(ownerText);
89
+ if (
90
+ ownerInfo &&
91
+ typeof ownerInfo === 'object' &&
92
+ Number.isInteger(ownerInfo.pid) &&
93
+ !isPidAlive(ownerInfo.pid)
94
+ ) {
95
+ await fs.rm(lockPath, { recursive: true, force: true });
96
+ safeEmit({
97
+ type: 'lock-broken',
98
+ key,
99
+ lockPath,
100
+ reason: 'dead-owner',
101
+ ownerPid: ownerInfo.pid,
102
+ });
103
+ return true;
104
+ }
105
+ } catch {
106
+ // rely on age-based lock breaking for malformed owner files
107
+ }
108
+ }
109
+
110
+ const lockStats = await statUnlessMissing(lockPath);
111
+ if (!lockStats) {
112
+ return true;
113
+ }
114
+ const ageMs = now() - lockStats.mtimeMs;
115
+ if (ageMs >= staleLockMs) {
116
+ await fs.rm(lockPath, { recursive: true, force: true });
117
+ safeEmit({
118
+ type: 'lock-broken',
119
+ key,
120
+ lockPath,
121
+ reason: 'stale-age',
122
+ ageMs,
123
+ staleLockMs,
124
+ });
125
+ return true;
126
+ }
127
+
128
+ return false;
129
+ };
130
+
131
+ for (;;) {
132
+ if (await mkdirUnlessExists(lockPath)) {
133
+ await fs.writeFile(
134
+ ownerPath,
135
+ JSON.stringify({ pid, createdAt: now() }),
136
+ 'utf8',
137
+ );
138
+ safeEmit({ type: 'lock-acquired', key, lockPath });
139
+ break;
140
+ }
141
+ const recovered = await maybeBreakStaleLock();
142
+ if (recovered) {
143
+ continue;
144
+ }
145
+ const waitedMs = now() - started;
146
+ safeEmit({ type: 'lock-waiting', key, lockPath, waitedMs });
147
+ if (waitedMs >= acquireTimeoutMs) {
148
+ throw Error(`Timed out waiting for cache lock ${lockPath}`);
149
+ }
150
+ await delayMs(20);
151
+ }
152
+
153
+ try {
154
+ return await body();
155
+ } finally {
156
+ await fs.rm(lockPath, { recursive: true, force: true });
157
+ safeEmit({ type: 'lock-released', key, lockPath });
158
+ }
159
+ };
160
+
161
+ return harden({ withLock });
162
+ };
163
+ harden(makeDirectoryLock);
164
+
165
+ let atomicWriteSequence = 0;
166
+
167
+ /**
168
+ * @param {{
169
+ * fs: Pick<import('node:fs/promises'), 'rename' | 'writeFile'>;
170
+ * filePath: string;
171
+ * data: string;
172
+ * now: () => number;
173
+ * pid: number;
174
+ * }} options
175
+ * @throws {NodeJS.ErrnoException} when a unique temp file cannot be created
176
+ * or the atomic rename fails.
177
+ */
178
+ export const writeFileAtomic = async ({ fs, filePath, data, now, pid }) => {
179
+ atomicWriteSequence += 1;
180
+ const tempPath = `${filePath}.${pid}.${now()}.${atomicWriteSequence}.tmp`;
181
+ await fs.writeFile(tempPath, data, { flag: 'wx' });
182
+ await fs.rename(tempPath, filePath);
183
+ };
184
+ harden(writeFileAtomic);
@@ -1 +1,2 @@
1
1
  export type * from './types.js';
2
+ export type * from './build-cache-types.js';