@agoric/swingset-vat 0.32.3-dev-3dacdc3.0.3dacdc3 → 0.32.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.
Files changed (2) hide show
  1. package/package.json +20 -12
  2. package/tools/bundleTool.js +210 -171
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swingset-vat",
3
- "version": "0.32.3-dev-3dacdc3.0.3dacdc3",
3
+ "version": "0.32.3-dev-2db363c.0.2db363c",
4
4
  "description": "Vat/Container Launcher",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -22,7 +22,7 @@
22
22
  "lint:eslint": "yarn run -T eslint ."
23
23
  },
24
24
  "devDependencies": {
25
- "@agoric/xsnap": "0.14.3-dev-3dacdc3.0.3dacdc3",
25
+ "@agoric/xsnap": "0.14.3-dev-2db363c.0.2db363c",
26
26
  "@types/better-sqlite3": "^7.6.13",
27
27
  "@types/microtime": "^2.1.0",
28
28
  "@types/tmp": "^0.2.0",
@@ -30,15 +30,15 @@
30
30
  "ava": "^6.4.1"
31
31
  },
32
32
  "dependencies": {
33
- "@agoric/internal": "0.3.3-dev-3dacdc3.0.3dacdc3",
34
- "@agoric/kmarshal": "0.1.1-dev-3dacdc3.0.3dacdc3",
35
- "@agoric/store": "0.9.3-dev-3dacdc3.0.3dacdc3",
36
- "@agoric/swing-store": "0.9.2-dev-3dacdc3.0.3dacdc3",
37
- "@agoric/swingset-liveslots": "0.10.3-dev-3dacdc3.0.3dacdc3",
38
- "@agoric/swingset-xsnap-supervisor": "0.10.3-dev-3dacdc3.0.3dacdc3",
39
- "@agoric/time": "0.3.3-dev-3dacdc3.0.3dacdc3",
40
- "@agoric/vat-data": "0.5.3-dev-3dacdc3.0.3dacdc3",
41
- "@agoric/xsnap-lockdown": "0.14.1-dev-3dacdc3.0.3dacdc3",
33
+ "@agoric/internal": "0.3.3-dev-2db363c.0.2db363c",
34
+ "@agoric/kmarshal": "0.1.1-dev-2db363c.0.2db363c",
35
+ "@agoric/store": "0.9.3-dev-2db363c.0.2db363c",
36
+ "@agoric/swing-store": "0.9.2-dev-2db363c.0.2db363c",
37
+ "@agoric/swingset-liveslots": "0.10.3-dev-2db363c.0.2db363c",
38
+ "@agoric/swingset-xsnap-supervisor": "0.10.3-dev-2db363c.0.2db363c",
39
+ "@agoric/time": "0.3.3-dev-2db363c.0.2db363c",
40
+ "@agoric/vat-data": "0.5.3-dev-2db363c.0.2db363c",
41
+ "@agoric/xsnap-lockdown": "0.14.1-dev-2db363c.0.2db363c",
42
42
  "@endo/base64": "^1.0.12",
43
43
  "@endo/bundle-source": "^4.1.2",
44
44
  "@endo/captp": "^4.4.8",
@@ -89,10 +89,18 @@
89
89
  "example": "examples"
90
90
  },
91
91
  "ava": {
92
+ "extensions": {
93
+ "js": true,
94
+ "ts": "module"
95
+ },
92
96
  "workerThreads": false,
93
97
  "files": [
94
98
  "test/**/*.test.*"
95
99
  ],
100
+ "nodeArguments": [
101
+ "--import=ts-blank-space/register",
102
+ "--no-warnings"
103
+ ],
96
104
  "require": [
97
105
  "@agoric/internal/src/ava-force-exit.mjs",
98
106
  "@endo/init/debug.js"
@@ -105,5 +113,5 @@
105
113
  "typeCoverage": {
106
114
  "atLeast": 76.72
107
115
  },
108
- "gitHead": "3dacdc33b578ee52258e5771192bf8e57beb603b"
116
+ "gitHead": "2db363cfdc60b99cc644823e99bced301cf748b5"
109
117
  }
@@ -1,15 +1,20 @@
1
1
  import { makeNodeBundleCache as wrappedMaker } from '@endo/bundle-source/cache.js';
2
2
  import styles from 'ansi-styles'; // less authority than 'chalk'
3
- import { mkdir, readFile, rm, stat, writeFile } from 'fs/promises';
3
+ import * as fsPromises from 'fs/promises';
4
+ import { createHash } from 'node:crypto';
4
5
  import { fileURLToPath } from 'url';
5
6
  import path from 'path';
6
7
  import { setTimeout as delay } from 'timers/promises';
8
+ import { makeDirectoryLock } from '@agoric/internal/src/build-cache.js';
7
9
 
8
10
  /**
9
11
  * @import {EReturn} from '@endo/far';
12
+ * @import {BuildCacheEvent} from '@agoric/internal/src/build-cache-types.js';
13
+ * @import {BundleOptions, ModuleFormat} from '@endo/bundle-source';
10
14
  */
11
15
 
12
16
  /** @typedef {EReturn<typeof wrappedMaker>} WrappedBundleCache */
17
+ /** @typedef {BundleOptions<ModuleFormat> & Parameters<typeof wrappedMaker>[1]} BundleCacheOptions */
13
18
  /**
14
19
  * @typedef {{
15
20
  * sourceSpec?: string;
@@ -31,18 +36,139 @@ import { setTimeout as delay } from 'timers/promises';
31
36
  * loadRegistry: <T extends Record<string, BundleRegistryEntry>>(registry: T) => Promise<LoadedRegistryBundles<T>>;
32
37
  * }} BundleCache
33
38
  */
39
+ /**
40
+ * @typedef {{
41
+ * onBundleToolEvent?: (event: BundleToolEvent) => void,
42
+ * }} BundleToolEventSink
43
+ */
44
+ /**
45
+ * @typedef {{
46
+ * type: 'bundle-source-log';
47
+ * args: unknown[];
48
+ * phase: string;
49
+ * timestamp: number;
50
+ * }} BundleSourceLogEvent
51
+ */
52
+ /**
53
+ * @typedef {{
54
+ * type: 'registry-loaded';
55
+ * args: string[];
56
+ * phase: '[registry]';
57
+ * elapsedMs: number;
58
+ * bundleCount: number;
59
+ * }} RegistryLoadedEvent
60
+ */
61
+ /**
62
+ * @typedef {BundleSourceLogEvent | RegistryLoadedEvent | BuildCacheEvent} BundleToolEvent
63
+ */
64
+ /**
65
+ * @typedef {{
66
+ * delayMs: (ms: number) => Promise<unknown>,
67
+ * eventSink?: BundleToolEventSink,
68
+ * isPidAlive: (pid: number) => boolean,
69
+ * loadModule: Parameters<typeof wrappedMaker>[2],
70
+ * monotonicNow: () => number,
71
+ * now: () => number,
72
+ * pid: number,
73
+ * }} BundleToolPowers
74
+ */
34
75
 
35
76
  const BUNDLE_LOCK_ACQUIRE_TIMEOUT_MS = 5 * 60_000;
36
77
  const BUNDLE_STALE_LOCK_MS = 60_000;
37
78
 
79
+ const defaultBundleToolEventSink = {
80
+ onBundleToolEvent: event => {
81
+ if (
82
+ event.type !== 'bundle-source-log' &&
83
+ event.type !== 'registry-loaded'
84
+ ) {
85
+ return;
86
+ }
87
+ const { args = [], phase = '[check]', type } = event;
88
+ if (type === 'bundle-source-log') {
89
+ const joined = args.map(arg => String(arg)).join(' ');
90
+ if (joined.includes(' valid:')) {
91
+ return;
92
+ }
93
+ }
94
+ const flattened = args.map(arg =>
95
+ // Don't print stack traces.
96
+ arg instanceof Error ? arg.message : arg,
97
+ );
98
+ console.log(
99
+ `${styles.dim.open}[bundleTool]${phase}`,
100
+ ...flattened,
101
+ styles.dim.close,
102
+ );
103
+ },
104
+ };
105
+ harden(defaultBundleToolEventSink);
106
+
107
+ const inferLogPhase = args => {
108
+ const joined = args.map(arg => String(arg)).join(' ');
109
+ let phase = '[check]';
110
+ if (joined.includes(' add:') || joined.includes(' bundled ')) {
111
+ phase = '[rebuilt]';
112
+ }
113
+ return phase;
114
+ };
115
+ harden(inferLogPhase);
116
+
38
117
  /**
39
- * @param {string} dest
40
- * @param {Parameters<typeof wrappedMaker>[1]} options
41
- * @param {Parameters<typeof wrappedMaker>[2]} loadModule
42
- * @param {number} [pid]
118
+ * @param {Partial<BundleToolPowers>} [options]
119
+ * @returns {BundleToolPowers}
120
+ * @alpha
121
+ */
122
+ export const makeAmbientBundleToolPowers = (options = {}) => {
123
+ const {
124
+ delayMs = ms => delay(ms),
125
+ eventSink = defaultBundleToolEventSink,
126
+ isPidAlive = lockPid => {
127
+ if (!Number.isInteger(lockPid) || lockPid <= 0) {
128
+ return false;
129
+ }
130
+ try {
131
+ process.kill(lockPid, 0);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ },
137
+ loadModule = specifier => import(specifier),
138
+ monotonicNow = () => performance.now(),
139
+ now = () => Date.now(),
140
+ pid = process.pid,
141
+ } = options;
142
+ return harden({
143
+ delayMs,
144
+ eventSink,
145
+ isPidAlive,
146
+ loadModule,
147
+ monotonicNow,
148
+ now,
149
+ pid,
150
+ });
151
+ };
152
+ harden(makeAmbientBundleToolPowers);
153
+
154
+ /**
155
+ * @param {string} destPath
156
+ * @param {BundleToolPowers} powers
157
+ * @param {BundleCacheOptions} [options]
43
158
  * @returns {Promise<BundleCache>}
159
+ * @alpha
44
160
  */
45
- export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
161
+ export const makeNodeBundleCache = async (destPath, powers, options = {}) => {
162
+ const {
163
+ delayMs,
164
+ eventSink = defaultBundleToolEventSink,
165
+ isPidAlive,
166
+ loadModule,
167
+ monotonicNow,
168
+ now,
169
+ pid,
170
+ } = powers;
171
+
46
172
  /** @param {string} sourceSpec */
47
173
  const canonicalizeSourceSpec = sourceSpec => {
48
174
  if (sourceSpec.startsWith('file:')) {
@@ -66,147 +192,83 @@ export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
66
192
  }
67
193
  };
68
194
 
69
- const log = (...args) => {
70
- const joined = args.map(arg => String(arg)).join(' ');
71
- const isCacheHit = joined.includes(' valid:');
72
- if (isCacheHit) {
73
- return;
74
- }
75
- let phase = '[check]';
76
- if (joined.includes(' add:') || joined.includes(' bundled ')) {
77
- phase = '[rebuilt]';
195
+ const onEvent = eventSink.onBundleToolEvent || (() => {});
196
+ const optionsKeyFor = options0 => {
197
+ // Include options in the key on purpose: bundle output may differ by
198
+ // bundle-source options, and correctness prefers over-segmentation over
199
+ // accidental cache aliasing.
200
+ try {
201
+ return JSON.stringify(options0 || {});
202
+ } catch {
203
+ return '[unstringifiable-options]';
78
204
  }
79
- const flattened = args.map(arg =>
80
- // Don't print stack traces.
81
- arg instanceof Error ? arg.message : arg,
82
- );
83
- console.log(
84
- // Make all messages prefixed and dim.
85
- `${styles.dim.open}[bundleTool]${phase}`,
86
- ...flattened,
87
- styles.dim.close,
88
- );
89
205
  };
206
+ const sanitizeName = name => name.replace(/[^a-zA-Z0-9._-]/g, '-');
207
+ const getTargetNames = (canonicalRootPath, targetName, options0) => {
208
+ const requestedTargetName =
209
+ targetName || path.basename(canonicalRootPath, '.js');
210
+ const optionsKey = optionsKeyFor(options0);
211
+ const hashKey = createHash('sha256')
212
+ .update(`${canonicalRootPath}\n${optionsKey}`)
213
+ .digest('hex')
214
+ .slice(0, 12);
215
+ const cacheTargetName = `${sanitizeName(requestedTargetName)}-${hashKey}`;
216
+ return harden({ requestedTargetName, cacheTargetName, optionsKey });
217
+ };
218
+ const log = (...args) => {
219
+ onEvent({
220
+ type: 'bundle-source-log',
221
+ args,
222
+ phase: inferLogPhase(args),
223
+ timestamp: now(),
224
+ });
225
+ };
226
+
90
227
  const rawCache = await wrappedMaker(
91
- dest,
228
+ destPath,
92
229
  { log, ...options },
93
230
  loadModule,
94
231
  pid,
95
232
  );
96
- const lockRoot = path.resolve(dest, '.bundle-locks');
233
+ const lockRoot = path.resolve(destPath, '.bundle-locks');
97
234
  /** @type {Map<string, Promise<unknown>>} */
98
235
  const inProcessLoads = new Map();
99
-
100
- /**
101
- * @param {string} targetName
102
- * @param {() => Promise<any>} duringLock
103
- */
104
- const withBundleLock = async (targetName, duringLock) => {
105
- const lockName = `${encodeURIComponent(targetName)}.lock`;
106
- const lockPath = path.resolve(lockRoot, lockName);
107
- const ownerPath = path.resolve(lockPath, 'owner.json');
108
- await mkdir(lockRoot, { recursive: true });
109
- const started = performance.now();
110
-
111
- const isPidAlive = lockPid => {
112
- if (!Number.isInteger(lockPid) || lockPid <= 0) {
113
- return false;
114
- }
115
- try {
116
- process.kill(lockPid, 0);
117
- return true;
118
- } catch {
119
- return false;
120
- }
121
- };
122
-
123
- const maybeBreakStaleLock = async () => {
124
- const ownerTxt = await readFile(ownerPath, 'utf8').catch(() => undefined);
125
- let lockInfo;
126
- if (typeof ownerTxt === 'string') {
127
- try {
128
- lockInfo = JSON.parse(ownerTxt);
129
- } catch {
130
- // Another process may be in the middle of writing owner.json.
131
- // Treat malformed data as unknown owner and rely on age checks below.
132
- lockInfo = undefined;
133
- }
134
- }
135
- if (
136
- lockInfo &&
137
- Number.isInteger(lockInfo.pid) &&
138
- !isPidAlive(lockInfo.pid)
139
- ) {
140
- await rm(lockPath, { recursive: true, force: true });
141
- return true;
142
- }
143
- try {
144
- const st = await stat(lockPath);
145
- const ageMs = Date.now() - st.mtimeMs;
146
- if (ageMs >= BUNDLE_STALE_LOCK_MS) {
147
- await rm(lockPath, { recursive: true, force: true });
148
- return true;
149
- }
150
- } catch {
151
- return true;
152
- }
153
- return false;
154
- };
155
-
156
- // Cross-process lock via mkdir(). Retriable on EEXIST.
157
- for (;;) {
158
- try {
159
- await mkdir(lockPath);
160
- await writeFile(
161
- ownerPath,
162
- JSON.stringify({ pid: process.pid, createdAt: Date.now() }),
163
- 'utf8',
164
- );
165
- break;
166
- } catch (e) {
167
- if (e && typeof e === 'object' && 'code' in e && e.code === 'EEXIST') {
168
- const brokeStaleLock = await maybeBreakStaleLock();
169
- if (brokeStaleLock) {
170
- continue;
171
- }
172
- const waitedMs = performance.now() - started;
173
- if (waitedMs >= BUNDLE_LOCK_ACQUIRE_TIMEOUT_MS) {
174
- throw Error(
175
- `Timed out waiting for bundle lock ${lockPath} after ${waitedMs}ms`,
176
- );
177
- }
178
- await delay(20);
179
- continue;
180
- }
181
- throw e;
182
- }
183
- }
184
-
185
- try {
186
- return await duringLock();
187
- } finally {
188
- await rm(lockPath, { recursive: true, force: true });
189
- }
190
- };
236
+ const { withLock } = makeDirectoryLock({
237
+ fs: fsPromises,
238
+ delayMs,
239
+ now,
240
+ pid,
241
+ isPidAlive,
242
+ lockRoot,
243
+ staleLockMs: BUNDLE_STALE_LOCK_MS,
244
+ acquireTimeoutMs: BUNDLE_LOCK_ACQUIRE_TIMEOUT_MS,
245
+ onEvent,
246
+ });
191
247
 
192
248
  const cache = harden({
193
249
  ...rawCache,
194
250
  add: (rootPath, targetName, log0, options0) => {
195
251
  const canonicalRootPath = canonicalizeSourceSpec(rootPath);
196
- const resolvedTargetName =
197
- targetName || path.basename(canonicalRootPath, '.js');
198
- return withBundleLock(resolvedTargetName, () =>
199
- rawCache.add(canonicalRootPath, resolvedTargetName, log0, options0),
252
+ const { requestedTargetName, cacheTargetName } = getTargetNames(
253
+ canonicalRootPath,
254
+ targetName,
255
+ options0,
256
+ );
257
+ return withLock(requestedTargetName, () =>
258
+ rawCache.add(canonicalRootPath, cacheTargetName, log0, options0),
200
259
  );
201
260
  },
202
261
  validateOrAdd: (rootPath, targetName, log0, options0) => {
203
262
  const canonicalRootPath = canonicalizeSourceSpec(rootPath);
204
- const resolvedTargetName =
205
- targetName || path.basename(canonicalRootPath, '.js');
206
- return withBundleLock(resolvedTargetName, () =>
263
+ const { requestedTargetName, cacheTargetName } = getTargetNames(
264
+ canonicalRootPath,
265
+ targetName,
266
+ options0,
267
+ );
268
+ return withLock(requestedTargetName, () =>
207
269
  rawCache.validateOrAdd(
208
270
  canonicalRootPath,
209
- resolvedTargetName,
271
+ cacheTargetName,
210
272
  log0,
211
273
  options0,
212
274
  ),
@@ -214,23 +276,21 @@ export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
214
276
  },
215
277
  load: async (rootPath, targetName, log0, options0) => {
216
278
  const canonicalRootPath = canonicalizeSourceSpec(rootPath);
217
- const resolvedTargetName =
218
- targetName || path.basename(canonicalRootPath, '.js');
219
- const key = `${resolvedTargetName}:${canonicalRootPath}:${JSON.stringify(
220
- options0 || {},
221
- )}`;
279
+ const { requestedTargetName, cacheTargetName, optionsKey } =
280
+ getTargetNames(canonicalRootPath, targetName, options0);
281
+ const key = `${cacheTargetName}:${canonicalRootPath}:${optionsKey}`;
222
282
  const found = inProcessLoads.get(key);
223
283
  if (found) {
224
284
  return found;
225
285
  }
226
- const pending = withBundleLock(resolvedTargetName, async () => {
286
+ const pending = withLock(requestedTargetName, async () => {
227
287
  const { bundleFileName } = await rawCache.validateOrAdd(
228
288
  canonicalRootPath,
229
- resolvedTargetName,
289
+ cacheTargetName,
230
290
  log0,
231
291
  options0,
232
292
  );
233
- const bundlePath = path.resolve(dest, bundleFileName);
293
+ const bundlePath = path.resolve(destPath, bundleFileName);
234
294
  return import(bundlePath).then(m => harden(m.default));
235
295
  });
236
296
  inProcessLoads.set(key, pending);
@@ -250,7 +310,7 @@ export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
250
310
  * @returns {Promise<LoadedRegistryBundles<T>>}
251
311
  */
252
312
  const loadRegistry = async registry => {
253
- const started = performance.now();
313
+ const started = monotonicNow();
254
314
  await null;
255
315
  try {
256
316
  const loaded = await Promise.all(
@@ -270,11 +330,16 @@ export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
270
330
  harden(Object.fromEntries(loaded))
271
331
  );
272
332
  } finally {
273
- const elapsedMs = performance.now() - started;
274
- console.log(
275
- `${styles.dim.open}[bundleTool][registry] loaded ${Object.keys(registry).length} bundle(s) in ${elapsedMs}ms`,
276
- styles.dim.close,
277
- );
333
+ const elapsedMs = monotonicNow() - started;
334
+ onEvent({
335
+ type: 'registry-loaded',
336
+ phase: '[registry]',
337
+ args: [
338
+ `loaded ${Object.keys(registry).length} bundle(s) in ${elapsedMs}ms`,
339
+ ],
340
+ elapsedMs,
341
+ bundleCount: Object.keys(registry).length,
342
+ });
278
343
  }
279
344
  };
280
345
  return /** @type {BundleCache} */ (
@@ -285,41 +350,14 @@ export const makeNodeBundleCache = async (dest, options, loadModule, pid) => {
285
350
  );
286
351
  };
287
352
 
288
- /** @type {Map<string, Promise<BundleCache>>} */
289
- const providedCaches = new Map();
290
-
291
353
  /**
292
- * Make a new bundle cache for the destination. If there is already one for that
293
- * destination, return it.
294
- *
295
354
  * @param {string} dest
296
- * @param {object} options
297
- * @param {string} [options.format]
298
- * @param {boolean} [options.dev]
299
- * @param {number} [options.byteLimit=490_000] - Maximum bundle size in bytes before
300
- * falling back to optimizations that may reduce legibility.
301
- * @param {(id: string) => Promise<any>} loadModule
302
355
  * @param {number} [pid]
303
356
  * @returns {Promise<BundleCache>}
357
+ * @alpha
304
358
  */
305
- export const provideBundleCache = (dest, options, loadModule, pid) => {
306
- const uniqueDest = [dest, options.format, options.dev].join('-');
307
- // store the promise instead of awaiting to prevent a race
308
- let bundleCache = providedCaches.get(uniqueDest);
309
- if (!bundleCache) {
310
- bundleCache = makeNodeBundleCache(dest, options, loadModule, pid);
311
- providedCaches.set(uniqueDest, bundleCache);
312
- }
313
- return bundleCache;
314
- };
315
- harden(provideBundleCache);
316
-
317
- /**
318
- * @param {string} dest
319
- * @returns {Promise<BundleCache>}
320
- */
321
- export const unsafeMakeBundleCache = dest =>
322
- makeNodeBundleCache(dest, {}, s => import(s));
359
+ export const unsafeMakeBundleCache = (dest, pid = process.pid) =>
360
+ makeNodeBundleCache(dest, makeAmbientBundleToolPowers({ pid }));
323
361
 
324
362
  /**
325
363
  * This is a hack to share bundles across multiple SwingSet instances. It is
@@ -344,6 +382,7 @@ export const sharedBundleCachePath = fileURLToPath(
344
382
  * separately for use in other contexts that need to know where the shared
345
383
  * bundles are located, but that also makes it more likely to cause conflicts if
346
384
  * used in multiple places at once.
385
+ * @alpha
347
386
  */
348
387
  export const unsafeSharedBundleCache = unsafeMakeBundleCache(
349
388
  sharedBundleCachePath,