@nocoo/pew 1.11.1 → 1.14.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/dist/cli.js +1 -1
- package/dist/commands/notify.d.ts.map +1 -1
- package/dist/commands/notify.js +134 -5
- package/dist/commands/notify.js.map +1 -1
- package/dist/notifier/coordinator.d.ts +14 -10
- package/dist/notifier/coordinator.d.ts.map +1 -1
- package/dist/notifier/coordinator.js +134 -78
- package/dist/notifier/coordinator.js.map +1 -1
- package/dist/notifier/lockfile.d.ts +81 -0
- package/dist/notifier/lockfile.d.ts.map +1 -0
- package/dist/notifier/lockfile.js +150 -0
- package/dist/notifier/lockfile.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ConfigManager } from "./config/manager.js";
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// CLI version — single source of truth within CLI runtime
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
|
-
const CLI_VERSION = "1.
|
|
23
|
+
const CLI_VERSION = "1.14.0";
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Dev mode detection (otter pattern)
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC5F,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EAEhB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mFAAmF;IACnF,aAAa,CAAC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACpD,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,eAAe,CAAC;IAC3C,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACvE;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,oBAAoB,CAAC,CA0F/B"}
|
package/dist/commands/notify.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeFile, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { executeSync } from "./sync.js";
|
|
2
4
|
import { executeSessionSync, } from "./session-sync.js";
|
|
3
5
|
import { coordinatedSync, } from "../notifier/coordinator.js";
|
|
@@ -55,15 +57,142 @@ export async function executeNotify(opts) {
|
|
|
55
57
|
}
|
|
56
58
|
return cycle;
|
|
57
59
|
});
|
|
60
|
+
const trigger = {
|
|
61
|
+
kind: "notify",
|
|
62
|
+
source: opts.source,
|
|
63
|
+
fileHint: opts.fileHint ?? null,
|
|
64
|
+
};
|
|
58
65
|
const coordinatorOptions = {
|
|
59
66
|
stateDir: opts.stateDir,
|
|
60
67
|
executeSyncFn,
|
|
61
68
|
version: opts.version,
|
|
69
|
+
cooldownMs: 300_000, // 5 minutes — skip sync if last success was recent
|
|
62
70
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const result = await coordinatedSyncFn(trigger, coordinatorOptions);
|
|
72
|
+
// --- Trailing-edge guarantee ---
|
|
73
|
+
// When cooldown fires, pending signals are preserved but no future hook
|
|
74
|
+
// is guaranteed to consume them. Schedule a single trailing-edge sync
|
|
75
|
+
// after cooldown expires to ensure the last batch of data is uploaded.
|
|
76
|
+
if (result.skippedReason === "cooldown" &&
|
|
77
|
+
result.cooldownRemainingMs != null &&
|
|
78
|
+
result.cooldownRemainingMs > 0) {
|
|
79
|
+
scheduleTrailingSync(trigger, coordinatorOptions, result.cooldownRemainingMs, coordinatedSyncFn);
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Schedule a trailing-edge sync after cooldown expires.
|
|
85
|
+
*
|
|
86
|
+
* Uses an O_EXCL trailing.lock file (containing PID) to ensure only one
|
|
87
|
+
* process sleeps at a time. If a trailing.lock exists from a dead process,
|
|
88
|
+
* it is removed and the lock is re-acquired (stale detection via
|
|
89
|
+
* `process.kill(pid, 0)`). If the lock is held by a live process, this
|
|
90
|
+
* is a no-op.
|
|
91
|
+
*
|
|
92
|
+
* The trailing sync runs fire-and-forget — errors are silently ignored.
|
|
93
|
+
*/
|
|
94
|
+
function scheduleTrailingSync(trigger, opts, delayMs, coordinatedSyncFn) {
|
|
95
|
+
const trailingLockPath = join(opts.stateDir, "trailing.lock");
|
|
96
|
+
// Fire-and-forget: acquire trailing lock, sleep, sync, release
|
|
97
|
+
void (async () => {
|
|
98
|
+
const acquired = await tryAcquireTrailingLock(trailingLockPath);
|
|
99
|
+
if (!acquired)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
103
|
+
await coordinatedSyncFn(trigger, opts);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Trailing sync errors are non-fatal
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
try {
|
|
110
|
+
await unlink(trailingLockPath);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Cleanup failure is non-fatal
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Try to acquire the trailing lock. If the lockfile exists, check if the
|
|
120
|
+
* owning PID is still alive. Dead PID → remove stale lock and retry.
|
|
121
|
+
* Live PID → return false (another trailing sync is in progress).
|
|
122
|
+
*
|
|
123
|
+
* @returns `true` if lock was acquired, `false` otherwise.
|
|
124
|
+
*/
|
|
125
|
+
async function tryAcquireTrailingLock(lockPath) {
|
|
126
|
+
const lockContent = JSON.stringify({
|
|
127
|
+
pid: process.pid,
|
|
128
|
+
startedAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await writeFile(lockPath, lockContent, { flag: "wx" });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err.code !== "EEXIST")
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// Lock exists — check if owner is alive
|
|
139
|
+
const ownerPid = await readTrailingLockPid(lockPath);
|
|
140
|
+
if (ownerPid === null) {
|
|
141
|
+
// Corrupted/unreadable — remove and retry
|
|
142
|
+
try {
|
|
143
|
+
await unlink(lockPath);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await writeFile(lockPath, lockContent, { flag: "wx" });
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Check if owner PID is alive
|
|
157
|
+
try {
|
|
158
|
+
process.kill(ownerPid, 0);
|
|
159
|
+
return false; // Process alive — valid lock
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err.code !== "ESRCH") {
|
|
163
|
+
// EPERM = process exists but we can't signal it → not stale
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Dead PID — remove stale lock and retry
|
|
168
|
+
try {
|
|
169
|
+
await unlink(lockPath);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await writeFile(lockPath, lockContent, { flag: "wx" });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read the PID from a trailing.lock file.
|
|
184
|
+
* Returns null on any error (missing, corrupted, etc.).
|
|
185
|
+
*/
|
|
186
|
+
async function readTrailingLockPid(lockPath) {
|
|
187
|
+
try {
|
|
188
|
+
const content = await readFile(lockPath, "utf8");
|
|
189
|
+
const parsed = JSON.parse(content);
|
|
190
|
+
if (typeof parsed?.pid === "number")
|
|
191
|
+
return parsed.pid;
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
68
197
|
}
|
|
69
198
|
//# sourceMappingURL=notify.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAoB,MAAM,WAAW,CAAC;AAC1D,OAAO,EACL,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,GAEhB,MAAM,4BAA4B,CAAC;AAapC,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAmB;IAEnB,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,IAAI,eAAe,CAAC;IACpE,MAAM,aAAa,GACjB,IAAI,CAAC,aAAa;QAClB,CAAC,KAAK,IAA8B,EAAE;YACpC,MAAM,KAAK,GAAoB,EAAE,CAAC;YAElC,aAAa;YACb,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,MAAM,WAAW,CAAC;oBACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;oBACvC,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;oBAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;oBACnC,aAAa,EAAE,IAAI,CAAC,aAAa;oBACjC,WAAW,EAAE,IAAI,CAAC,WAAW;oBAC7B,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;oBACzC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;iBAC1C,CAAC,CAAC;gBACH,KAAK,CAAC,SAAS,GAAG;oBAChB,WAAW,EAAE,WAAW,CAAC,WAAW;oBACpC,YAAY,EAAE,WAAW,CAAC,YAAY;oBACtC,YAAY,EAAE,WAAW,CAAC,YAAY;oBACtC,OAAO,EAAE,WAAW,CAAC,OAAO;iBAC7B,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,CAAC,cAAc,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1E,CAAC;YAED,eAAe;YACf,IAAI,CAAC;gBACH,MAAM,aAAa,GAAG,MAAM,kBAAkB,CAAC;oBAC7C,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;oBACvC,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;oBAC3C,cAAc,EAAE,IAAI,CAAC,cAAc;oBACnC,aAAa,EAAE,IAAI,CAAC,aAAa;oBACjC,WAAW,EAAE,IAAI,CAAC,WAAW;iBAC9B,CAAC,CAAC;gBACH,KAAK,CAAC,WAAW,GAAG;oBAClB,cAAc,EAAE,aAAa,CAAC,cAAc;oBAC5C,YAAY,EAAE,aAAa,CAAC,YAAY;oBACxC,YAAY,EAAE,aAAa,CAAC,YAAY;oBACxC,OAAO,EAAE,aAAa,CAAC,OAAO;iBAC/B,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,CAAC,gBAAgB,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5E,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IAEL,MAAM,OAAO,GAAgB;QAC3B,IAAI,EAAE,QAAQ;QACd,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;KAChC,CAAC;IAEF,MAAM,kBAAkB,GAAuB;QAC7C,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,aAAa;QACb,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,UAAU,EAAE,OAAO,EAAE,mDAAmD;KACzE,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;IAEpE,kCAAkC;IAClC,wEAAwE;IACxE,sEAAsE;IACtE,uEAAuE;IACvE,IACE,MAAM,CAAC,aAAa,KAAK,UAAU;QACnC,MAAM,CAAC,mBAAmB,IAAI,IAAI;QAClC,MAAM,CAAC,mBAAmB,GAAG,CAAC,EAC9B,CAAC;QACD,oBAAoB,CAClB,OAAO,EACP,kBAAkB,EAClB,MAAM,CAAC,mBAAmB,EAC1B,iBAAiB,CAClB,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,oBAAoB,CAC3B,OAAoB,EACpB,IAAwB,EACxB,OAAe,EACf,iBAAyC;IAEzC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IAE9D,+DAA+D;IAC/D,KAAK,CAAC,KAAK,IAAI,EAAE;QACf,MAAM,QAAQ,GAAG,MAAM,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;QAChE,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,IAAI,CAAC;YACH,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;YACjD,MAAM,iBAAiB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,qCAAqC;QACvC,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,+BAA+B;YACjC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;AACP,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,sBAAsB,CAAC,QAAgB;IACpD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC;QACjC,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;IACrE,CAAC;IAED,wCAAwC;IACxC,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,0CAA0C;QAC1C,IAAI,CAAC;YAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,KAAK,CAAC;QAAC,CAAC;QACvD,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,KAAK,CAAC;QAAC,CAAC;IAC3B,CAAC;IAED,8BAA8B;IAC9B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC1B,OAAO,KAAK,CAAC,CAAC,6BAA6B;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACpD,4DAA4D;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,yCAAyC;IACzC,IAAI,CAAC;QAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,OAAO,MAAM,EAAE,GAAG,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC;QACvD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { CoordinatorRunResult, SyncCycleResult, SyncTrigger } from "@pew/core";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
nonBlocking?: boolean;
|
|
5
|
-
}): Promise<void>;
|
|
6
|
-
close(): Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
interface FsOps {
|
|
9
|
-
open: (path: string, flags: string) => Promise<LockHandle>;
|
|
2
|
+
import { type ProcessOps } from "./lockfile.js";
|
|
3
|
+
export interface FsOps {
|
|
10
4
|
stat: (path: string) => Promise<{
|
|
11
5
|
size: number;
|
|
12
6
|
}>;
|
|
13
7
|
appendFile: (path: string, data: string) => Promise<unknown>;
|
|
14
|
-
writeFile: (path: string, data: string
|
|
8
|
+
writeFile: (path: string, data: string, options?: {
|
|
9
|
+
flag?: string;
|
|
10
|
+
}) => Promise<unknown>;
|
|
11
|
+
readFile: (path: string) => Promise<string>;
|
|
12
|
+
unlink: (path: string) => Promise<unknown>;
|
|
15
13
|
mkdir: (path: string, options: {
|
|
16
14
|
recursive: boolean;
|
|
17
15
|
}) => Promise<unknown>;
|
|
@@ -22,9 +20,15 @@ export interface CoordinatorOptions {
|
|
|
22
20
|
version?: string;
|
|
23
21
|
now?: () => number;
|
|
24
22
|
fs?: FsOps;
|
|
23
|
+
process?: ProcessOps;
|
|
25
24
|
maxFollowUps?: number;
|
|
26
25
|
lockTimeoutMs?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Cooldown duration in ms. If the last successful sync completed less than
|
|
28
|
+
* this many ms ago, skip sync. Set to 0 to disable cooldown.
|
|
29
|
+
* Default: undefined (no cooldown — always runs).
|
|
30
|
+
*/
|
|
31
|
+
cooldownMs?: number;
|
|
27
32
|
}
|
|
28
33
|
export declare function coordinatedSync(trigger: SyncTrigger, opts: CoordinatorOptions): Promise<CoordinatorRunResult>;
|
|
29
|
-
export {};
|
|
30
34
|
//# sourceMappingURL=coordinator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,oBAAoB,EAEpB,eAAe,EACf,WAAW,EACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,eAAe,CAAC;AAMvB,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,SAAS,EAAE,CACT,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KACxB,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAoBD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,KAAK,CAAC;IACX,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,oBAAoB,CAAC,CA6C/B"}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat, appendFile, writeFile, readFile, unlink, mkdir, } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { acquireLock, releaseLock, waitForLock, } from "./lockfile.js";
|
|
3
4
|
const defaultFs = {
|
|
4
|
-
open: open,
|
|
5
5
|
stat: stat,
|
|
6
6
|
appendFile: appendFile,
|
|
7
7
|
writeFile: writeFile,
|
|
8
|
+
readFile: readFile,
|
|
9
|
+
unlink: unlink,
|
|
8
10
|
mkdir: mkdir,
|
|
9
11
|
};
|
|
12
|
+
const defaultProcess = {
|
|
13
|
+
pid: process.pid,
|
|
14
|
+
kill: (pid, signal) => process.kill(pid, signal),
|
|
15
|
+
};
|
|
10
16
|
const DEFAULT_MAX_FOLLOW_UPS = 3;
|
|
11
17
|
const DEFAULT_LOCK_TIMEOUT_MS = 60_000;
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Main entry point
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
12
21
|
export async function coordinatedSync(trigger, opts) {
|
|
13
22
|
const fs = opts.fs ?? defaultFs;
|
|
23
|
+
const proc = opts.process ?? defaultProcess;
|
|
14
24
|
const now = opts.now ?? Date.now;
|
|
15
25
|
const startTime = now();
|
|
16
26
|
const maxFollowUps = opts.maxFollowUps ?? DEFAULT_MAX_FOLLOW_UPS;
|
|
@@ -29,7 +39,7 @@ export async function coordinatedSync(trigger, opts) {
|
|
|
29
39
|
await fs.mkdir(opts.stateDir, { recursive: true });
|
|
30
40
|
let result;
|
|
31
41
|
try {
|
|
32
|
-
result = await runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutMs, baseResult);
|
|
42
|
+
result = await runCoordinator(trigger, opts, fs, proc, now, maxFollowUps, lockTimeoutMs, baseResult);
|
|
33
43
|
}
|
|
34
44
|
catch (err) {
|
|
35
45
|
result = { ...baseResult, error: toErrorMessage(err) };
|
|
@@ -37,52 +47,58 @@ export async function coordinatedSync(trigger, opts) {
|
|
|
37
47
|
await writeRunLog(result, startTime, now, opts.version ?? "unknown", opts.stateDir, fs);
|
|
38
48
|
return result;
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Lock acquisition + coordination loop
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
async function runCoordinator(trigger, opts, fs, proc, now, maxFollowUps, lockTimeoutMs, baseResult) {
|
|
42
54
|
const lockPath = join(opts.stateDir, "sync.lock");
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
const lockFs = {
|
|
56
|
+
writeFile: async (path, data, options) => {
|
|
57
|
+
await fs.writeFile(path, data, options);
|
|
58
|
+
},
|
|
59
|
+
readFile: (path) => fs.readFile(path),
|
|
60
|
+
unlink: async (path) => {
|
|
61
|
+
await fs.unlink(path);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
45
64
|
let acquiredLock = false;
|
|
46
65
|
let waitedForLock = false;
|
|
47
66
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await
|
|
51
|
-
closeHandled = true;
|
|
52
|
-
return runUnlocked(baseResult, trigger, opts.executeSyncFn);
|
|
67
|
+
// --- Try immediate (non-blocking) lock acquisition ---
|
|
68
|
+
try {
|
|
69
|
+
acquiredLock = await acquireLock(lockPath, { fs: lockFs, process: proc });
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Fail-closed: lock mechanism broken → skip sync, never run unlocked
|
|
73
|
+
return {
|
|
74
|
+
...baseResult,
|
|
75
|
+
skippedSync: true,
|
|
76
|
+
error: toErrorMessage(err),
|
|
77
|
+
};
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
if (!acquiredLock) {
|
|
80
|
+
// Lock held by another process → append signal + poll wait
|
|
63
81
|
waitedForLock = true;
|
|
64
82
|
await appendSignal(opts.stateDir, fs);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await lockHandle.close();
|
|
73
|
-
closeHandled = true;
|
|
83
|
+
const waitResult = await waitForLock(lockPath, {
|
|
84
|
+
fs: lockFs,
|
|
85
|
+
process: proc,
|
|
86
|
+
timeoutMs: lockTimeoutMs,
|
|
87
|
+
});
|
|
88
|
+
if (!waitResult.acquired) {
|
|
89
|
+
// Fail-closed: could not acquire lock → skip sync, report error
|
|
74
90
|
return {
|
|
75
91
|
...baseResult,
|
|
76
92
|
waitedForLock: true,
|
|
77
93
|
skippedSync: true,
|
|
78
|
-
error: "lock timeout",
|
|
94
|
+
error: waitResult.error ?? "lock timeout",
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
acquiredLock = true;
|
|
98
|
+
// --- Waiter dedup: check if the previous holder's follow-up
|
|
99
|
+
// already consumed our signal ---
|
|
82
100
|
const signalSize = await readSignalSize(opts.stateDir, fs);
|
|
83
101
|
if (signalSize === 0) {
|
|
84
|
-
await lockHandle.close();
|
|
85
|
-
closeHandled = true;
|
|
86
102
|
return {
|
|
87
103
|
...baseResult,
|
|
88
104
|
waitedForLock: true,
|
|
@@ -90,13 +106,21 @@ async function runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutM
|
|
|
90
106
|
};
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
// --- We hold the lock — check cooldown before running sync ---
|
|
110
|
+
const cooldownMs = opts.cooldownMs;
|
|
111
|
+
if (cooldownMs != null && cooldownMs > 0) {
|
|
112
|
+
const remainingMs = await checkCooldown(opts.stateDir, fs, now, cooldownMs);
|
|
113
|
+
if (remainingMs > 0) {
|
|
114
|
+
return {
|
|
115
|
+
...baseResult,
|
|
116
|
+
waitedForLock,
|
|
117
|
+
skippedSync: true,
|
|
118
|
+
skippedReason: "cooldown",
|
|
119
|
+
cooldownRemainingMs: remainingMs,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
123
|
+
// --- Run sync cycles ---
|
|
100
124
|
const lockedResult = await runLockedCycles({
|
|
101
125
|
stateDir: opts.stateDir,
|
|
102
126
|
fs,
|
|
@@ -111,12 +135,14 @@ async function runCoordinator(trigger, opts, fs, now, maxFollowUps, lockTimeoutM
|
|
|
111
135
|
};
|
|
112
136
|
}
|
|
113
137
|
finally {
|
|
114
|
-
if (acquiredLock
|
|
115
|
-
await
|
|
138
|
+
if (acquiredLock) {
|
|
139
|
+
await releaseLock(lockPath, { fs: lockFs, process: proc });
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
142
|
}
|
|
119
|
-
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Locked cycle loop (unchanged semantics from original coordinator)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
120
146
|
async function runLockedCycles({ stateDir, fs, executeSyncFn, trigger, maxFollowUps, }) {
|
|
121
147
|
let hadFollowUp = false;
|
|
122
148
|
let error;
|
|
@@ -140,26 +166,17 @@ async function runLockedCycles({ stateDir, fs, executeSyncFn, trigger, maxFollow
|
|
|
140
166
|
hadFollowUp = true;
|
|
141
167
|
followUps += 1;
|
|
142
168
|
}
|
|
143
|
-
return {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
degradedToUnlocked: true,
|
|
151
|
-
cycles: [cycleResult],
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
return {
|
|
156
|
-
...baseResult,
|
|
157
|
-
degradedToUnlocked: true,
|
|
158
|
-
cycles: [{}],
|
|
159
|
-
error: toErrorMessage(err),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
169
|
+
return {
|
|
170
|
+
hadFollowUp,
|
|
171
|
+
followUpCount: followUps,
|
|
172
|
+
skippedSync: false,
|
|
173
|
+
cycles,
|
|
174
|
+
error,
|
|
175
|
+
};
|
|
162
176
|
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Status derivation + run log (unchanged from original)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
163
180
|
function deriveStatus(result) {
|
|
164
181
|
if (result.skippedSync)
|
|
165
182
|
return "skipped";
|
|
@@ -189,6 +206,12 @@ async function writeRunLog(result, startTime, now, version, stateDir, fs) {
|
|
|
189
206
|
coordination: {
|
|
190
207
|
waitedForLock: result.waitedForLock,
|
|
191
208
|
skippedSync: result.skippedSync,
|
|
209
|
+
...(result.skippedReason != null
|
|
210
|
+
? { skippedReason: result.skippedReason }
|
|
211
|
+
: {}),
|
|
212
|
+
...(result.cooldownRemainingMs != null
|
|
213
|
+
? { cooldownRemainingMs: result.cooldownRemainingMs }
|
|
214
|
+
: {}),
|
|
192
215
|
hadFollowUp: result.hadFollowUp,
|
|
193
216
|
followUpCount: result.followUpCount,
|
|
194
217
|
degradedToUnlocked: result.degradedToUnlocked,
|
|
@@ -202,11 +225,59 @@ async function writeRunLog(result, startTime, now, version, stateDir, fs) {
|
|
|
202
225
|
await fs.mkdir(runsDir, { recursive: true });
|
|
203
226
|
await fs.writeFile(join(runsDir, `${result.runId}.json`), json);
|
|
204
227
|
await fs.writeFile(join(stateDir, "last-run.json"), json);
|
|
228
|
+
// Write last-success.json only on success — used exclusively by cooldown.
|
|
229
|
+
// Kept separate from last-run.json so skipped/error runs don't overwrite
|
|
230
|
+
// the success timestamp.
|
|
231
|
+
if (entry.status === "success") {
|
|
232
|
+
await fs.writeFile(join(stateDir, "last-success.json"), entry.completedAt);
|
|
233
|
+
}
|
|
205
234
|
}
|
|
206
235
|
catch {
|
|
207
236
|
// Run log write failures are non-fatal
|
|
208
237
|
}
|
|
209
238
|
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Cooldown check
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Check if the last successful sync completed within the cooldown window.
|
|
244
|
+
* Returns the remaining cooldown time in ms, or 0 if cooldown is not active.
|
|
245
|
+
*
|
|
246
|
+
* Reads `last-success.json` (a plain ISO timestamp), which is only written
|
|
247
|
+
* after a successful sync. This avoids the problem where `last-run.json`
|
|
248
|
+
* (written on every run including skipped/error) would overwrite the success
|
|
249
|
+
* timestamp and break cooldown for subsequent runs.
|
|
250
|
+
*/
|
|
251
|
+
async function checkCooldown(stateDir, fs, now, cooldownMs) {
|
|
252
|
+
const lastSuccessAt = await readLastSuccessAt(stateDir, fs);
|
|
253
|
+
if (lastSuccessAt == null)
|
|
254
|
+
return 0;
|
|
255
|
+
const completedAtMs = new Date(lastSuccessAt).getTime();
|
|
256
|
+
if (Number.isNaN(completedAtMs))
|
|
257
|
+
return 0;
|
|
258
|
+
const elapsed = now() - completedAtMs;
|
|
259
|
+
const remaining = cooldownMs - elapsed;
|
|
260
|
+
return remaining > 0 ? remaining : 0;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Read the last-success.json file (plain ISO timestamp string).
|
|
264
|
+
* Returns null on any error (missing file, corrupted, etc.).
|
|
265
|
+
*/
|
|
266
|
+
async function readLastSuccessAt(stateDir, fs) {
|
|
267
|
+
try {
|
|
268
|
+
const content = await fs.readFile(join(stateDir, "last-success.json"));
|
|
269
|
+
const trimmed = String(content).trim();
|
|
270
|
+
if (trimmed.length === 0)
|
|
271
|
+
return null;
|
|
272
|
+
return trimmed;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Signal file helpers (unchanged from original)
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
210
281
|
async function readSignalSize(stateDir, fs) {
|
|
211
282
|
try {
|
|
212
283
|
const file = await fs.stat(join(stateDir, "notify.signal"));
|
|
@@ -225,25 +296,10 @@ async function appendSignal(stateDir, fs) {
|
|
|
225
296
|
async function truncateSignal(stateDir, fs) {
|
|
226
297
|
await fs.writeFile(join(stateDir, "notify.signal"), "");
|
|
227
298
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Utilities
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
232
302
|
function toErrorMessage(error) {
|
|
233
303
|
return error instanceof Error ? error.message : String(error);
|
|
234
304
|
}
|
|
235
|
-
function withTimeout(promise, timeoutMs) {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
const timer = setTimeout(() => {
|
|
238
|
-
reject(new Error("lock timeout"));
|
|
239
|
-
}, timeoutMs);
|
|
240
|
-
promise.then((value) => {
|
|
241
|
-
clearTimeout(timer);
|
|
242
|
-
resolve(value);
|
|
243
|
-
}, (error) => {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
reject(error);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
305
|
//# sourceMappingURL=coordinator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/notifier/coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O_EXCL-based lockfile with PID-based stale detection.
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-process mutual exclusion that works on all Node.js/Bun
|
|
5
|
+
* versions (unlike FileHandle.lock() which requires Node 22+).
|
|
6
|
+
*
|
|
7
|
+
* The lockfile contains `{ pid, startedAt }` JSON. Stale detection uses
|
|
8
|
+
* `process.kill(pid, 0)` — PID-only, no age-based checks (a slow fetch()
|
|
9
|
+
* is still a valid lock holder).
|
|
10
|
+
*/
|
|
11
|
+
export interface LockFsOps {
|
|
12
|
+
writeFile(path: string, data: string, options?: {
|
|
13
|
+
flag?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
readFile(path: string): Promise<string>;
|
|
16
|
+
unlink(path: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface ProcessOps {
|
|
19
|
+
readonly pid: number;
|
|
20
|
+
kill(pid: number, signal: number): boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Try to create the lockfile atomically (O_EXCL).
|
|
24
|
+
*
|
|
25
|
+
* @returns `true` if acquired, `false` if another lockfile already exists.
|
|
26
|
+
* @throws On unexpected fs errors (not EEXIST).
|
|
27
|
+
*/
|
|
28
|
+
export declare function acquireLock(lockPath: string, opts: {
|
|
29
|
+
fs: LockFsOps;
|
|
30
|
+
process: ProcessOps;
|
|
31
|
+
}): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Release the lockfile by unlinking it, but only if the PID inside matches
|
|
34
|
+
* our own. Silently handles all errors (lockfile already gone, permission
|
|
35
|
+
* issues, corrupted content).
|
|
36
|
+
*/
|
|
37
|
+
export declare function releaseLock(lockPath: string, opts: {
|
|
38
|
+
fs: LockFsOps;
|
|
39
|
+
process: ProcessOps;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Read and parse the PID from a lockfile.
|
|
43
|
+
*
|
|
44
|
+
* @returns The PID number, or `null` if the file doesn't exist or is
|
|
45
|
+
* corrupted/unparseable.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readLockPid(lockPath: string, opts: {
|
|
48
|
+
fs: Pick<LockFsOps, "readFile">;
|
|
49
|
+
}): Promise<number | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Check if the lockfile is stale (owner process is dead).
|
|
52
|
+
*
|
|
53
|
+
* - PID dead (ESRCH) → stale
|
|
54
|
+
* - PID alive → not stale
|
|
55
|
+
* - PID alive but no permission (EPERM) → not stale (process exists)
|
|
56
|
+
* - Lockfile missing or corrupted → stale (nothing to protect)
|
|
57
|
+
*/
|
|
58
|
+
export declare function isLockStale(lockPath: string, opts: {
|
|
59
|
+
fs: Pick<LockFsOps, "readFile">;
|
|
60
|
+
process: ProcessOps;
|
|
61
|
+
}): Promise<boolean>;
|
|
62
|
+
export interface WaitForLockResult {
|
|
63
|
+
acquired: boolean;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Poll for the lockfile to become available with exponential backoff.
|
|
68
|
+
*
|
|
69
|
+
* If the current holder's PID is dead, removes the stale lockfile and
|
|
70
|
+
* retries acquisition. Gives up after `timeoutMs`.
|
|
71
|
+
*
|
|
72
|
+
* Backoff: starts at 100ms, doubles each iteration, caps at 2000ms.
|
|
73
|
+
*/
|
|
74
|
+
export declare function waitForLock(lockPath: string, opts: {
|
|
75
|
+
fs: LockFsOps;
|
|
76
|
+
process: ProcessOps;
|
|
77
|
+
timeoutMs: number;
|
|
78
|
+
sleep?: (ms: number) => Promise<void>;
|
|
79
|
+
now?: () => number;
|
|
80
|
+
}): Promise<WaitForLockResult>;
|
|
81
|
+
//# sourceMappingURL=lockfile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/notifier/lockfile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,MAAM,WAAW,SAAS;IACxB,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5C;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,OAAO,CAAC,CAclB;AAMD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC3C,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAMD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAC7D,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAMD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IACJ,EAAE,EAAE,SAAS,CAAC;IACd,OAAO,EAAE,UAAU,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,iBAAiB,CAAC,CAsC5B"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O_EXCL-based lockfile with PID-based stale detection.
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-process mutual exclusion that works on all Node.js/Bun
|
|
5
|
+
* versions (unlike FileHandle.lock() which requires Node 22+).
|
|
6
|
+
*
|
|
7
|
+
* The lockfile contains `{ pid, startedAt }` JSON. Stale detection uses
|
|
8
|
+
* `process.kill(pid, 0)` — PID-only, no age-based checks (a slow fetch()
|
|
9
|
+
* is still a valid lock holder).
|
|
10
|
+
*/
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// acquireLock — O_EXCL atomic create
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Try to create the lockfile atomically (O_EXCL).
|
|
16
|
+
*
|
|
17
|
+
* @returns `true` if acquired, `false` if another lockfile already exists.
|
|
18
|
+
* @throws On unexpected fs errors (not EEXIST).
|
|
19
|
+
*/
|
|
20
|
+
export async function acquireLock(lockPath, opts) {
|
|
21
|
+
const content = JSON.stringify({
|
|
22
|
+
pid: opts.process.pid,
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
await opts.fs.writeFile(lockPath, content, { flag: "wx" });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "EEXIST") {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// releaseLock — unlink only if we own the lockfile
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Release the lockfile by unlinking it, but only if the PID inside matches
|
|
41
|
+
* our own. Silently handles all errors (lockfile already gone, permission
|
|
42
|
+
* issues, corrupted content).
|
|
43
|
+
*/
|
|
44
|
+
export async function releaseLock(lockPath, opts) {
|
|
45
|
+
try {
|
|
46
|
+
const pid = await readLockPid(lockPath, { fs: opts.fs });
|
|
47
|
+
if (pid !== opts.process.pid)
|
|
48
|
+
return;
|
|
49
|
+
await opts.fs.unlink(lockPath);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Best-effort cleanup — don't let release failures propagate
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// readLockPid — parse PID from lockfile content
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Read and parse the PID from a lockfile.
|
|
60
|
+
*
|
|
61
|
+
* @returns The PID number, or `null` if the file doesn't exist or is
|
|
62
|
+
* corrupted/unparseable.
|
|
63
|
+
*/
|
|
64
|
+
export async function readLockPid(lockPath, opts) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await opts.fs.readFile(lockPath);
|
|
67
|
+
const parsed = JSON.parse(content);
|
|
68
|
+
if (typeof parsed?.pid === "number")
|
|
69
|
+
return parsed.pid;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// isLockStale — PID-based stale detection
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Check if the lockfile is stale (owner process is dead).
|
|
81
|
+
*
|
|
82
|
+
* - PID dead (ESRCH) → stale
|
|
83
|
+
* - PID alive → not stale
|
|
84
|
+
* - PID alive but no permission (EPERM) → not stale (process exists)
|
|
85
|
+
* - Lockfile missing or corrupted → stale (nothing to protect)
|
|
86
|
+
*/
|
|
87
|
+
export async function isLockStale(lockPath, opts) {
|
|
88
|
+
const pid = await readLockPid(lockPath, { fs: opts.fs });
|
|
89
|
+
if (pid === null)
|
|
90
|
+
return true;
|
|
91
|
+
// Our own PID — not stale
|
|
92
|
+
if (pid === opts.process.pid)
|
|
93
|
+
return false;
|
|
94
|
+
try {
|
|
95
|
+
opts.process.kill(pid, 0);
|
|
96
|
+
return false; // Process exists
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const code = err.code;
|
|
100
|
+
if (code === "ESRCH")
|
|
101
|
+
return true; // No such process
|
|
102
|
+
// EPERM = process exists but we can't signal it → not stale
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Poll for the lockfile to become available with exponential backoff.
|
|
108
|
+
*
|
|
109
|
+
* If the current holder's PID is dead, removes the stale lockfile and
|
|
110
|
+
* retries acquisition. Gives up after `timeoutMs`.
|
|
111
|
+
*
|
|
112
|
+
* Backoff: starts at 100ms, doubles each iteration, caps at 2000ms.
|
|
113
|
+
*/
|
|
114
|
+
export async function waitForLock(lockPath, opts) {
|
|
115
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
116
|
+
const now = opts.now ?? Date.now;
|
|
117
|
+
const startTime = now();
|
|
118
|
+
let backoff = 100;
|
|
119
|
+
const maxBackoff = 2000;
|
|
120
|
+
while (true) {
|
|
121
|
+
// Check if stale → remove and retry
|
|
122
|
+
const stale = await isLockStale(lockPath, {
|
|
123
|
+
fs: opts.fs,
|
|
124
|
+
process: opts.process,
|
|
125
|
+
});
|
|
126
|
+
if (stale) {
|
|
127
|
+
try {
|
|
128
|
+
await opts.fs.unlink(lockPath);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// May already be removed by another process — fine
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Try to acquire
|
|
135
|
+
const acquired = await acquireLock(lockPath, {
|
|
136
|
+
fs: opts.fs,
|
|
137
|
+
process: opts.process,
|
|
138
|
+
});
|
|
139
|
+
if (acquired)
|
|
140
|
+
return { acquired: true };
|
|
141
|
+
// Check timeout
|
|
142
|
+
const elapsed = now() - startTime;
|
|
143
|
+
if (elapsed >= opts.timeoutMs) {
|
|
144
|
+
return { acquired: false, error: "lock timeout" };
|
|
145
|
+
}
|
|
146
|
+
await sleep(backoff);
|
|
147
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=lockfile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lockfile.js","sourceRoot":"","sources":["../../src/notifier/lockfile.ts"],"names":[],"mappings":"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"}
|