@veryfront/ext-content-mdx 0.1.1190 → 0.1.1192

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 (66) hide show
  1. package/esm/deno.d.ts +6 -0
  2. package/esm/deno.js +13 -7
  3. package/esm/src/errors/error-registry/module.d.ts +4 -0
  4. package/esm/src/errors/error-registry/module.d.ts.map +1 -1
  5. package/esm/src/errors/error-registry/module.js +16 -0
  6. package/esm/src/errors/error-registry.d.ts +2 -0
  7. package/esm/src/errors/error-registry.d.ts.map +1 -1
  8. package/esm/src/errors/types.d.ts.map +1 -1
  9. package/esm/src/errors/types.js +5 -2
  10. package/esm/src/platform/compat/error-introspection.d.ts +12 -1
  11. package/esm/src/platform/compat/error-introspection.d.ts.map +1 -1
  12. package/esm/src/platform/compat/error-introspection.js +40 -5
  13. package/esm/src/platform/compat/path/basic-operations.d.ts.map +1 -1
  14. package/esm/src/platform/compat/path/basic-operations.js +6 -2
  15. package/esm/src/platform/compat/path/portable.d.ts.map +1 -1
  16. package/esm/src/platform/compat/path/portable.js +98 -54
  17. package/esm/src/platform/compat/path/resolution.d.ts.map +1 -1
  18. package/esm/src/platform/compat/path/resolution.js +14 -5
  19. package/esm/src/platform/compat/primordials/array.d.ts +8 -0
  20. package/esm/src/platform/compat/primordials/array.d.ts.map +1 -0
  21. package/esm/src/platform/compat/primordials/array.js +33 -0
  22. package/esm/src/transforms/import-rewriter/url-builder.d.ts +6 -0
  23. package/esm/src/transforms/import-rewriter/url-builder.d.ts.map +1 -1
  24. package/esm/src/transforms/import-rewriter/url-builder.js +96 -29
  25. package/esm/src/utils/base64url.d.ts.map +1 -1
  26. package/esm/src/utils/base64url.js +10 -4
  27. package/esm/src/utils/bundle-manifest.d.ts +19 -1
  28. package/esm/src/utils/bundle-manifest.d.ts.map +1 -1
  29. package/esm/src/utils/bundle-manifest.js +124 -27
  30. package/esm/src/utils/constants/limits.d.ts +1 -1
  31. package/esm/src/utils/constants/limits.d.ts.map +1 -1
  32. package/esm/src/utils/constants/limits.js +1 -1
  33. package/esm/src/utils/hash-utils.d.ts.map +1 -1
  34. package/esm/src/utils/hash-utils.js +32 -6
  35. package/esm/src/utils/id.d.ts +16 -0
  36. package/esm/src/utils/id.d.ts.map +1 -0
  37. package/esm/src/utils/id.js +69 -0
  38. package/esm/src/utils/import-lockfile.d.ts +52 -1
  39. package/esm/src/utils/import-lockfile.d.ts.map +1 -1
  40. package/esm/src/utils/import-lockfile.js +1042 -58
  41. package/esm/src/utils/index.d.ts +1 -1
  42. package/esm/src/utils/index.d.ts.map +1 -1
  43. package/esm/src/utils/index.js +1 -1
  44. package/esm/src/utils/logger/logger.d.ts +1 -0
  45. package/esm/src/utils/logger/logger.d.ts.map +1 -1
  46. package/esm/src/utils/logger/logger.js +359 -78
  47. package/esm/src/utils/logger/redact.d.ts +4 -2
  48. package/esm/src/utils/logger/redact.d.ts.map +1 -1
  49. package/esm/src/utils/logger/redact.js +300 -112
  50. package/esm/src/utils/logger/serialization.d.ts +3 -0
  51. package/esm/src/utils/logger/serialization.d.ts.map +1 -0
  52. package/esm/src/utils/logger/serialization.js +81 -0
  53. package/esm/src/utils/memoize.d.ts +1 -1
  54. package/esm/src/utils/memoize.d.ts.map +1 -1
  55. package/esm/src/utils/memoize.js +16 -5
  56. package/esm/src/utils/path-utils.d.ts.map +1 -1
  57. package/esm/src/utils/path-utils.js +4 -2
  58. package/esm/src/utils/sleep.d.ts +8 -0
  59. package/esm/src/utils/sleep.d.ts.map +1 -1
  60. package/esm/src/utils/sleep.js +11 -2
  61. package/esm/src/utils/timer.d.ts +10 -0
  62. package/esm/src/utils/timer.d.ts.map +1 -0
  63. package/esm/src/utils/timer.js +19 -0
  64. package/esm/src/utils/version-constant.d.ts +1 -1
  65. package/esm/src/utils/version-constant.js +1 -1
  66. package/package.json +2 -2
@@ -1,12 +1,340 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
1
2
  import { computeHash } from "./hash-utils.js";
3
+ import { generateUuid } from "./id.js";
2
4
  import { serverLogger } from "./logger/index.js";
3
- import { createFileSystem } from "../platform/compat/fs.js";
4
- import { CACHE_ERROR, NETWORK_ERROR } from "../errors/error-registry.js";
5
+ import { createFileSystem, realPath as resolvePlatformRealPath, } from "../platform/compat/fs.js";
6
+ import { normalize } from "../platform/compat/path/resolution.js";
7
+ import { CACHE_ERROR, INVALID_ARGUMENT, LOCKFILE_FORMAT_MISMATCH, LOCKFILE_READ_ERROR, NETWORK_ERROR, } from "../errors/error-registry.js";
8
+ import { snapshotVeryfrontError } from "../errors/types.js";
5
9
  const logger = serverLogger.component("lockfile");
6
10
  const LOCKFILE_NAME = "veryfront.lock";
7
11
  const LOCKFILE_VERSION = 1;
12
+ const LOCKFILE_CANONICALIZATION_TIMEOUT_MS = 1_000;
13
+ // Canonicalization failures are exceptional and must not create an unbounded
14
+ // process-wide journal. The limit applies only while a shared backing store
15
+ // cannot establish stable identities; healthy paths reconcile and release
16
+ // records as normal.
17
+ const MAX_UNRESOLVED_LOCKFILE_RECORDS = 1_024;
18
+ // Historical aliases are opportunistic. Never let an old or deleted project
19
+ // hold the shared filesystem queue for the normal one-second path timeout.
20
+ const HISTORICAL_LOCKFILE_CANONICALIZATION_TIMEOUT_MS = 25;
21
+ const MAX_HISTORICAL_RECONCILIATIONS_PER_ACCESS = 8;
22
+ const MAX_HISTORICAL_CANONICALIZATION_ATTEMPTS = 8;
23
+ const HISTORICAL_CANONICALIZATION_RETRY_BASE_MS = 250;
24
+ const HISTORICAL_CANONICALIZATION_RETRY_MAX_MS = 30_000;
25
+ const apply = Reflect.apply;
26
+ const NativeError = Error;
27
+ const NativeFinalizationRegistry = FinalizationRegistry;
28
+ const NativeJSON = JSON;
29
+ const NativeMap = Map;
30
+ const NativePromise = Promise;
31
+ const NativeURL = URL;
32
+ const NativeWeakMap = WeakMap;
33
+ const NativeWeakRef = WeakRef;
34
+ const arrayIsArray = Array.isArray;
35
+ const arraySort = Array.prototype.sort;
36
+ const finalizationRegistryRegister = FinalizationRegistry.prototype.register;
37
+ const finalizationRegistryUnregister = FinalizationRegistry.prototype.unregister;
38
+ const mapClear = Map.prototype.clear;
39
+ const mapDelete = Map.prototype.delete;
40
+ const mapEntries = Map.prototype.entries;
41
+ const mapGet = Map.prototype.get;
42
+ const mapSet = Map.prototype.set;
43
+ const jsonParse = JSON.parse;
44
+ const jsonStringify = JSON.stringify;
45
+ const nativeClearTimeout = clearTimeout;
46
+ const nativeSetTimeout = dntShim.setTimeout;
47
+ const numberIsSafeInteger = Number.isSafeInteger;
48
+ const objectCreate = Object.create;
49
+ const objectDefineProperty = Object.defineProperty;
50
+ const objectEntries = Object.entries;
51
+ const objectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
52
+ const objectGetPrototypeOf = Object.getPrototypeOf;
53
+ const objectHasOwn = Object.hasOwn;
54
+ const objectKeys = Object.keys;
55
+ const mapIteratorNext = objectGetPrototypeOf(new NativeMap().entries()).next;
56
+ const mapSizeGetter = objectGetOwnPropertyDescriptor(Map.prototype, "size").get;
57
+ const promiseResolve = Promise.resolve;
58
+ const promiseReject = Promise.reject;
59
+ const promiseThen = Promise.prototype.then;
60
+ const weakMapGet = WeakMap.prototype.get;
61
+ const weakMapSet = WeakMap.prototype.set;
62
+ const weakRefDeref = WeakRef.prototype.deref;
63
+ const urlToString = URL.prototype.toString;
64
+ const dateNow = Date.now;
65
+ function getMapValue(map, key) {
66
+ return apply(mapGet, map, [key]);
67
+ }
68
+ function setMapValue(map, key, value) {
69
+ apply(mapSet, map, [key, value]);
70
+ }
71
+ function deleteMapValue(map, key) {
72
+ return apply(mapDelete, map, [key]);
73
+ }
74
+ function clearMap(map) {
75
+ apply(mapClear, map, []);
76
+ }
77
+ function getMapSize(map) {
78
+ return apply(mapSizeGetter, map, []);
79
+ }
80
+ function getWeakMapValue(map, key) {
81
+ return apply(weakMapGet, map, [key]);
82
+ }
83
+ function setWeakMapValue(map, key, value) {
84
+ apply(weakMapSet, map, [key, value]);
85
+ }
86
+ function dereference(reference) {
87
+ return apply(weakRefDeref, reference, []);
88
+ }
89
+ function registerFinalizer(registry, target, heldValue, unregisterToken) {
90
+ apply(finalizationRegistryRegister, registry, [target, heldValue, unregisterToken]);
91
+ }
92
+ function unregisterFinalizer(registry, unregisterToken) {
93
+ return apply(finalizationRegistryUnregister, registry, [unregisterToken]);
94
+ }
95
+ function resolveVoidPromise() {
96
+ return apply(promiseResolve, NativePromise, []);
97
+ }
98
+ function rejectPromise(reason) {
99
+ return apply(promiseReject, NativePromise, [reason]);
100
+ }
101
+ function thenPromise(promise, onFulfilled, onRejected) {
102
+ return apply(promiseThen, promise, [onFulfilled, onRejected]);
103
+ }
104
+ function chainPromise(promise, onFulfilled) {
105
+ return new NativePromise((resolve, reject) => {
106
+ thenPromise(promise, (value) => {
107
+ let next;
108
+ try {
109
+ next = onFulfilled(value);
110
+ }
111
+ catch (error) {
112
+ reject(error);
113
+ return;
114
+ }
115
+ // Do not return `next` from this handler. Native promise adoption reads
116
+ // the live `.then` property, which tenant code can replace after this
117
+ // module loads. Apply the captured intrinsic explicitly instead.
118
+ thenPromise(next, resolve, reject);
119
+ }, reject);
120
+ });
121
+ }
122
+ function allPromises(promises) {
123
+ return new NativePromise((resolve, reject) => {
124
+ const results = [];
125
+ results.length = promises.length;
126
+ let remaining = promises.length;
127
+ if (remaining === 0) {
128
+ resolve(results);
129
+ return;
130
+ }
131
+ for (let index = 0; index < promises.length; index++) {
132
+ thenPromise(promises[index], (value) => {
133
+ results[index] = value;
134
+ remaining--;
135
+ if (remaining === 0)
136
+ resolve(results);
137
+ }, reject);
138
+ }
139
+ });
140
+ }
141
+ function racePromiseWithTimeout(promise, timeoutValue, timeoutMs) {
142
+ let timeoutId;
143
+ const raced = new NativePromise((resolve, reject) => {
144
+ thenPromise(promise, resolve, reject);
145
+ timeoutId = nativeSetTimeout(() => resolve(timeoutValue), timeoutMs);
146
+ });
147
+ return {
148
+ promise: raced,
149
+ cancelTimeout: () => {
150
+ if (timeoutId !== undefined)
151
+ nativeClearTimeout(timeoutId);
152
+ },
153
+ };
154
+ }
155
+ function forEachMapEntry(map, callback) {
156
+ const iterator = apply(mapEntries, map, []);
157
+ while (true) {
158
+ const next = apply(mapIteratorNext, iterator, []);
159
+ if (next.done)
160
+ return;
161
+ if (callback(next.value[0], next.value[1]) === false)
162
+ return;
163
+ }
164
+ }
165
+ function cloneLockfileEntry(entry) {
166
+ const sanitized = sanitizeLockfileEntry(entry);
167
+ if (sanitized === null)
168
+ throw lockfileInputError("invalid-structure");
169
+ return sanitized;
170
+ }
171
+ function defineImportEntry(imports, url, entry) {
172
+ // A null-prototype descriptor is required here. Tenant code can mutate
173
+ // Object.prototype in a shared runtime; inherited accessor/value fields can
174
+ // otherwise turn an ordinary data descriptor into an invalid descriptor.
175
+ const descriptor = objectCreate(null);
176
+ descriptor.value = entry;
177
+ descriptor.enumerable = true;
178
+ descriptor.configurable = true;
179
+ descriptor.writable = true;
180
+ objectDefineProperty(imports, url, descriptor);
181
+ }
182
+ function createImportDictionary(entries = [], prototype = "internal") {
183
+ const imports = prototype === "internal"
184
+ ? objectCreate(null)
185
+ : {};
186
+ for (let index = 0; index < entries.length; index++) {
187
+ const entry = entries[index];
188
+ defineImportEntry(imports, entry[0], entry[1]);
189
+ }
190
+ return imports;
191
+ }
192
+ function cloneLockfileData(data, prototype = "internal") {
193
+ const sourceEntries = objectEntries(data.imports);
194
+ const entries = [];
195
+ entries.length = sourceEntries.length;
196
+ for (let index = 0; index < sourceEntries.length; index++) {
197
+ const sourceEntry = sourceEntries[index];
198
+ entries[index] = [sourceEntry[0], cloneLockfileEntry(sourceEntry[1])];
199
+ }
200
+ return {
201
+ version: LOCKFILE_VERSION,
202
+ imports: createImportDictionary(entries, prototype),
203
+ };
204
+ }
205
+ function createInternalLockfile() {
206
+ return { version: LOCKFILE_VERSION, imports: createImportDictionary() };
207
+ }
208
+ function isRecord(value) {
209
+ return typeof value === "object" && value !== null && !arrayIsArray(value);
210
+ }
211
+ function getOwnDataProperty(value, key) {
212
+ const descriptor = objectGetOwnPropertyDescriptor(value, key);
213
+ if (descriptor === undefined || !objectHasOwn(descriptor, "value"))
214
+ return undefined;
215
+ return { value: descriptor.value };
216
+ }
217
+ function sanitizeLockfileEntry(value) {
218
+ if (!isRecord(value))
219
+ return null;
220
+ const resolved = getOwnDataProperty(value, "resolved")?.value;
221
+ const integrity = getOwnDataProperty(value, "integrity")?.value;
222
+ if (typeof resolved !== "string" || typeof integrity !== "string")
223
+ return null;
224
+ const dependencies = getOwnDataProperty(value, "dependencies")?.value;
225
+ let sanitizedDependencies;
226
+ if (dependencies !== undefined) {
227
+ const sanitized = sanitizeStringArray(dependencies);
228
+ if (sanitized === null)
229
+ return null;
230
+ sanitizedDependencies = sanitized;
231
+ }
232
+ const fetchedAt = getOwnDataProperty(value, "fetchedAt")?.value;
233
+ if (fetchedAt !== undefined && typeof fetchedAt !== "string")
234
+ return null;
235
+ return {
236
+ resolved,
237
+ integrity,
238
+ ...(sanitizedDependencies === undefined ? {} : { dependencies: sanitizedDependencies }),
239
+ ...(fetchedAt === undefined ? {} : { fetchedAt }),
240
+ };
241
+ }
242
+ function sanitizeStringArray(value) {
243
+ if (!arrayIsArray(value))
244
+ return null;
245
+ const length = getOwnDataProperty(value, "length")?.value;
246
+ if (typeof length !== "number" || !numberIsSafeInteger(length) || length < 0)
247
+ return null;
248
+ const sanitized = [];
249
+ let index = 0;
250
+ const keys = objectKeys(value);
251
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex++) {
252
+ const key = keys[keyIndex];
253
+ if (key !== String(index))
254
+ continue;
255
+ const item = getOwnDataProperty(value, key)?.value;
256
+ if (typeof item !== "string")
257
+ return null;
258
+ objectDefineProperty(sanitized, index, {
259
+ value: item,
260
+ enumerable: true,
261
+ configurable: true,
262
+ writable: true,
263
+ });
264
+ index++;
265
+ }
266
+ return index === length ? sanitized : null;
267
+ }
268
+ function lockfileReadError(lockfilePath, reason, cause) {
269
+ const description = reason === "access-failed"
270
+ ? "could not be accessed"
271
+ : reason === "invalid-json"
272
+ ? "does not contain valid JSON"
273
+ : "has an invalid structure";
274
+ return LOCKFILE_READ_ERROR.create({
275
+ detail: `The lockfile ${description}. The file was left untouched.`,
276
+ cause,
277
+ context: { lockfilePath, reason },
278
+ });
279
+ }
280
+ function lockfileInputError(reason, cause) {
281
+ return INVALID_ARGUMENT.create({
282
+ detail: "The provided lockfile data has an invalid structure. The file was left untouched.",
283
+ cause,
284
+ context: { reason },
285
+ });
286
+ }
287
+ function lockfileFormatMismatch(lockfilePath, version) {
288
+ return LOCKFILE_FORMAT_MISMATCH.create({
289
+ detail: `The lockfile uses format version ${version}, but this ` +
290
+ `Veryfront build supports version ${LOCKFILE_VERSION}. The file was left untouched. ` +
291
+ "Upgrade Veryfront or migrate the lockfile before reading or modifying it.",
292
+ context: {
293
+ lockfilePath,
294
+ expectedVersion: LOCKFILE_VERSION,
295
+ actualVersion: version,
296
+ },
297
+ });
298
+ }
299
+ function sanitizeLockfileData(value, lockfilePath, invalidStructureError = () => lockfileReadError(lockfilePath, "invalid-structure")) {
300
+ if (!isRecord(value))
301
+ throw invalidStructureError();
302
+ const version = getOwnDataProperty(value, "version")?.value;
303
+ if (typeof version !== "number" || !numberIsSafeInteger(version)) {
304
+ throw invalidStructureError();
305
+ }
306
+ if (version !== LOCKFILE_VERSION) {
307
+ throw lockfileFormatMismatch(lockfilePath, version);
308
+ }
309
+ const parsedImports = getOwnDataProperty(value, "imports")?.value;
310
+ if (!isRecord(parsedImports)) {
311
+ throw invalidStructureError();
312
+ }
313
+ const imports = [];
314
+ const urls = objectKeys(parsedImports);
315
+ for (let index = 0; index < urls.length; index++) {
316
+ const url = urls[index];
317
+ const entry = getOwnDataProperty(parsedImports, url)?.value;
318
+ const sanitizedEntry = sanitizeLockfileEntry(entry);
319
+ if (sanitizedEntry === null) {
320
+ throw invalidStructureError();
321
+ }
322
+ imports[imports.length] = [url, sanitizedEntry];
323
+ }
324
+ return { version: LOCKFILE_VERSION, imports: createImportDictionary(imports) };
325
+ }
326
+ function parseLockfile(content, lockfilePath) {
327
+ let parsed;
328
+ try {
329
+ parsed = apply(jsonParse, NativeJSON, [content]);
330
+ }
331
+ catch (cause) {
332
+ throw lockfileReadError(lockfilePath, "invalid-json", cause);
333
+ }
334
+ return sanitizeLockfileData(parsed, lockfilePath);
335
+ }
8
336
  export function createEmptyLockfile() {
9
- return { version: LOCKFILE_VERSION, imports: {} };
337
+ return { version: LOCKFILE_VERSION, imports: createImportDictionary([], "public") };
10
338
  }
11
339
  /** Compute integrity. */
12
340
  export async function computeIntegrity(content) {
@@ -17,92 +345,747 @@ export async function verifyIntegrity(content, integrity) {
17
345
  const computed = await computeIntegrity(content);
18
346
  return computed === integrity;
19
347
  }
348
+ const PLATFORM_FS_COORDINATION_KEY = "veryfront-platform-filesystem";
349
+ const adapterIdentityByInstance = new NativeWeakMap();
350
+ const lockfileAccessTails = new NativeMap();
351
+ const lockfileSharedStateReferences = new NativeMap();
352
+ const lockfileSharedStateAccessIdentities = new NativeWeakMap();
353
+ const lockfileCoordinationDomainByAdapter = new NativeWeakMap();
354
+ const sharedLockfileCoordinationDomains = new NativeMap();
355
+ const sharedLockfileCoordinationRegistry = new NativeFinalizationRegistry(({ accessKey, unregisterToken }) => {
356
+ const current = getMapValue(sharedLockfileCoordinationDomains, accessKey);
357
+ if (current?.unregisterToken === unregisterToken &&
358
+ dereference(current.reference) === undefined) {
359
+ deleteMapValue(sharedLockfileCoordinationDomains, accessKey);
360
+ }
361
+ });
362
+ let nextLockfileMutationSequence = 1n;
363
+ let nextLockfileSharedStateAccessIdentity = 1n;
364
+ const lockfileSharedStateRegistry = new NativeFinalizationRegistry(({ stateKey, unregisterToken }) => {
365
+ const current = getMapValue(lockfileSharedStateReferences, stateKey);
366
+ if (current?.unregisterToken === unregisterToken &&
367
+ dereference(current.reference) === undefined) {
368
+ deleteMapValue(lockfileSharedStateReferences, stateKey);
369
+ }
370
+ });
371
+ let nextAdapterIdentity = 1n;
372
+ function createLockfileSharedState() {
373
+ const state = objectCreate(null);
374
+ state.revision = 0n;
375
+ state.lastClearSequence = 0n;
376
+ return state;
377
+ }
378
+ function resolveLockfileSharedState(state) {
379
+ let root = state;
380
+ while (root.parent)
381
+ root = root.parent;
382
+ let current = state;
383
+ while (current.parent && current.parent !== root) {
384
+ const parent = current.parent;
385
+ current.parent = root;
386
+ current = parent;
387
+ }
388
+ return root;
389
+ }
390
+ function getLockfileSharedStateAccessKey(state) {
391
+ const root = resolveLockfileSharedState(state);
392
+ let identity = getWeakMapValue(lockfileSharedStateAccessIdentities, root);
393
+ if (identity === undefined) {
394
+ identity = nextLockfileSharedStateAccessIdentity++;
395
+ setWeakMapValue(lockfileSharedStateAccessIdentities, root, identity);
396
+ }
397
+ return `lockfile-state:${identity}`;
398
+ }
399
+ function registerLockfileSharedState(stateKey, state) {
400
+ const root = resolveLockfileSharedState(state);
401
+ const current = getMapValue(lockfileSharedStateReferences, stateKey);
402
+ if (current && dereference(current.reference) === root)
403
+ return;
404
+ if (current)
405
+ unregisterFinalizer(lockfileSharedStateRegistry, current.unregisterToken);
406
+ const unregisterToken = objectCreate(null);
407
+ setMapValue(lockfileSharedStateReferences, stateKey, {
408
+ reference: new NativeWeakRef(root),
409
+ unregisterToken,
410
+ });
411
+ registerFinalizer(lockfileSharedStateRegistry, root, { stateKey, unregisterToken }, unregisterToken);
412
+ }
413
+ function getLockfileSharedState(stateKeys) {
414
+ const uniqueStateKeys = [];
415
+ for (let stateKeyIndex = 0; stateKeyIndex < stateKeys.length; stateKeyIndex++) {
416
+ const stateKey = stateKeys[stateKeyIndex];
417
+ let duplicate = false;
418
+ for (let knownKeyIndex = 0; knownKeyIndex < uniqueStateKeys.length; knownKeyIndex++) {
419
+ const knownKey = uniqueStateKeys[knownKeyIndex];
420
+ if (knownKey === stateKey) {
421
+ duplicate = true;
422
+ break;
423
+ }
424
+ }
425
+ if (!duplicate)
426
+ uniqueStateKeys[uniqueStateKeys.length] = stateKey;
427
+ }
428
+ const roots = [];
429
+ for (let stateKeyIndex = 0; stateKeyIndex < uniqueStateKeys.length; stateKeyIndex++) {
430
+ const stateKey = uniqueStateKeys[stateKeyIndex];
431
+ const reference = getMapValue(lockfileSharedStateReferences, stateKey)?.reference;
432
+ const referenced = reference ? dereference(reference) : undefined;
433
+ if (!referenced)
434
+ continue;
435
+ const root = resolveLockfileSharedState(referenced);
436
+ let duplicate = false;
437
+ for (let rootIndex = 0; rootIndex < roots.length; rootIndex++) {
438
+ const knownRoot = roots[rootIndex];
439
+ if (knownRoot === root) {
440
+ duplicate = true;
441
+ break;
442
+ }
443
+ }
444
+ if (!duplicate)
445
+ roots[roots.length] = root;
446
+ }
447
+ const state = roots[0] ?? createLockfileSharedState();
448
+ for (let index = 1; index < roots.length; index++) {
449
+ const other = roots[index];
450
+ other.parent = state;
451
+ state.revision = (state.revision > other.revision ? state.revision : other.revision) + 1n;
452
+ state.lastClearSequence = state.lastClearSequence > other.lastClearSequence
453
+ ? state.lastClearSequence
454
+ : other.lastClearSequence;
455
+ }
456
+ for (let stateKeyIndex = 0; stateKeyIndex < uniqueStateKeys.length; stateKeyIndex++) {
457
+ registerLockfileSharedState(uniqueStateKeys[stateKeyIndex], state);
458
+ }
459
+ return state;
460
+ }
461
+ function getAdapterInstanceIdentity(fs) {
462
+ let identity = getWeakMapValue(adapterIdentityByInstance, fs);
463
+ if (identity === undefined) {
464
+ identity = nextAdapterIdentity++;
465
+ setWeakMapValue(adapterIdentityByInstance, fs, identity);
466
+ }
467
+ return `instance:${identity}`;
468
+ }
469
+ function getLockfileCoordinationDomain(fs, accessKey, hasSharedCoordinationKey, managerDomain) {
470
+ if (hasSharedCoordinationKey) {
471
+ if (managerDomain)
472
+ return managerDomain;
473
+ const current = getMapValue(sharedLockfileCoordinationDomains, accessKey);
474
+ let domain = current ? dereference(current.reference) : undefined;
475
+ if (!domain) {
476
+ if (current) {
477
+ unregisterFinalizer(sharedLockfileCoordinationRegistry, current.unregisterToken);
478
+ }
479
+ domain = objectCreate(null);
480
+ domain.records = new NativeMap();
481
+ const unregisterToken = objectCreate(null);
482
+ setMapValue(sharedLockfileCoordinationDomains, accessKey, {
483
+ reference: new NativeWeakRef(domain),
484
+ unregisterToken,
485
+ });
486
+ registerFinalizer(sharedLockfileCoordinationRegistry, domain, { accessKey, unregisterToken }, unregisterToken);
487
+ }
488
+ return domain;
489
+ }
490
+ let domain = getWeakMapValue(lockfileCoordinationDomainByAdapter, fs);
491
+ if (!domain) {
492
+ domain = objectCreate(null);
493
+ domain.records = new NativeMap();
494
+ setWeakMapValue(lockfileCoordinationDomainByAdapter, fs, domain);
495
+ }
496
+ return domain;
497
+ }
498
+ async function resolveCanonicalLockfilePath(fs, projectDir, timeoutMs = LOCKFILE_CANONICALIZATION_TIMEOUT_MS, state) {
499
+ if (!fs.realPath)
500
+ return undefined;
501
+ if (state?.completed) {
502
+ const canonicalPath = state.completed.canonicalPath;
503
+ state.completed = undefined;
504
+ return canonicalPath;
505
+ }
506
+ let resolution = state?.inFlight;
507
+ if (!resolution) {
508
+ resolution = thenPromise(chainPromise(resolveVoidPromise(), () => fs.realPath(projectDir)), (canonicalProjectDir) => normalize(`${normalize(canonicalProjectDir)}/${LOCKFILE_NAME}`));
509
+ if (state) {
510
+ state.inFlight = resolution;
511
+ void thenPromise(resolution, (canonicalPath) => {
512
+ if (state.inFlight !== resolution)
513
+ return;
514
+ state.inFlight = undefined;
515
+ state.completed = { promise: resolution, canonicalPath };
516
+ }, () => {
517
+ if (state.inFlight === resolution)
518
+ state.inFlight = undefined;
519
+ });
520
+ }
521
+ }
522
+ const timedOut = Symbol("lockfile canonicalization timed out");
523
+ const race = racePromiseWithTimeout(resolution, timedOut, timeoutMs);
524
+ try {
525
+ const canonicalProjectDirResult = await race.promise;
526
+ if (canonicalProjectDirResult === timedOut)
527
+ return undefined;
528
+ if (state?.completed?.promise === resolution)
529
+ state.completed = undefined;
530
+ return canonicalProjectDirResult;
531
+ }
532
+ catch {
533
+ return undefined;
534
+ }
535
+ finally {
536
+ race.cancelTimeout();
537
+ }
538
+ }
539
+ function pruneCollectedCoordinationRecords(domain) {
540
+ forEachMapEntry(domain.records, (recordKey, record) => {
541
+ const liveState = record.state ? dereference(record.state) : undefined;
542
+ if (!record.retainedState && liveState === undefined) {
543
+ deleteMapValue(domain.records, recordKey);
544
+ }
545
+ });
546
+ }
547
+ function getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey) {
548
+ const existing = getMapValue(domain.records, logicalStateKey);
549
+ if (existing)
550
+ return existing;
551
+ if (getMapSize(domain.records) >= MAX_UNRESOLVED_LOCKFILE_RECORDS) {
552
+ pruneCollectedCoordinationRecords(domain);
553
+ }
554
+ if (getMapSize(domain.records) >= MAX_UNRESOLVED_LOCKFILE_RECORDS) {
555
+ throw lockfileReadError(lockfilePath, "access-failed", new NativeError(`The shared lockfile coordinator reached its limit of ` +
556
+ `${MAX_UNRESOLVED_LOCKFILE_RECORDS} unresolved project paths`));
557
+ }
558
+ const record = objectCreate(null);
559
+ record.recordKey = logicalStateKey;
560
+ record.projectDir = projectDir;
561
+ record.logicalStateKey = logicalStateKey;
562
+ return record;
563
+ }
564
+ async function reconcileHistoricalCoordinationRecords(fs, domain, currentRecordKey, accessKey) {
565
+ const candidates = [];
566
+ const now = dateNow();
567
+ // Durable mutations take precedence over warmed-reader hints. A second pass
568
+ // fills the remaining bounded batch with live weak records.
569
+ for (let durablePass = 0; durablePass < 2; durablePass++) {
570
+ const durableOnly = durablePass === 0;
571
+ forEachMapEntry(domain.records, (knownRecordKey, knownRecord) => {
572
+ if (knownRecordKey === currentRecordKey)
573
+ return;
574
+ if (candidates.length >= MAX_HISTORICAL_RECONCILIATIONS_PER_ACCESS)
575
+ return false;
576
+ if (durableOnly !== (knownRecord.retainedState !== undefined))
577
+ return;
578
+ if ((knownRecord.historicalAttempts ?? 0) >=
579
+ MAX_HISTORICAL_CANONICALIZATION_ATTEMPTS ||
580
+ (knownRecord.historicalRetryAfter ?? 0) > now) {
581
+ return;
582
+ }
583
+ const knownState = knownRecord.retainedState ??
584
+ (knownRecord.state ? dereference(knownRecord.state) : undefined);
585
+ if (!knownState) {
586
+ deleteMapValue(domain.records, knownRecordKey);
587
+ return;
588
+ }
589
+ candidates[candidates.length] = {
590
+ recordKey: knownRecordKey,
591
+ record: knownRecord,
592
+ state: knownState,
593
+ };
594
+ });
595
+ }
596
+ // Resolve one bounded batch concurrently, so N stale paths cost at most the
597
+ // historical timeout once rather than N times while the shared queue waits.
598
+ const canonicalizationPromises = [];
599
+ for (let index = 0; index < candidates.length; index++) {
600
+ const candidate = candidates[index];
601
+ let canonicalization = candidate.record.historicalCanonicalization;
602
+ if (!canonicalization) {
603
+ canonicalization = resolveCanonicalLockfilePath(fs, candidate.record.projectDir, HISTORICAL_LOCKFILE_CANONICALIZATION_TIMEOUT_MS);
604
+ candidate.record.historicalCanonicalization = canonicalization;
605
+ candidate.ownsCanonicalization = true;
606
+ }
607
+ candidate.canonicalization = canonicalization;
608
+ canonicalizationPromises[index] = canonicalization;
609
+ }
610
+ const canonicalPaths = await allPromises(canonicalizationPromises);
611
+ for (let index = 0; index < candidates.length; index++) {
612
+ const candidate = candidates[index];
613
+ // Concurrent healthy projects share the same bounded historical lookup.
614
+ // Only its owner mutates retry/state bookkeeping once it settles.
615
+ if (!candidate.ownsCanonicalization)
616
+ continue;
617
+ if (candidate.record.historicalCanonicalization === candidate.canonicalization) {
618
+ candidate.record.historicalCanonicalization = undefined;
619
+ }
620
+ const knownCanonicalPath = canonicalPaths[index];
621
+ if (knownCanonicalPath === undefined) {
622
+ const attempts = (candidate.record.historicalAttempts ?? 0) + 1;
623
+ candidate.record.historicalAttempts = attempts;
624
+ const retryDelay = HISTORICAL_CANONICALIZATION_RETRY_BASE_MS *
625
+ 2 ** (attempts - 1);
626
+ candidate.record.historicalRetryAfter = now +
627
+ (retryDelay < HISTORICAL_CANONICALIZATION_RETRY_MAX_MS
628
+ ? retryDelay
629
+ : HISTORICAL_CANONICALIZATION_RETRY_MAX_MS);
630
+ // Keep failures retryable without letting a permanently missing project
631
+ // starve records inserted later in the bounded journal. Attempts are
632
+ // finite here; the record's own manager can still retry its canonical
633
+ // identity on every operation and reconcile it without this path.
634
+ deleteMapValue(domain.records, candidate.recordKey);
635
+ setMapValue(domain.records, candidate.recordKey, candidate.record);
636
+ continue;
637
+ }
638
+ deleteMapValue(domain.records, candidate.recordKey);
639
+ getLockfileSharedState([
640
+ candidate.record.logicalStateKey,
641
+ apply(jsonStringify, NativeJSON, [[accessKey, knownCanonicalPath]]),
642
+ ]);
643
+ }
644
+ return candidates.length > 0;
645
+ }
646
+ async function resolveLockfileCoordinationIdentity(fs, projectDir, lockfilePath, accessKey, hasSharedCoordinationKey, canonicalizationState, managerDomain) {
647
+ // A declared coordination key is the complete shared lock domain. Including
648
+ // a canonical path here would split adapters that share a backing store when
649
+ // only some of them implement realPath, recreating a read-merge-write race.
650
+ const logicalStateKey = apply(jsonStringify, NativeJSON, [[accessKey, lockfilePath]]);
651
+ const domain = getLockfileCoordinationDomain(fs, accessKey, hasSharedCoordinationKey, managerDomain);
652
+ if (!fs.realPath) {
653
+ if (!hasSharedCoordinationKey) {
654
+ return { coordinationDomain: domain, stateKeys: [logicalStateKey] };
655
+ }
656
+ const unresolvedRecord = getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey);
657
+ return {
658
+ coordinationDomain: domain,
659
+ stateKeys: [logicalStateKey],
660
+ unresolvedRecord,
661
+ };
662
+ }
663
+ const recordKey = logicalStateKey;
664
+ const canonicalLockfilePath = await resolveCanonicalLockfilePath(fs, projectDir, LOCKFILE_CANONICALIZATION_TIMEOUT_MS, canonicalizationState);
665
+ if (canonicalLockfilePath === undefined) {
666
+ const unresolvedRecord = getUnresolvedCoordinationRecord(domain, projectDir, lockfilePath, logicalStateKey);
667
+ return {
668
+ coordinationDomain: domain,
669
+ stateKeys: [logicalStateKey],
670
+ unresolvedRecord,
671
+ };
672
+ }
673
+ // Historical aliases are reconciled incrementally. A stale or deleted
674
+ // project remains retryable but cannot fail an unrelated healthy project or
675
+ // monopolize the shared backing-store queue.
676
+ const reconciledUnresolvedRecords = await reconcileHistoricalCoordinationRecords(fs, domain, recordKey, accessKey);
677
+ return {
678
+ coordinationDomain: domain,
679
+ resolvedRecordKey: recordKey,
680
+ reconciledUnresolvedRecords,
681
+ stateKeys: [
682
+ logicalStateKey,
683
+ apply(jsonStringify, NativeJSON, [[accessKey, canonicalLockfilePath]]),
684
+ ],
685
+ };
686
+ }
687
+ function compareLockfileImportKeys(left, right) {
688
+ return left < right ? -1 : left > right ? 1 : 0;
689
+ }
690
+ function serializeLockfileAccess(accessKey, operation) {
691
+ const predecessor = getMapValue(lockfileAccessTails, accessKey) ?? resolveVoidPromise();
692
+ const result = chainPromise(predecessor, operation);
693
+ const tail = thenPromise(result, () => undefined, () => undefined);
694
+ setMapValue(lockfileAccessTails, accessKey, tail);
695
+ const release = () => {
696
+ if (getMapValue(lockfileAccessTails, accessKey) === tail) {
697
+ deleteMapValue(lockfileAccessTails, accessKey);
698
+ }
699
+ };
700
+ return thenPromise(result, (value) => {
701
+ release();
702
+ return value;
703
+ }, (error) => {
704
+ release();
705
+ throw error;
706
+ });
707
+ }
20
708
  function createPlatformFSAdapter() {
21
709
  const fs = createFileSystem();
710
+ const renameFile = fs.rename;
22
711
  return {
712
+ coordinationKey: PLATFORM_FS_COORDINATION_KEY,
23
713
  readFile(path) {
24
714
  return fs.readTextFile(path);
25
715
  },
26
716
  writeFile(path, content) {
27
717
  return fs.writeTextFile(path, content);
28
718
  },
719
+ ...(renameFile === undefined ? {} : {
720
+ rename(from, to) {
721
+ return apply(renameFile, fs, [from, to]);
722
+ },
723
+ }),
29
724
  exists(path) {
30
725
  return fs.exists(path);
31
726
  },
32
727
  remove(path) {
33
728
  return fs.remove(path);
34
729
  },
730
+ realPath(path) {
731
+ return resolvePlatformRealPath(path);
732
+ },
35
733
  };
36
734
  }
37
735
  /** Create lockfile manager. */
38
736
  export function createLockfileManager(projectDir, fsAdapter) {
39
737
  const fs = fsAdapter ?? createPlatformFSAdapter();
40
- const lockfilePath = `${projectDir}/${LOCKFILE_NAME}`;
738
+ const normalizedProjectDir = normalize(projectDir);
739
+ const lockfilePath = normalize(`${normalizedProjectDir}/${LOCKFILE_NAME}`);
740
+ const adapterIdentity = getAdapterInstanceIdentity(fs);
741
+ const coordinationKey = fs.coordinationKey;
742
+ const hasSharedCoordinationKey = coordinationKey !== undefined;
743
+ const accessKey = hasSharedCoordinationKey
744
+ ? apply(jsonStringify, NativeJSON, [[`shared:${coordinationKey}`]])
745
+ : apply(jsonStringify, NativeJSON, [[adapterIdentity]]);
746
+ const logicalQueueKey = hasSharedCoordinationKey
747
+ ? apply(jsonStringify, NativeJSON, [[accessKey, lockfilePath]])
748
+ : accessKey;
41
749
  let cache = null;
42
- let dirty = false;
43
- async function read() {
44
- if (cache)
45
- return cache;
750
+ let cacheRevision = -1n;
751
+ let managerOperationTail = resolveVoidPromise();
752
+ let managerCoordinationDomain;
753
+ let managerUnresolvedRecord;
754
+ let managerSharedState;
755
+ const canonicalizationState = objectCreate(null);
756
+ const pendingEntries = new NativeMap();
757
+ function serializeManagerOperation(operation) {
758
+ const result = chainPromise(managerOperationTail, operation);
759
+ managerOperationTail = thenPromise(result, () => undefined, () => undefined);
760
+ return result;
761
+ }
762
+ async function resolveStateUnderAccess() {
763
+ // Retry canonicalization for every serialized manager operation. A
764
+ // project path may not exist on the first access, and a later successful
765
+ // resolution must bridge the earlier logical state to canonical aliases.
766
+ const { coordinationDomain, stateKeys, unresolvedRecord, resolvedRecordKey, reconciledUnresolvedRecords, } = await resolveLockfileCoordinationIdentity(fs, normalizedProjectDir, lockfilePath, accessKey, hasSharedCoordinationKey, canonicalizationState, managerCoordinationDomain);
767
+ managerCoordinationDomain = coordinationDomain;
768
+ // A coordinationKey declares a shared backing store. Canonicalize the
769
+ // per-lockfile cache state independently so aliases invalidate each other
770
+ // without coupling separate projects.
771
+ managerSharedState = getLockfileSharedState(stateKeys);
772
+ if (resolvedRecordKey !== undefined) {
773
+ deleteMapValue(coordinationDomain.records, resolvedRecordKey);
774
+ }
775
+ managerUnresolvedRecord = unresolvedRecord;
776
+ if (unresolvedRecord) {
777
+ // A live warmed reader must be discoverable by a healthy alias so a
778
+ // later clear can invalidate its cache. Keep only the state weakly: dead
779
+ // readers and their adapters remain collectible.
780
+ unresolvedRecord.state = new NativeWeakRef(managerSharedState);
781
+ setMapValue(coordinationDomain.records, unresolvedRecord.recordKey, unresolvedRecord);
782
+ }
783
+ const state = resolveLockfileSharedState(managerSharedState);
784
+ return {
785
+ state,
786
+ // Queue by the reconciled state root rather than the manager's current
787
+ // spelling of the path. A shared adapter with realPath can bridge an
788
+ // alias owned by an adapter without realPath; both must then serialize
789
+ // through the same queue to protect the read-merge-write window.
790
+ accessQueueKey: getLockfileSharedStateAccessKey(state),
791
+ unresolved: unresolvedRecord !== undefined || reconciledUnresolvedRecords === true,
792
+ };
793
+ }
794
+ function retainUnresolvedMutation(state, durable) {
795
+ if (!managerCoordinationDomain || !managerUnresolvedRecord)
796
+ return;
797
+ managerUnresolvedRecord.state = new NativeWeakRef(state);
798
+ if (durable)
799
+ managerUnresolvedRecord.retainedState = state;
800
+ setMapValue(managerCoordinationDomain.records, managerUnresolvedRecord.recordKey, managerUnresolvedRecord);
801
+ }
802
+ function withLockfileAccess(operation) {
803
+ return serializeLockfileAccess(logicalQueueKey, () => chainPromise(resolveStateUnderAccess(), ({ state, accessQueueKey, unresolved }) => {
804
+ const resolvedAccessQueueKey = hasSharedCoordinationKey && unresolved
805
+ ? accessKey
806
+ : accessQueueKey;
807
+ return resolvedAccessQueueKey === logicalQueueKey
808
+ ? operation(state)
809
+ : serializeLockfileAccess(resolvedAccessQueueKey, () => operation(state));
810
+ }));
811
+ }
812
+ async function lockfileExists() {
46
813
  try {
47
- const exists = await fs.exists(lockfilePath);
48
- if (!exists)
49
- return null;
50
- const content = await fs.readFile(lockfilePath);
51
- const parsed = JSON.parse(content);
52
- if (parsed.version !== LOCKFILE_VERSION) {
53
- logger.warn(`[lockfile] Version mismatch, expected ${LOCKFILE_VERSION}, got ${parsed.version}`);
54
- cache = createEmptyLockfile();
55
- return cache;
56
- }
57
- cache = parsed;
58
- return cache;
814
+ return await fs.exists(lockfilePath);
815
+ }
816
+ catch (cause) {
817
+ throw lockfileReadError(lockfilePath, "access-failed", cause);
59
818
  }
60
- catch (e) {
61
- logger.debug(`Could not read lockfile: ${e}`);
819
+ }
820
+ async function readFromDisk() {
821
+ if (!(await lockfileExists()))
62
822
  return null;
823
+ let content;
824
+ try {
825
+ content = await fs.readFile(lockfilePath);
63
826
  }
827
+ catch (cause) {
828
+ throw lockfileReadError(lockfilePath, "access-failed", cause);
829
+ }
830
+ return parseLockfile(content, lockfilePath);
64
831
  }
65
- async function write(data) {
66
- cache = data;
832
+ async function readCurrentUnderAccess(state) {
833
+ forEachMapEntry(pendingEntries, (url, pending) => {
834
+ if (pending.sequence < state.lastClearSequence)
835
+ deleteMapValue(pendingEntries, url);
836
+ });
837
+ if (cacheRevision === state.revision)
838
+ return cache;
839
+ const data = await readFromDisk();
840
+ if (getMapSize(pendingEntries) === 0) {
841
+ cache = data;
842
+ }
843
+ else {
844
+ const resolvedCache = data ?? createInternalLockfile();
845
+ cache = resolvedCache;
846
+ forEachMapEntry(pendingEntries, (url, pending) => {
847
+ defineImportEntry(resolvedCache.imports, url, cloneLockfileEntry(pending.entry));
848
+ });
849
+ }
850
+ cacheRevision = state.revision;
851
+ return cache;
852
+ }
853
+ function readCurrent() {
854
+ // Cold and invalidated reads share the same access turn as write/remove
855
+ // operations, so peer managers cannot observe a partially replaced file.
856
+ return withLockfileAccess((state) => readCurrentUnderAccess(state));
857
+ }
858
+ function read() {
859
+ return serializeManagerOperation(async () => {
860
+ const data = await readCurrent();
861
+ return data === null ? null : cloneLockfileData(data, "public");
862
+ });
863
+ }
864
+ async function replaceLockfileAtomically(rename, serialized) {
865
+ // Stage the new content next to the lockfile and rename it into place, so
866
+ // readers observe either the previous or the new complete file and a
867
+ // crash mid-write can never leave a truncated lockfile behind.
868
+ const tempPath = `${lockfilePath}.${generateUuid()}.tmp`;
869
+ try {
870
+ await fs.writeFile(tempPath, serialized);
871
+ await rename(tempPath, lockfilePath);
872
+ }
873
+ catch (error) {
874
+ if (fs.remove) {
875
+ try {
876
+ await fs.remove(tempPath);
877
+ }
878
+ catch {
879
+ // Best-effort cleanup only; the original write/rename failure wins.
880
+ }
881
+ }
882
+ throw error;
883
+ }
884
+ }
885
+ async function writeToDisk(data) {
886
+ const sourceEntries = objectEntries(data.imports);
887
+ apply(arraySort, sourceEntries, [
888
+ (left, right) => compareLockfileImportKeys(left[0], right[0]),
889
+ ]);
890
+ const sortedEntries = [];
891
+ sortedEntries.length = sourceEntries.length;
892
+ for (let index = 0; index < sourceEntries.length; index++) {
893
+ const sourceEntry = sourceEntries[index];
894
+ sortedEntries[index] = [sourceEntry[0], cloneLockfileEntry(sourceEntry[1])];
895
+ }
67
896
  const sorted = {
68
- version: data.version,
69
- imports: Object.fromEntries(Object.entries(data.imports).sort(([a], [b]) => a.localeCompare(b))),
897
+ version: LOCKFILE_VERSION,
898
+ imports: createImportDictionary(sortedEntries),
70
899
  };
71
- await fs.writeFile(lockfilePath, `${JSON.stringify(sorted, null, 2)}\n`);
72
- dirty = false;
73
- logger.debug(`Written ${Object.keys(data.imports).length} entries`);
74
- }
75
- async function get(url) {
76
- const data = await read();
77
- return data?.imports[url] ?? null;
78
- }
79
- async function set(url, entry) {
80
- const data = (await read()) ?? createEmptyLockfile();
81
- data.imports[url] = entry;
82
- cache = data;
83
- dirty = true;
84
- }
85
- async function has(url) {
86
- const data = await read();
87
- return url in (data?.imports ?? {});
88
- }
89
- async function clear() {
90
- cache = createEmptyLockfile();
91
- dirty = false;
92
- if (!fs.remove)
93
- return;
94
- const exists = await fs.exists(lockfilePath);
95
- if (!exists)
96
- return;
97
- await fs.remove(lockfilePath);
900
+ const serialized = `${apply(jsonStringify, NativeJSON, [sorted, null, 2])}\n`;
901
+ const renameFile = fs.rename;
902
+ if (renameFile) {
903
+ await replaceLockfileAtomically((from, to) => apply(renameFile, fs, [from, to]), serialized);
904
+ }
905
+ else {
906
+ // Adapters without rename cannot replace the file atomically; fall back
907
+ // to an in-place write and accept the torn-write risk for them.
908
+ await fs.writeFile(lockfilePath, serialized);
909
+ }
910
+ logger.debug(`Written ${objectKeys(data.imports).length} entries`);
98
911
  }
99
- async function flush() {
100
- if (!dirty || !cache)
101
- return;
102
- await write(cache);
912
+ function write(data) {
913
+ return serializeManagerOperation(() => {
914
+ const snapshot = sanitizeLockfileData(data, lockfilePath, () => lockfileInputError("invalid-structure"));
915
+ return withLockfileAccess(async (state) => {
916
+ // Revalidate the existing file before replacing it. Unsupported,
917
+ // unreadable, and malformed files are always preserved.
918
+ await readFromDisk();
919
+ await writeToDisk(snapshot);
920
+ cache = snapshot;
921
+ clearMap(pendingEntries);
922
+ cacheRevision = ++state.revision;
923
+ retainUnresolvedMutation(state, true);
924
+ });
925
+ });
926
+ }
927
+ function get(url) {
928
+ return serializeManagerOperation(async () => {
929
+ const entry = (await readCurrent())?.imports[url];
930
+ return entry === undefined ? null : cloneLockfileEntry(entry);
931
+ });
932
+ }
933
+ function set(url, entry) {
934
+ let snapshot;
935
+ try {
936
+ snapshot = cloneLockfileEntry(entry);
937
+ }
938
+ catch (error) {
939
+ return rejectPromise(error);
940
+ }
941
+ return serializeManagerOperation(() => withLockfileAccess(async (state) => {
942
+ const data = (await readCurrentUnderAccess(state)) ?? createInternalLockfile();
943
+ defineImportEntry(data.imports, url, snapshot);
944
+ cache = data;
945
+ setMapValue(pendingEntries, url, {
946
+ entry: snapshot,
947
+ sequence: nextLockfileMutationSequence++,
948
+ });
949
+ retainUnresolvedMutation(state, false);
950
+ }));
951
+ }
952
+ function has(url) {
953
+ return serializeManagerOperation(async () => {
954
+ const data = await readCurrent();
955
+ return data !== null && objectHasOwn(data.imports, url);
956
+ });
957
+ }
958
+ function clear() {
959
+ return serializeManagerOperation(() => withLockfileAccess(async (state) => {
960
+ // Clear is an explicit destructive recovery path. Check access to the
961
+ // path, but do not parse content that the user has chosen to discard.
962
+ const existing = await lockfileExists();
963
+ let cleared = null;
964
+ if (existing) {
965
+ if (fs.remove)
966
+ await fs.remove(lockfilePath);
967
+ else {
968
+ cleared = createInternalLockfile();
969
+ await writeToDisk(cleared);
970
+ }
971
+ }
972
+ // State changes only after the access check and any requested deletion
973
+ // or fallback write have succeeded, so a failed clear leaves memory and
974
+ // disk untouched.
975
+ cache = cleared;
976
+ clearMap(pendingEntries);
977
+ state.lastClearSequence = nextLockfileMutationSequence++;
978
+ cacheRevision = ++state.revision;
979
+ retainUnresolvedMutation(state, true);
980
+ }));
981
+ }
982
+ function flush() {
983
+ return serializeManagerOperation(async () => {
984
+ if (getMapSize(pendingEntries) === 0)
985
+ return;
986
+ await withLockfileAccess(async (state) => {
987
+ forEachMapEntry(pendingEntries, (url, pending) => {
988
+ if (pending.sequence < state.lastClearSequence) {
989
+ deleteMapValue(pendingEntries, url);
990
+ }
991
+ });
992
+ if (getMapSize(pendingEntries) === 0) {
993
+ cache = await readFromDisk();
994
+ cacheRevision = state.revision;
995
+ return;
996
+ }
997
+ // Merge only this manager's pending entries onto the latest on-disk
998
+ // state while holding the canonical backing-store access turn. This
999
+ // makes the whole read-merge-write sequence atomic relative to other
1000
+ // in-process managers, including managers using path aliases.
1001
+ const snapshot = new NativeMap();
1002
+ forEachMapEntry(pendingEntries, (url, pending) => {
1003
+ setMapValue(snapshot, url, pending);
1004
+ });
1005
+ const merged = (await readFromDisk()) ?? createInternalLockfile();
1006
+ forEachMapEntry(snapshot, (url, pending) => {
1007
+ defineImportEntry(merged.imports, url, cloneLockfileEntry(pending.entry));
1008
+ });
1009
+ await writeToDisk(merged);
1010
+ forEachMapEntry(snapshot, (url, pending) => {
1011
+ if (getMapValue(pendingEntries, url) === pending) {
1012
+ deleteMapValue(pendingEntries, url);
1013
+ }
1014
+ });
1015
+ forEachMapEntry(pendingEntries, (url, pending) => {
1016
+ defineImportEntry(merged.imports, url, cloneLockfileEntry(pending.entry));
1017
+ });
1018
+ cache = merged;
1019
+ cacheRevision = ++state.revision;
1020
+ retainUnresolvedMutation(state, true);
1021
+ });
1022
+ });
103
1023
  }
104
1024
  return { read, write, get, set, has, clear, flush };
105
1025
  }
1026
+ const lockfileReadWarningIssued = new NativeWeakMap();
1027
+ /**
1028
+ * Degrade an unreadable-lockfile failure (`lockfile-read-error`) on a build
1029
+ * hot path: warn once per manager, naming the file and the
1030
+ * `veryfront lock --clear` remedy, and report the error as absorbed. Every
1031
+ * other failure — most importantly `lockfile-format-mismatch`, a valid
1032
+ * lockfile written by a newer Veryfront build — is reported as not absorbed
1033
+ * so it keeps failing loudly.
1034
+ */
1035
+ function absorbUnreadableLockfileError(lockfile, error) {
1036
+ const snapshot = snapshotVeryfrontError(error);
1037
+ if (snapshot?.slug !== LOCKFILE_READ_ERROR.slug)
1038
+ return false;
1039
+ if (getWeakMapValue(lockfileReadWarningIssued, lockfile) !== true) {
1040
+ setWeakMapValue(lockfileReadWarningIssued, lockfile, true);
1041
+ logger.warn(`Lockfile ${LOCKFILE_NAME} could not be read (${snapshot.message}); ` +
1042
+ "continuing this build without lockfile entries. " +
1043
+ "Run `veryfront lock --clear` to reset the unreadable or invalid lockfile.");
1044
+ }
1045
+ return true;
1046
+ }
1047
+ /**
1048
+ * Read a lockfile entry on a build or dev-server hot path.
1049
+ *
1050
+ * A lockfile written by a newer Veryfront build (`lockfile-format-mismatch`)
1051
+ * keeps failing loudly: proceeding around data the running build cannot
1052
+ * understand is exactly the overwrite hazard this module guards against. An
1053
+ * unreadable or malformed lockfile (`lockfile-read-error`) instead degrades to
1054
+ * a cache miss so builds keep working from fresh fetches; a warning naming
1055
+ * the lockfile and the `veryfront lock --clear` remedy is logged once per
1056
+ * manager instead of once per import.
1057
+ */
1058
+ export async function getLockfileEntryForBuild(lockfile, url) {
1059
+ try {
1060
+ return await lockfile.get(url);
1061
+ }
1062
+ catch (error) {
1063
+ if (!absorbUnreadableLockfileError(lockfile, error))
1064
+ throw error;
1065
+ return null;
1066
+ }
1067
+ }
1068
+ /**
1069
+ * Persist a lockfile entry from a build or dev-server hot path.
1070
+ *
1071
+ * Mirrors {@link getLockfileEntryForBuild}: when the on-disk lockfile is
1072
+ * unreadable or malformed (`lockfile-read-error`), the file is left untouched
1073
+ * for `veryfront lock --clear` and the entry is simply not recorded, so a
1074
+ * successful refetch still serves the build. Returns whether the entry was
1075
+ * staged; callers should only `flush()` after a `true` result. A newer-format
1076
+ * lockfile (`lockfile-format-mismatch`) keeps failing loudly.
1077
+ */
1078
+ export async function setLockfileEntryForBuild(lockfile, url, entry) {
1079
+ try {
1080
+ await lockfile.set(url, entry);
1081
+ return true;
1082
+ }
1083
+ catch (error) {
1084
+ if (!absorbUnreadableLockfileError(lockfile, error))
1085
+ throw error;
1086
+ return false;
1087
+ }
1088
+ }
106
1089
  const USER_AGENT_HEADERS = { "user-agent": "Mozilla/5.0 Veryfront/1.0" };
107
1090
  export async function fetchWithLock(options) {
108
1091
  const { lockfile, url, fetchFn = fetch, strict = false } = options;
@@ -178,7 +1161,8 @@ export function resolveImportUrl(specifier, baseUrl) {
178
1161
  if (!specifier.startsWith("./") && !specifier.startsWith("../"))
179
1162
  return null;
180
1163
  try {
181
- return new URL(specifier, baseUrl).toString();
1164
+ const resolved = new NativeURL(specifier, baseUrl);
1165
+ return apply(urlToString, resolved, []);
182
1166
  }
183
1167
  catch (_) {
184
1168
  /* expected: specifier may not be a valid relative URL */