@particle-academy/prism-harness 0.9.1 → 0.9.2
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 +7 -0
- package/dist/stores/file-session-store.js +37 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -393,6 +393,13 @@ badly.
|
|
|
393
393
|
native boot/PID realm and a proven dead or explicitly released owner. Local
|
|
394
394
|
filesystems on one host only; SMB/NFS are unsupported.
|
|
395
395
|
|
|
396
|
+
State readers close their handles before decoding the document. On Windows,
|
|
397
|
+
replacement of a state file can fail while another process holds it open,
|
|
398
|
+
even with delete sharing. Permission failures retry for up to 750 ms. If the
|
|
399
|
+
reader stays open or access is permanently denied, the original native error
|
|
400
|
+
is raised and the temporary file is removed; the previous state stays intact.
|
|
401
|
+
Other errors and POSIX replacement do not retry. Callbacks are never replayed.
|
|
402
|
+
|
|
396
403
|
On Windows, the first file-lock operation makes one fixed, read-only metadata
|
|
397
404
|
query using the system Windows PowerShell and CIM module, with a fifteen-second
|
|
398
405
|
timeout. Established identities and detected container conditions are cached
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdirSync } from 'node:fs';
|
|
3
3
|
import { mkdir, open, readFile, rename, rm, stat, truncate, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
6
|
import { isJsonObject } from '../json.js';
|
|
6
7
|
import { HarnessError } from '../errors.js';
|
|
7
8
|
import { withFileOwnerLock, pendingFileOwnerReleases } from './file-owner-lock.js';
|
|
@@ -52,6 +53,8 @@ export class FileSessionStore {
|
|
|
52
53
|
async get(key) {
|
|
53
54
|
let raw;
|
|
54
55
|
try {
|
|
56
|
+
// Node's whole-file helper closes its own handle on both outcomes before
|
|
57
|
+
// resolving; JSON decoding and expiry housekeeping happen after it closes.
|
|
55
58
|
raw = await readFile(this.#pathFor(key), 'utf8');
|
|
56
59
|
}
|
|
57
60
|
catch (error) {
|
|
@@ -91,7 +94,7 @@ export class FileSessionStore {
|
|
|
91
94
|
try {
|
|
92
95
|
// Atomic on both POSIX and Windows. A reader either sees the whole old
|
|
93
96
|
// payload or the whole new one, never half of either.
|
|
94
|
-
await
|
|
97
|
+
await replaceStateFile(temporary, target);
|
|
95
98
|
}
|
|
96
99
|
catch (error) {
|
|
97
100
|
await rm(temporary, { force: true });
|
|
@@ -255,3 +258,36 @@ function digestOf(key) {
|
|
|
255
258
|
function isMissing(error) {
|
|
256
259
|
return typeof error === 'object' && error !== null && error.code === 'ENOENT';
|
|
257
260
|
}
|
|
261
|
+
/** An open Windows target can block replacement even with delete sharing. */
|
|
262
|
+
async function replaceStateFile(temporary, target) {
|
|
263
|
+
try {
|
|
264
|
+
await rename(temporary, target);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
catch (original) {
|
|
268
|
+
if (process.platform !== 'win32' || !isReplacePermissionError(original))
|
|
269
|
+
throw original;
|
|
270
|
+
const deadline = performance.now() + 750;
|
|
271
|
+
while (true) {
|
|
272
|
+
const remaining = deadline - performance.now();
|
|
273
|
+
if (remaining <= 0)
|
|
274
|
+
throw original;
|
|
275
|
+
// This timer stays referenced: an awaited write must keep the process alive.
|
|
276
|
+
await delay(Math.min(10, remaining));
|
|
277
|
+
if (performance.now() >= deadline)
|
|
278
|
+
throw original;
|
|
279
|
+
try {
|
|
280
|
+
await rename(temporary, target);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (!isReplacePermissionError(error))
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function isReplacePermissionError(error) {
|
|
291
|
+
const code = typeof error === 'object' && error !== null ? error.code : undefined;
|
|
292
|
+
return code === 'EPERM' || code === 'EACCES';
|
|
293
|
+
}
|