@wrongstack/persistence 0.295.0
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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/atomic-write.d.ts +43 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +7 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# `@wrongstack/persistence`
|
|
2
|
+
|
|
3
|
+
Dependency-free filesystem persistence primitives shared by WrongStack packages.
|
|
4
|
+
|
|
5
|
+
The package owns atomic replacement, parent-directory creation, cooperative file
|
|
6
|
+
locks, stale-lock recovery, watcher-assisted contention waits, bounded lock
|
|
7
|
+
timeouts, and transient Windows rename retries. It intentionally owns no domain
|
|
8
|
+
repository, serialization format, migration policy, or cloud synchronization.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { atomicWrite, withFileLock } from '@wrongstack/persistence';
|
|
12
|
+
|
|
13
|
+
await withFileLock(file, async () => {
|
|
14
|
+
await atomicWrite(file, JSON.stringify(value));
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The default timeout error is `PersistenceFsError`. A host with an established
|
|
19
|
+
error hierarchy can call `createPersistencePrimitives` and inject only the
|
|
20
|
+
timeout-error factory. This keeps dependency direction toward the primitive
|
|
21
|
+
while preserving host compatibility contracts.
|
|
22
|
+
|
|
23
|
+
Core and Kanban retain thin adapters during migration. Their behavior is locked
|
|
24
|
+
to this package by one shared adversarial conformance suite.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AtomicWriteOptions {
|
|
2
|
+
mode?: number | undefined;
|
|
3
|
+
encoding?: BufferEncoding | undefined;
|
|
4
|
+
}
|
|
5
|
+
export interface FileLockOptions {
|
|
6
|
+
timeoutMs?: number | undefined;
|
|
7
|
+
staleMs?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface FileLockTimeoutDetails {
|
|
10
|
+
targetPath: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PersistencePrimitiveOptions {
|
|
14
|
+
createLockTimeoutError?: ((details: FileLockTimeoutDetails) => Error) | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface PersistencePrimitives {
|
|
17
|
+
atomicWrite(targetPath: string, content: string | Uint8Array, opts?: AtomicWriteOptions): Promise<void>;
|
|
18
|
+
ensureDir(dir: string): Promise<void>;
|
|
19
|
+
withFileLock<T>(targetPath: string, fn: () => Promise<T>, opts?: FileLockOptions): Promise<T>;
|
|
20
|
+
}
|
|
21
|
+
/** A dependency-free structured error for persistence boundary failures. */
|
|
22
|
+
export declare class PersistenceFsError extends Error {
|
|
23
|
+
name: string;
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly path?: string | undefined;
|
|
26
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
27
|
+
constructor(opts: {
|
|
28
|
+
message: string;
|
|
29
|
+
code: string;
|
|
30
|
+
path?: string | undefined;
|
|
31
|
+
context?: Record<string, unknown> | undefined;
|
|
32
|
+
cause?: unknown | undefined;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an isolated primitive set. Hosts may inject their own structured
|
|
37
|
+
* timeout error without making this low-level package depend on that host.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createPersistencePrimitives(options?: PersistencePrimitiveOptions): PersistencePrimitives;
|
|
40
|
+
export declare const atomicWrite: (targetPath: string, content: string | Uint8Array, opts?: AtomicWriteOptions) => Promise<void>;
|
|
41
|
+
export declare const ensureDir: (dir: string) => Promise<void>;
|
|
42
|
+
export declare const withFileLock: <T>(targetPath: string, fn: () => Promise<T>, opts?: FileLockOptions) => Promise<T>;
|
|
43
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,KAAK,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/F;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,SAAa;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAEvD,YAAY,IAAI,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC9C,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;KAC7B,EAKA;CACF;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,2BAAgC,GACxC,qBAAqB,CAyHvB;AAgED,eAAO,MAAM,WAAW,eA5NR,MAAM,WACT,MAAM,GAAG,UAAU,SACrB,kBAAkB,KACxB,OAAO,CAAC,IAAI,CAyNuC,CAAC;AACzD,eAAO,MAAM,SAAS,QAzNL,MAAM,KAAG,OAAO,CAAC,IAAI,CAyNc,CAAC;AACrD,eAAO,MAAM,YAAY,GAzNV,CAAC,cAAc,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,SAAS,eAAe,KAAG,OAAO,CAAC,CAAC,CAyNpC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/atomic-write.ts
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { watch as watchDir } from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
var PersistenceFsError = class extends Error {
|
|
7
|
+
name = "FsError";
|
|
8
|
+
code;
|
|
9
|
+
path;
|
|
10
|
+
context;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
super(opts.message, { cause: opts.cause });
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.path = opts.path;
|
|
15
|
+
this.context = opts.context;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function createPersistencePrimitives(options = {}) {
|
|
19
|
+
const createLockTimeoutError = options.createLockTimeoutError ?? (({ targetPath, timeoutMs }) => new PersistenceFsError({
|
|
20
|
+
message: `Timed out waiting for file lock: ${targetPath}`,
|
|
21
|
+
code: "FS_ATOMIC_WRITE_FAILED",
|
|
22
|
+
path: targetPath,
|
|
23
|
+
context: { timeoutMs }
|
|
24
|
+
}));
|
|
25
|
+
async function atomicWrite2(targetPath, content, opts = {}) {
|
|
26
|
+
const dir = path.dirname(targetPath);
|
|
27
|
+
await fs.mkdir(dir, { recursive: true });
|
|
28
|
+
const tmp = path.join(
|
|
29
|
+
dir,
|
|
30
|
+
`.${path.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`
|
|
31
|
+
);
|
|
32
|
+
try {
|
|
33
|
+
if (typeof content === "string") {
|
|
34
|
+
await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
35
|
+
} else {
|
|
36
|
+
await fs.writeFile(tmp, content, { flag: "wx" });
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const fileHandle = await fs.open(tmp, "r+");
|
|
40
|
+
try {
|
|
41
|
+
await fileHandle.sync();
|
|
42
|
+
} finally {
|
|
43
|
+
await fileHandle.close();
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
let mode;
|
|
48
|
+
try {
|
|
49
|
+
const stat2 = await fs.stat(targetPath);
|
|
50
|
+
mode = stat2.mode & 511;
|
|
51
|
+
} catch {
|
|
52
|
+
mode = opts.mode;
|
|
53
|
+
}
|
|
54
|
+
if (mode !== void 0) await fs.chmod(tmp, mode);
|
|
55
|
+
await renameWithRetry(tmp, targetPath);
|
|
56
|
+
if (mode !== void 0 && process.platform === "win32") {
|
|
57
|
+
await fs.chmod(targetPath, mode).catch(() => void 0);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
await fs.unlink(tmp).catch(() => void 0);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function ensureDir2(dir) {
|
|
65
|
+
await fs.mkdir(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
async function withFileLock2(targetPath, fn, opts = {}) {
|
|
68
|
+
const dir = path.dirname(targetPath);
|
|
69
|
+
const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? 15e3;
|
|
71
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
72
|
+
const started = Date.now();
|
|
73
|
+
let handle;
|
|
74
|
+
for (; ; ) {
|
|
75
|
+
try {
|
|
76
|
+
handle = await fs.open(lockPath, "wx");
|
|
77
|
+
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
78
|
+
break;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (handle) {
|
|
81
|
+
await handle.close().catch(() => void 0);
|
|
82
|
+
await fs.unlink(lockPath).catch(() => void 0);
|
|
83
|
+
handle = void 0;
|
|
84
|
+
}
|
|
85
|
+
const code = error.code;
|
|
86
|
+
if (code === "ENOENT") {
|
|
87
|
+
await fs.mkdir(dir, { recursive: true });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (code !== "EEXIST" && code !== "EPERM") throw error;
|
|
91
|
+
try {
|
|
92
|
+
const stat2 = await fs.stat(lockPath);
|
|
93
|
+
if (Date.now() - stat2.mtimeMs > staleMs) {
|
|
94
|
+
await fs.unlink(lockPath);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const elapsed = Date.now() - started;
|
|
101
|
+
if (elapsed >= timeoutMs) {
|
|
102
|
+
throw createLockTimeoutError({ targetPath, timeoutMs });
|
|
103
|
+
}
|
|
104
|
+
await waitForLockRelease(lockPath, timeoutMs - elapsed);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await fn();
|
|
109
|
+
} finally {
|
|
110
|
+
await handle?.close().catch(() => void 0);
|
|
111
|
+
await fs.unlink(lockPath).catch(() => void 0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { atomicWrite: atomicWrite2, ensureDir: ensureDir2, withFileLock: withFileLock2 };
|
|
115
|
+
}
|
|
116
|
+
async function waitForLockRelease(lockPath, remainingMs) {
|
|
117
|
+
const parentDir = path.dirname(lockPath);
|
|
118
|
+
const lockName = path.basename(lockPath);
|
|
119
|
+
const intervalMs = Math.min(remainingMs, 100);
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
let settled = false;
|
|
122
|
+
let watcher = null;
|
|
123
|
+
const settle = () => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
watcher?.close();
|
|
127
|
+
resolve();
|
|
128
|
+
};
|
|
129
|
+
const timer = setTimeout(settle, intervalMs);
|
|
130
|
+
try {
|
|
131
|
+
watcher = watchDir(parentDir, (eventType, filename) => {
|
|
132
|
+
if (filename === lockName && (eventType === "rename" || eventType === "change")) {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
settle();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
setTimeout(settle, Math.min(remainingMs, 25));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
void fs.access(lockPath).catch(() => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
settle();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
149
|
+
async function renameWithRetry(from, to) {
|
|
150
|
+
if (process.platform !== "win32") {
|
|
151
|
+
await fs.rename(from, to);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const delays = [10, 25, 60, 120, 250];
|
|
155
|
+
let lastError;
|
|
156
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
157
|
+
try {
|
|
158
|
+
await fs.rename(from, to);
|
|
159
|
+
return;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
lastError = error;
|
|
162
|
+
const code = error.code;
|
|
163
|
+
if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) throw error;
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw lastError;
|
|
168
|
+
}
|
|
169
|
+
var defaultPrimitives = createPersistencePrimitives();
|
|
170
|
+
var atomicWrite = defaultPrimitives.atomicWrite;
|
|
171
|
+
var ensureDir = defaultPrimitives.ensureDir;
|
|
172
|
+
var withFileLock = defaultPrimitives.withFileLock;
|
|
173
|
+
export {
|
|
174
|
+
PersistenceFsError,
|
|
175
|
+
atomicWrite,
|
|
176
|
+
createPersistencePrimitives,
|
|
177
|
+
ensureDir,
|
|
178
|
+
withFileLock
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/atomic-write.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomBytes } from 'node:crypto';\nimport type { FSWatcher } from 'node:fs';\nimport { watch as watchDir } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\n\nexport interface AtomicWriteOptions {\n mode?: number | undefined;\n encoding?: BufferEncoding | undefined;\n}\n\nexport interface FileLockOptions {\n timeoutMs?: number | undefined;\n staleMs?: number | undefined;\n}\n\nexport interface FileLockTimeoutDetails {\n targetPath: string;\n timeoutMs: number;\n}\n\nexport interface PersistencePrimitiveOptions {\n createLockTimeoutError?: ((details: FileLockTimeoutDetails) => Error) | undefined;\n}\n\nexport interface PersistencePrimitives {\n atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts?: AtomicWriteOptions,\n ): Promise<void>;\n ensureDir(dir: string): Promise<void>;\n withFileLock<T>(targetPath: string, fn: () => Promise<T>, opts?: FileLockOptions): Promise<T>;\n}\n\n/** A dependency-free structured error for persistence boundary failures. */\nexport class PersistenceFsError extends Error {\n override name = 'FsError';\n readonly code: string;\n readonly path?: string | undefined;\n readonly context?: Record<string, unknown> | undefined;\n\n constructor(opts: {\n message: string;\n code: string;\n path?: string | undefined;\n context?: Record<string, unknown> | undefined;\n cause?: unknown | undefined;\n }) {\n super(opts.message, { cause: opts.cause });\n this.code = opts.code;\n this.path = opts.path;\n this.context = opts.context;\n }\n}\n\n/**\n * Create an isolated primitive set. Hosts may inject their own structured\n * timeout error without making this low-level package depend on that host.\n */\nexport function createPersistencePrimitives(\n options: PersistencePrimitiveOptions = {},\n): PersistencePrimitives {\n const createLockTimeoutError =\n options.createLockTimeoutError ??\n (({ targetPath, timeoutMs }: FileLockTimeoutDetails) =>\n new PersistenceFsError({\n message: `Timed out waiting for file lock: ${targetPath}`,\n code: 'FS_ATOMIC_WRITE_FAILED',\n path: targetPath,\n context: { timeoutMs },\n }));\n\n async function atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts: AtomicWriteOptions = {},\n ): Promise<void> {\n const dir = path.dirname(targetPath);\n await fs.mkdir(dir, { recursive: true });\n const tmp = path.join(\n dir,\n `.${path.basename(targetPath)}.${randomBytes(6).toString('hex')}.tmp`,\n );\n\n try {\n if (typeof content === 'string') {\n await fs.writeFile(tmp, content, { flag: 'wx', encoding: opts.encoding ?? 'utf8' });\n } else {\n await fs.writeFile(tmp, content, { flag: 'wx' });\n }\n try {\n const fileHandle = await fs.open(tmp, 'r+');\n try {\n await fileHandle.sync();\n } finally {\n await fileHandle.close();\n }\n } catch {\n // fsync is best-effort; the atomic rename still protects readers.\n }\n\n let mode: number | undefined;\n try {\n const stat = await fs.stat(targetPath);\n mode = stat.mode & 0o777;\n } catch {\n mode = opts.mode;\n }\n if (mode !== undefined) await fs.chmod(tmp, mode);\n\n await renameWithRetry(tmp, targetPath);\n if (mode !== undefined && process.platform === 'win32') {\n await fs.chmod(targetPath, mode).catch(() => undefined);\n }\n } catch (error) {\n await fs.unlink(tmp).catch(() => undefined);\n throw error;\n }\n }\n\n async function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n }\n\n async function withFileLock<T>(\n targetPath: string,\n fn: () => Promise<T>,\n opts: FileLockOptions = {},\n ): Promise<T> {\n const dir = path.dirname(targetPath);\n const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const staleMs = opts.staleMs ?? 30_000;\n const started = Date.now();\n let handle: fs.FileHandle | undefined;\n\n for (;;) {\n try {\n handle = await fs.open(lockPath, 'wx');\n await handle.writeFile(`${process.pid}:${Date.now()}`);\n break;\n } catch (error) {\n if (handle) {\n await handle.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n handle = undefined;\n }\n\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') {\n await fs.mkdir(dir, { recursive: true });\n continue;\n }\n if (code !== 'EEXIST' && code !== 'EPERM') throw error;\n\n try {\n const stat = await fs.stat(lockPath);\n if (Date.now() - stat.mtimeMs > staleMs) {\n await fs.unlink(lockPath);\n continue;\n }\n } catch {\n continue;\n }\n\n const elapsed = Date.now() - started;\n if (elapsed >= timeoutMs) {\n throw createLockTimeoutError({ targetPath, timeoutMs });\n }\n await waitForLockRelease(lockPath, timeoutMs - elapsed);\n }\n }\n\n try {\n return await fn();\n } finally {\n await handle?.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n }\n }\n\n return { atomicWrite, ensureDir, withFileLock };\n}\n\nasync function waitForLockRelease(lockPath: string, remainingMs: number): Promise<void> {\n const parentDir = path.dirname(lockPath);\n const lockName = path.basename(lockPath);\n const intervalMs = Math.min(remainingMs, 100);\n\n return new Promise<void>((resolve) => {\n let settled = false;\n let watcher: FSWatcher | null = null;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n watcher?.close();\n resolve();\n };\n const timer = setTimeout(settle, intervalMs);\n\n try {\n watcher = watchDir(parentDir, (eventType, filename) => {\n if (filename === lockName && (eventType === 'rename' || eventType === 'change')) {\n clearTimeout(timer);\n settle();\n }\n });\n } catch {\n clearTimeout(timer);\n setTimeout(settle, Math.min(remainingMs, 25));\n return;\n }\n\n void fs.access(lockPath).catch(() => {\n clearTimeout(timer);\n settle();\n });\n });\n}\n\nconst TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY']);\n\nasync function renameWithRetry(from: string, to: string): Promise<void> {\n if (process.platform !== 'win32') {\n await fs.rename(from, to);\n return;\n }\n\n const delays = [10, 25, 60, 120, 250];\n let lastError: unknown;\n for (let attempt = 0; attempt <= delays.length; attempt++) {\n try {\n await fs.rename(from, to);\n return;\n } catch (error) {\n lastError = error;\n const code = (error as NodeJS.ErrnoException).code;\n if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) throw error;\n await new Promise((resolve) => setTimeout(resolve, delays[attempt]));\n }\n }\n throw lastError;\n}\n\nconst defaultPrimitives = createPersistencePrimitives();\n\nexport const atomicWrite = defaultPrimitives.atomicWrite;\nexport const ensureDir = defaultPrimitives.ensureDir;\nexport const withFileLock = defaultPrimitives.withFileLock;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["atomicWrite", "stat", "ensureDir", "withFileLock"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wrongstack/persistence",
|
|
3
|
+
"version": "0.295.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Dependency-free filesystem persistence primitives shared across WrongStack packages.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/WrongStack/WrongStack.git",
|
|
9
|
+
"directory": "packages/persistence"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/WrongStack/WrongStack#readme",
|
|
12
|
+
"bugs": "https://github.com/WrongStack/WrongStack/issues",
|
|
13
|
+
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^26.1.1",
|
|
30
|
+
"typescript": "^7.0.2"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
39
|
+
}
|
|
40
|
+
}
|