ushman-ledger 1.2.1 → 1.3.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/AGENTS.md +7 -5
- package/ARCHITECTURE.md +85 -0
- package/CHANGELOG.md +11 -0
- package/README.md +114 -5
- package/TROUBLESHOOTING.md +184 -0
- package/dist/blobs.d.ts +3 -0
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +41 -15
- package/dist/builders.d.ts +33 -0
- package/dist/builders.d.ts.map +1 -1
- package/dist/builders.js +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +176 -59
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +3 -2
- package/dist/doctor.d.ts +17 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +263 -62
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +67 -30
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/list.d.ts +34 -1
- package/dist/list.d.ts.map +1 -1
- package/dist/list.js +19 -9
- package/dist/patch-resolver.d.ts.map +1 -1
- package/dist/patch-resolver.js +193 -53
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +16 -0
- package/dist/read-index.d.ts +7 -7
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +18 -13
- package/dist/record.js +2 -2
- package/dist/recovery.d.ts +8 -0
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +142 -30
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +4 -1
- package/dist/runtime-config.d.ts +14 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +97 -0
- package/dist/schema/entry-core.d.ts +5 -2
- package/dist/schema/entry-core.d.ts.map +1 -1
- package/dist/schema/entry-core.js +3 -0
- package/dist/schema/entry-read.d.ts +57 -0
- package/dist/schema/entry-read.d.ts.map +1 -1
- package/dist/schema/entry-read.js +9 -1
- package/dist/schema/entry-write.d.ts +51 -0
- package/dist/schema/entry-write.d.ts.map +1 -1
- package/dist/schema/entry-write.js +9 -1
- package/dist/storage/filesystem.d.ts +15 -2
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +234 -37
- package/dist/storage/lock.d.ts.map +1 -1
- package/dist/storage/lock.js +38 -16
- package/dist/text-lines.d.ts +8 -0
- package/dist/text-lines.d.ts.map +1 -0
- package/dist/text-lines.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -1
- package/package.json +4 -2
package/dist/storage/lock.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { open, readFile, rename, rm
|
|
2
|
+
import { open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { isProcessAlive } from "../process.js";
|
|
3
4
|
const DEFAULT_LOCK_OPTIONS = {
|
|
4
5
|
// Give writers enough time to detect and reclaim a stale lock before timing out.
|
|
5
6
|
retryDelayMs: 10,
|
|
@@ -7,6 +8,12 @@ const DEFAULT_LOCK_OPTIONS = {
|
|
|
7
8
|
timeoutMs: 35_000,
|
|
8
9
|
};
|
|
9
10
|
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
const parseOwnerPid = (value) => {
|
|
12
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
10
17
|
const readLockState = async (lockPath) => {
|
|
11
18
|
try {
|
|
12
19
|
const rawText = await readFile(lockPath, 'utf8');
|
|
@@ -14,6 +21,7 @@ const readLockState = async (lockPath) => {
|
|
|
14
21
|
const parsed = JSON.parse(rawText);
|
|
15
22
|
const startedAt = typeof parsed.startedAt === 'string' ? Date.parse(parsed.startedAt) : Number.NaN;
|
|
16
23
|
return {
|
|
24
|
+
ownerPid: parseOwnerPid(parsed.pid),
|
|
17
25
|
rawText,
|
|
18
26
|
startedAtMs: Number.isNaN(startedAt) ? null : startedAt,
|
|
19
27
|
token: typeof parsed.token === 'string' && parsed.token.length > 0 ? parsed.token : null,
|
|
@@ -21,6 +29,7 @@ const readLockState = async (lockPath) => {
|
|
|
21
29
|
}
|
|
22
30
|
catch {
|
|
23
31
|
return {
|
|
32
|
+
ownerPid: null,
|
|
24
33
|
rawText,
|
|
25
34
|
startedAtMs: null,
|
|
26
35
|
token: null,
|
|
@@ -35,6 +44,9 @@ const readLockState = async (lockPath) => {
|
|
|
35
44
|
}
|
|
36
45
|
};
|
|
37
46
|
const isFreshLockState = (lockState, staleMs) => {
|
|
47
|
+
if (lockState.ownerPid !== null && !isProcessAlive(lockState.ownerPid)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
38
50
|
if (lockState.startedAtMs === null) {
|
|
39
51
|
return false;
|
|
40
52
|
}
|
|
@@ -62,17 +74,17 @@ const writeExclusiveTextFile = async (filePath, text) => {
|
|
|
62
74
|
await handle.close();
|
|
63
75
|
}
|
|
64
76
|
};
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
const hasActiveReclaimMarker = async (lockPath, staleMs) => {
|
|
78
|
+
const reclaimMarkerPath = buildReclaimMarkerPath(lockPath);
|
|
79
|
+
const reclaimMarkerState = await readLockState(reclaimMarkerPath);
|
|
80
|
+
if (!reclaimMarkerState) {
|
|
81
|
+
return false;
|
|
69
82
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
throw error;
|
|
83
|
+
if (isFreshLockState(reclaimMarkerState, staleMs)) {
|
|
84
|
+
return true;
|
|
75
85
|
}
|
|
86
|
+
await rm(reclaimMarkerPath, { force: true });
|
|
87
|
+
return false;
|
|
76
88
|
};
|
|
77
89
|
const acquireReclaimMarker = async (lockPath) => {
|
|
78
90
|
const reclaimMarkerPath = buildReclaimMarkerPath(lockPath);
|
|
@@ -126,17 +138,24 @@ const reclaimIfObservedStateMatches = async ({ lockPath, observedState, testHook
|
|
|
126
138
|
}
|
|
127
139
|
throw error;
|
|
128
140
|
}
|
|
141
|
+
let shouldRemoveQuarantine = false;
|
|
129
142
|
try {
|
|
130
143
|
const quarantinedState = await readLockState(quarantinePath);
|
|
131
144
|
if (!quarantinedState || quarantinedState.rawText !== observedState.rawText) {
|
|
132
145
|
await restoreQuarantinedLock(lockPath, quarantinePath);
|
|
133
146
|
return false;
|
|
134
147
|
}
|
|
135
|
-
|
|
148
|
+
shouldRemoveQuarantine = true;
|
|
136
149
|
return true;
|
|
137
150
|
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
await restoreQuarantinedLock(lockPath, quarantinePath);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
138
155
|
finally {
|
|
139
|
-
|
|
156
|
+
if (shouldRemoveQuarantine) {
|
|
157
|
+
await rm(quarantinePath, { force: true });
|
|
158
|
+
}
|
|
140
159
|
}
|
|
141
160
|
}
|
|
142
161
|
finally {
|
|
@@ -155,7 +174,10 @@ const reclaimIfStale = async (lockPath, options) => {
|
|
|
155
174
|
});
|
|
156
175
|
};
|
|
157
176
|
const describeCurrentLockState = async (lockPath) => {
|
|
158
|
-
const [currentState, reclaiming] = await Promise.all([
|
|
177
|
+
const [currentState, reclaiming] = await Promise.all([
|
|
178
|
+
readLockState(lockPath),
|
|
179
|
+
hasActiveReclaimMarker(lockPath, DEFAULT_LOCK_OPTIONS.staleMs),
|
|
180
|
+
]);
|
|
159
181
|
if (!currentState) {
|
|
160
182
|
return reclaiming ? 'lock missing while stale reclamation marker is present' : 'lock file is missing';
|
|
161
183
|
}
|
|
@@ -163,7 +185,7 @@ const describeCurrentLockState = async (lockPath) => {
|
|
|
163
185
|
return `${ageMs}, token=${currentState.token ?? 'missing'}, reclaiming=${reclaiming ? 'yes' : 'no'}`;
|
|
164
186
|
};
|
|
165
187
|
const assertOwnership = async (lockPath, token) => {
|
|
166
|
-
if (await
|
|
188
|
+
if (await hasActiveReclaimMarker(lockPath, DEFAULT_LOCK_OPTIONS.staleMs)) {
|
|
167
189
|
throw new Error(`Lost ledger lock ownership for ${lockPath}: stale reclamation is in progress.`);
|
|
168
190
|
}
|
|
169
191
|
const currentState = await readLockState(lockPath);
|
|
@@ -182,7 +204,7 @@ export const acquireLock = async (lockPath, options = {}) => {
|
|
|
182
204
|
const mergedOptions = { ...DEFAULT_LOCK_OPTIONS, ...options };
|
|
183
205
|
const deadline = Date.now() + mergedOptions.timeoutMs;
|
|
184
206
|
while (Date.now() <= deadline) {
|
|
185
|
-
if (await
|
|
207
|
+
if (await hasActiveReclaimMarker(lockPath, mergedOptions.staleMs)) {
|
|
186
208
|
await sleep(mergedOptions.retryDelayMs);
|
|
187
209
|
continue;
|
|
188
210
|
}
|
|
@@ -201,7 +223,7 @@ export const acquireLock = async (lockPath, options = {}) => {
|
|
|
201
223
|
finally {
|
|
202
224
|
await handle.close();
|
|
203
225
|
}
|
|
204
|
-
if (await
|
|
226
|
+
if (await hasActiveReclaimMarker(lockPath, mergedOptions.staleMs)) {
|
|
205
227
|
await releaseIfOwned(lockPath, token);
|
|
206
228
|
await sleep(mergedOptions.retryDelayMs);
|
|
207
229
|
continue;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visit each logical line in `text`, normalizing trailing `\r` from CRLF input.
|
|
3
|
+
*
|
|
4
|
+
* The final unterminated line is still visited, which keeps diff parsing stable for
|
|
5
|
+
* patch content that does not end with a trailing newline.
|
|
6
|
+
*/
|
|
7
|
+
export declare const forEachLine: (text: string, visit: (line: string) => void) => void;
|
|
8
|
+
//# sourceMappingURL=text-lines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-lines.d.ts","sourceRoot":"","sources":["../src/text-lines.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,SAatE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visit each logical line in `text`, normalizing trailing `\r` from CRLF input.
|
|
3
|
+
*
|
|
4
|
+
* The final unterminated line is still visited, which keeps diff parsing stable for
|
|
5
|
+
* patch content that does not end with a trailing newline.
|
|
6
|
+
*/
|
|
7
|
+
export const forEachLine = (text, visit) => {
|
|
8
|
+
let lineStart = 0;
|
|
9
|
+
for (let index = 0; index <= text.length; index += 1) {
|
|
10
|
+
if (index !== text.length && text.charCodeAt(index) !== 10) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
let line = text.slice(lineStart, index);
|
|
14
|
+
if (line.endsWith('\r')) {
|
|
15
|
+
line = line.slice(0, -1);
|
|
16
|
+
}
|
|
17
|
+
visit(line);
|
|
18
|
+
lineStart = index + 1;
|
|
19
|
+
}
|
|
20
|
+
};
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const LEDGER_LIBRARY_VERSION
|
|
1
|
+
export declare const LEDGER_LIBRARY_VERSION: string;
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB,QAAsB,CAAC"}
|
package/dist/version.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
2
|
+
export const LEDGER_LIBRARY_VERSION = packageJson.version;
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"description": "Append-only workspace ledger library and CLI for Ushman v4.",
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@biomejs/biome": "^2.4.
|
|
19
|
+
"@biomejs/biome": "^2.4.16",
|
|
20
20
|
"@types/bun": "^1.3.14",
|
|
21
21
|
"@types/node": "^25.9.1",
|
|
22
22
|
"typescript": "^6.0.3"
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"dist",
|
|
40
|
+
"ARCHITECTURE.md",
|
|
40
41
|
"CHANGELOG.md",
|
|
42
|
+
"TROUBLESHOOTING.md",
|
|
41
43
|
"README.md",
|
|
42
44
|
"AGENTS.md",
|
|
43
45
|
"LICENSE.md"
|
|
@@ -68,5 +70,5 @@
|
|
|
68
70
|
},
|
|
69
71
|
"type": "module",
|
|
70
72
|
"types": "dist/index.d.ts",
|
|
71
|
-
"version": "1.
|
|
73
|
+
"version": "1.3.0"
|
|
72
74
|
}
|