atabey-shared 0.1.0-beta.0 → 0.1.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/README.md +63 -24
- package/dist/agent-identity.d.ts +1 -34
- package/dist/agent-identity.js +1 -76
- package/dist/agent-identity.js.map +1 -1
- package/dist/api-catalog.d.ts +1 -57
- package/dist/api-catalog.js +1 -253
- package/dist/api-catalog.js.map +1 -1
- package/dist/audit-chain.d.ts +1 -80
- package/dist/audit-chain.js +1 -261
- package/dist/audit-chain.js.map +1 -1
- package/dist/audit.d.ts +1 -121
- package/dist/audit.js +1 -321
- package/dist/audit.js.map +1 -1
- package/dist/constants.d.ts +1 -153
- package/dist/constants.js +1 -214
- package/dist/constants.js.map +1 -1
- package/dist/database.d.ts +1 -5
- package/dist/database.js +1 -12
- package/dist/database.js.map +1 -1
- package/dist/errors.d.ts +1 -46
- package/dist/errors.js +1 -68
- package/dist/errors.js.map +1 -1
- package/dist/file-lock.d.ts +1 -93
- package/dist/file-lock.js +1 -239
- package/dist/file-lock.js.map +1 -1
- package/dist/fs.d.ts +1 -9
- package/dist/fs.js +1 -53
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +2 -31
- package/dist/index.js +2 -31
- package/dist/index.js.map +1 -1
- package/dist/job-queue.d.ts +1 -53
- package/dist/job-queue.js +1 -126
- package/dist/job-queue.js.map +1 -1
- package/dist/lock.d.ts +1 -82
- package/dist/lock.js +1 -158
- package/dist/lock.js.map +1 -1
- package/dist/logger.d.ts +1 -31
- package/dist/logger.js +1 -152
- package/dist/logger.js.map +1 -1
- package/dist/memory-prune.d.ts +1 -18
- package/dist/memory-prune.js +1 -47
- package/dist/memory-prune.js.map +1 -1
- package/dist/migrations.d.ts +1 -69
- package/dist/migrations.js +1 -374
- package/dist/migrations.js.map +1 -1
- package/dist/modules/agent/agent-identity.d.ts +34 -0
- package/dist/modules/agent/agent-identity.js +77 -0
- package/dist/modules/agent/agent-identity.js.map +1 -0
- package/dist/modules/agent/api-catalog.d.ts +57 -0
- package/dist/modules/agent/api-catalog.js +254 -0
- package/dist/modules/agent/api-catalog.js.map +1 -0
- package/dist/modules/agent/index.d.ts +3 -0
- package/dist/modules/agent/index.js +4 -0
- package/dist/modules/agent/index.js.map +1 -0
- package/dist/modules/agent/tokenizer.d.ts +54 -0
- package/dist/modules/agent/tokenizer.js +272 -0
- package/dist/modules/agent/tokenizer.js.map +1 -0
- package/dist/modules/concurrency/file-lock.d.ts +93 -0
- package/dist/modules/concurrency/file-lock.js +240 -0
- package/dist/modules/concurrency/file-lock.js.map +1 -0
- package/dist/modules/concurrency/index.d.ts +6 -0
- package/dist/modules/concurrency/index.js +7 -0
- package/dist/modules/concurrency/index.js.map +1 -0
- package/dist/modules/concurrency/job-queue.d.ts +76 -0
- package/dist/modules/concurrency/job-queue.js +200 -0
- package/dist/modules/concurrency/job-queue.js.map +1 -0
- package/dist/modules/concurrency/lock.d.ts +82 -0
- package/dist/modules/concurrency/lock.js +159 -0
- package/dist/modules/concurrency/lock.js.map +1 -0
- package/dist/modules/concurrency/resilience.d.ts +84 -0
- package/dist/modules/concurrency/resilience.js +217 -0
- package/dist/modules/concurrency/resilience.js.map +1 -0
- package/dist/modules/concurrency/worker-pool.d.ts +52 -0
- package/dist/modules/concurrency/worker-pool.js +229 -0
- package/dist/modules/concurrency/worker-pool.js.map +1 -0
- package/dist/modules/concurrency/worker-thread.d.ts +21 -0
- package/dist/modules/concurrency/worker-thread.js +67 -0
- package/dist/modules/concurrency/worker-thread.js.map +1 -0
- package/dist/modules/index.d.ts +14 -0
- package/dist/modules/index.js +15 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/observability/audit-chain.d.ts +80 -0
- package/dist/modules/observability/audit-chain.js +262 -0
- package/dist/modules/observability/audit-chain.js.map +1 -0
- package/dist/modules/observability/audit.d.ts +121 -0
- package/dist/modules/observability/audit.js +322 -0
- package/dist/modules/observability/audit.js.map +1 -0
- package/dist/modules/observability/index.d.ts +4 -0
- package/dist/modules/observability/index.js +5 -0
- package/dist/modules/observability/index.js.map +1 -0
- package/dist/modules/observability/logger.d.ts +31 -0
- package/dist/modules/observability/logger.js +153 -0
- package/dist/modules/observability/logger.js.map +1 -0
- package/dist/modules/observability/otel.d.ts +39 -0
- package/dist/modules/observability/otel.js +93 -0
- package/dist/modules/observability/otel.js.map +1 -0
- package/dist/modules/primitives/constants.d.ts +150 -0
- package/dist/modules/primitives/constants.js +212 -0
- package/dist/modules/primitives/constants.js.map +1 -0
- package/dist/modules/primitives/errors.d.ts +46 -0
- package/dist/modules/primitives/errors.js +69 -0
- package/dist/modules/primitives/errors.js.map +1 -0
- package/dist/modules/primitives/fs.d.ts +9 -0
- package/dist/modules/primitives/fs.js +54 -0
- package/dist/modules/primitives/fs.js.map +1 -0
- package/dist/modules/primitives/index.d.ts +7 -0
- package/dist/modules/primitives/index.js +8 -0
- package/dist/modules/primitives/index.js.map +1 -0
- package/dist/modules/primitives/outbound-http.d.ts +32 -0
- package/dist/modules/primitives/outbound-http.js +88 -0
- package/dist/modules/primitives/outbound-http.js.map +1 -0
- package/dist/modules/primitives/string.d.ts +1 -0
- package/dist/modules/primitives/string.js +30 -0
- package/dist/modules/primitives/string.js.map +1 -0
- package/dist/modules/primitives/time.d.ts +5 -0
- package/dist/modules/primitives/time.js +35 -0
- package/dist/modules/primitives/time.js.map +1 -0
- package/dist/modules/primitives/types.d.ts +27 -0
- package/dist/modules/primitives/types.js +17 -0
- package/dist/modules/primitives/types.js.map +1 -0
- package/dist/modules/sandbox/index.d.ts +3 -0
- package/dist/modules/sandbox/index.js +4 -0
- package/dist/modules/sandbox/index.js.map +1 -0
- package/dist/modules/sandbox/sandbox-fs.d.ts +28 -0
- package/dist/modules/sandbox/sandbox-fs.js +143 -0
- package/dist/modules/sandbox/sandbox-fs.js.map +1 -0
- package/dist/modules/sandbox/sandbox-runtime.d.ts +78 -0
- package/dist/modules/sandbox/sandbox-runtime.js +282 -0
- package/dist/modules/sandbox/sandbox-runtime.js.map +1 -0
- package/dist/modules/sandbox/sandbox.d.ts +54 -0
- package/dist/modules/sandbox/sandbox.js +179 -0
- package/dist/modules/sandbox/sandbox.js.map +1 -0
- package/dist/modules/security/index.d.ts +5 -0
- package/dist/modules/security/index.js +6 -0
- package/dist/modules/security/index.js.map +1 -0
- package/dist/modules/security/pii.d.ts +70 -0
- package/dist/modules/security/pii.js +276 -0
- package/dist/modules/security/pii.js.map +1 -0
- package/dist/modules/security/policy-gate.d.ts +42 -0
- package/dist/modules/security/policy-gate.js +172 -0
- package/dist/modules/security/policy-gate.js.map +1 -0
- package/dist/modules/security/retention.d.ts +68 -0
- package/dist/modules/security/retention.js +218 -0
- package/dist/modules/security/retention.js.map +1 -0
- package/dist/modules/security/secrets.d.ts +99 -0
- package/dist/modules/security/secrets.js +371 -0
- package/dist/modules/security/secrets.js.map +1 -0
- package/dist/modules/security/tls.d.ts +87 -0
- package/dist/modules/security/tls.js +154 -0
- package/dist/modules/security/tls.js.map +1 -0
- package/dist/modules/storage/database.d.ts +6 -0
- package/dist/modules/storage/database.js +16 -0
- package/dist/modules/storage/database.js.map +1 -0
- package/dist/modules/storage/index.d.ts +4 -0
- package/dist/modules/storage/index.js +5 -0
- package/dist/modules/storage/index.js.map +1 -0
- package/dist/modules/storage/memory-prune.d.ts +18 -0
- package/dist/modules/storage/memory-prune.js +48 -0
- package/dist/modules/storage/memory-prune.js.map +1 -0
- package/dist/modules/storage/migrations.d.ts +69 -0
- package/dist/modules/storage/migrations.js +394 -0
- package/dist/modules/storage/migrations.js.map +1 -0
- package/dist/modules/storage/storage.d.ts +341 -0
- package/dist/modules/storage/storage.js +1004 -0
- package/dist/modules/storage/storage.js.map +1 -0
- package/dist/otel.d.ts +1 -39
- package/dist/otel.js +1 -92
- package/dist/otel.js.map +1 -1
- package/dist/outbound-http.d.ts +1 -32
- package/dist/outbound-http.js +1 -87
- package/dist/outbound-http.js.map +1 -1
- package/dist/pii.d.ts +1 -70
- package/dist/pii.js +1 -275
- package/dist/pii.js.map +1 -1
- package/dist/policy-gate.d.ts +1 -42
- package/dist/policy-gate.js +1 -171
- package/dist/policy-gate.js.map +1 -1
- package/dist/resilience.d.ts +1 -84
- package/dist/resilience.js +1 -216
- package/dist/resilience.js.map +1 -1
- package/dist/retention.d.ts +1 -68
- package/dist/retention.js +1 -217
- package/dist/retention.js.map +1 -1
- package/dist/sandbox-fs.d.ts +1 -28
- package/dist/sandbox-fs.js +1 -142
- package/dist/sandbox-fs.js.map +1 -1
- package/dist/sandbox-runtime.d.ts +1 -78
- package/dist/sandbox-runtime.js +1 -281
- package/dist/sandbox-runtime.js.map +1 -1
- package/dist/sandbox.d.ts +1 -54
- package/dist/sandbox.js +1 -178
- package/dist/sandbox.js.map +1 -1
- package/dist/secrets.d.ts +1 -99
- package/dist/secrets.js +1 -370
- package/dist/secrets.js.map +1 -1
- package/dist/storage.d.ts +1 -341
- package/dist/storage.js +1 -1003
- package/dist/storage.js.map +1 -1
- package/dist/string.d.ts +1 -1
- package/dist/string.js +1 -29
- package/dist/string.js.map +1 -1
- package/dist/time.d.ts +1 -5
- package/dist/time.js +1 -34
- package/dist/time.js.map +1 -1
- package/dist/tls.d.ts +1 -87
- package/dist/tls.js +1 -153
- package/dist/tls.js.map +1 -1
- package/dist/tokenizer.d.ts +1 -54
- package/dist/tokenizer.js +1 -271
- package/dist/tokenizer.js.map +1 -1
- package/dist/types.d.ts +1 -27
- package/dist/types.js +1 -16
- package/dist/types.js.map +1 -1
- package/dist/worker-pool.d.ts +1 -52
- package/dist/worker-pool.js +1 -228
- package/dist/worker-pool.js.map +1 -1
- package/dist/worker-thread.d.ts +1 -21
- package/dist/worker-thread.js +1 -66
- package/dist/worker-thread.js.map +1 -1
- package/package.json +44 -34
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic File-Based Locking — OS-level exclusive create via open('wx').
|
|
3
|
+
*
|
|
4
|
+
* Replaces check-then-act lock races. No Redis/Redlock required.
|
|
5
|
+
*
|
|
6
|
+
* Pattern:
|
|
7
|
+
* resource → .atabey/locks/<relative>.lock (created with O_EXCL / 'wx')
|
|
8
|
+
* holder writes JSON metadata after exclusive create
|
|
9
|
+
* release unlinks the lock file
|
|
10
|
+
*
|
|
11
|
+
* Stale locks (TTL exceeded) are reclaimed atomically when possible.
|
|
12
|
+
*/
|
|
13
|
+
export interface FileLockOptions {
|
|
14
|
+
/** Base directory for lock files. Default: `.atabey/locks` under cwd or projectRoot. */
|
|
15
|
+
locksDir?: string;
|
|
16
|
+
/** Project root used to relativize resource paths. Default: process.cwd(). */
|
|
17
|
+
projectRoot?: string;
|
|
18
|
+
/** Lock TTL in ms. Stale locks may be stolen after this. Default: 5 minutes. */
|
|
19
|
+
ttlMs?: number;
|
|
20
|
+
/** Retry interval when waiting for a lock. Default: 50ms. */
|
|
21
|
+
retryMs?: number;
|
|
22
|
+
/** Max wait when using acquireAsync. Default: 10s. */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface FileLockMeta {
|
|
26
|
+
ownerId: string;
|
|
27
|
+
resource: string;
|
|
28
|
+
acquiredAt: string;
|
|
29
|
+
expiresAt: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
pid: number;
|
|
32
|
+
hostname: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class AtomicFileLock {
|
|
35
|
+
private readonly locksDir;
|
|
36
|
+
private readonly projectRoot;
|
|
37
|
+
private readonly ttlMs;
|
|
38
|
+
private readonly retryMs;
|
|
39
|
+
private readonly timeoutMs;
|
|
40
|
+
constructor(options?: FileLockOptions);
|
|
41
|
+
/** Absolute path of the lock file for a resource. */
|
|
42
|
+
getLockPath(resource: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Try to acquire a lock once (non-blocking).
|
|
45
|
+
* Uses atomic exclusive file create (`wx` = O_CREAT | O_EXCL | O_WRONLY).
|
|
46
|
+
*/
|
|
47
|
+
tryAcquire(resource: string, ownerId: string, reason?: string, _reclaimDepth?: number): {
|
|
48
|
+
acquired: boolean;
|
|
49
|
+
meta?: FileLockMeta;
|
|
50
|
+
heldBy?: FileLockMeta;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Acquire with async retry until timeout.
|
|
54
|
+
*/
|
|
55
|
+
acquireAsync(resource: string, ownerId: string, options?: {
|
|
56
|
+
reason?: string;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
retryMs?: number;
|
|
59
|
+
}): Promise<FileLockMeta>;
|
|
60
|
+
/**
|
|
61
|
+
* Release lock only if owned by ownerId (or force=true).
|
|
62
|
+
*/
|
|
63
|
+
release(resource: string, ownerId: string, force?: boolean): boolean;
|
|
64
|
+
isLocked(resource: string): FileLockMeta | null;
|
|
65
|
+
/**
|
|
66
|
+
* Run `fn` while holding the lock.
|
|
67
|
+
* ALWAYS releases in `finally` even if `fn` throws (disk full, parse errors, etc.).
|
|
68
|
+
* Without this, a leftover `.lock` file would deadlock every subsequent writer until TTL.
|
|
69
|
+
*
|
|
70
|
+
* Prefer this API over bare acquire/release for any mutating work.
|
|
71
|
+
*/
|
|
72
|
+
withLock<T>(resource: string, ownerId: string, fn: () => Promise<T> | T, options?: {
|
|
73
|
+
reason?: string;
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
}): Promise<T>;
|
|
76
|
+
/**
|
|
77
|
+
* Synchronous withLock — same try/finally release guarantee.
|
|
78
|
+
*/
|
|
79
|
+
withLockSync<T>(resource: string, ownerId: string, fn: () => T, options?: {
|
|
80
|
+
reason?: string;
|
|
81
|
+
}): T;
|
|
82
|
+
private reclaimIfStale;
|
|
83
|
+
/**
|
|
84
|
+
* True when an existing lock file may be safely stolen: unparseable/empty
|
|
85
|
+
* (corrupt, e.g. crashed mid-write) or past its TTL.
|
|
86
|
+
*/
|
|
87
|
+
private isReclaimable;
|
|
88
|
+
private isExpired;
|
|
89
|
+
private readMeta;
|
|
90
|
+
private forceRemove;
|
|
91
|
+
}
|
|
92
|
+
/** Convenience: lock under project `.atabey/locks`. */
|
|
93
|
+
export declare function createProjectFileLock(projectRoot: string, options?: Omit<FileLockOptions, "projectRoot">): AtomicFileLock;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic File-Based Locking — OS-level exclusive create via open('wx').
|
|
3
|
+
*
|
|
4
|
+
* Replaces check-then-act lock races. No Redis/Redlock required.
|
|
5
|
+
*
|
|
6
|
+
* Pattern:
|
|
7
|
+
* resource → .atabey/locks/<relative>.lock (created with O_EXCL / 'wx')
|
|
8
|
+
* holder writes JSON metadata after exclusive create
|
|
9
|
+
* release unlinks the lock file
|
|
10
|
+
*
|
|
11
|
+
* Stale locks (TTL exceeded) are reclaimed atomically when possible.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import os from "os";
|
|
15
|
+
import path from "path";
|
|
16
|
+
export class AtomicFileLock {
|
|
17
|
+
locksDir;
|
|
18
|
+
projectRoot;
|
|
19
|
+
ttlMs;
|
|
20
|
+
retryMs;
|
|
21
|
+
timeoutMs;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.projectRoot = options.projectRoot ?? process.cwd();
|
|
24
|
+
this.locksDir = options.locksDir
|
|
25
|
+
? path.resolve(options.locksDir)
|
|
26
|
+
: path.join(this.projectRoot, ".atabey", "locks");
|
|
27
|
+
this.ttlMs = options.ttlMs ?? 5 * 60 * 1000;
|
|
28
|
+
this.retryMs = options.retryMs ?? 50;
|
|
29
|
+
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
30
|
+
}
|
|
31
|
+
/** Absolute path of the lock file for a resource. */
|
|
32
|
+
getLockPath(resource) {
|
|
33
|
+
const abs = path.isAbsolute(resource)
|
|
34
|
+
? resource
|
|
35
|
+
: path.resolve(this.projectRoot, resource);
|
|
36
|
+
let relative = path.relative(this.projectRoot, abs);
|
|
37
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
38
|
+
// Outside project root — hash path to avoid escaping locks dir
|
|
39
|
+
relative = `_ext/${Buffer.from(abs).toString("base64url")}`;
|
|
40
|
+
}
|
|
41
|
+
// Normalize separators for cross-platform lock paths
|
|
42
|
+
relative = relative.split(path.sep).join("/");
|
|
43
|
+
return path.join(this.locksDir, `${relative}.lock`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Try to acquire a lock once (non-blocking).
|
|
47
|
+
* Uses atomic exclusive file create (`wx` = O_CREAT | O_EXCL | O_WRONLY).
|
|
48
|
+
*/
|
|
49
|
+
tryAcquire(resource, ownerId, reason, _reclaimDepth = 0) {
|
|
50
|
+
const lockPath = this.getLockPath(resource);
|
|
51
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
52
|
+
// Reclaim stale lock first (best-effort, still atomic on create)
|
|
53
|
+
this.reclaimIfStale(lockPath);
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const meta = {
|
|
56
|
+
ownerId,
|
|
57
|
+
resource,
|
|
58
|
+
acquiredAt: new Date(now).toISOString(),
|
|
59
|
+
expiresAt: new Date(now + this.ttlMs).toISOString(),
|
|
60
|
+
reason,
|
|
61
|
+
pid: process.pid,
|
|
62
|
+
hostname: os.hostname(),
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
// Atomic exclusive create WITH content in one syscall sequence
|
|
66
|
+
// (writeFileSync + flag 'wx' = O_CREAT|O_EXCL|O_WRONLY): there is no
|
|
67
|
+
// window where the lock file exists empty for a reader to misread.
|
|
68
|
+
fs.writeFileSync(lockPath, JSON.stringify(meta, null, 2), { encoding: "utf8", flag: "wx" });
|
|
69
|
+
return { acquired: true, meta };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const code = err.code;
|
|
73
|
+
if (code === "EEXIST") {
|
|
74
|
+
// Another process holds it. Reclaim only if corrupt/empty or expired.
|
|
75
|
+
const heldBy = this.readMeta(lockPath) ?? undefined;
|
|
76
|
+
if ((!heldBy || this.isExpired(heldBy)) && _reclaimDepth < 1) {
|
|
77
|
+
this.forceRemove(lockPath, heldBy ?? null);
|
|
78
|
+
return this.tryAcquire(resource, ownerId, reason, _reclaimDepth + 1);
|
|
79
|
+
}
|
|
80
|
+
return { acquired: false, heldBy };
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Acquire with async retry until timeout.
|
|
87
|
+
*/
|
|
88
|
+
async acquireAsync(resource, ownerId, options) {
|
|
89
|
+
const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
|
|
90
|
+
const retryMs = options?.retryMs ?? this.retryMs;
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
while (true) {
|
|
93
|
+
const result = this.tryAcquire(resource, ownerId, options?.reason);
|
|
94
|
+
if (result.acquired && result.meta) {
|
|
95
|
+
return result.meta;
|
|
96
|
+
}
|
|
97
|
+
if (Date.now() - start >= timeoutMs) {
|
|
98
|
+
const holder = result.heldBy?.ownerId ?? "unknown";
|
|
99
|
+
throw new Error(`[AtomicFileLock] Timeout acquiring lock for "${resource}" after ${timeoutMs}ms (held by ${holder})`);
|
|
100
|
+
}
|
|
101
|
+
await sleep(retryMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Release lock only if owned by ownerId (or force=true).
|
|
106
|
+
*/
|
|
107
|
+
release(resource, ownerId, force = false) {
|
|
108
|
+
const lockPath = this.getLockPath(resource);
|
|
109
|
+
if (!fs.existsSync(lockPath))
|
|
110
|
+
return true;
|
|
111
|
+
const meta = this.readMeta(lockPath);
|
|
112
|
+
if (!meta) {
|
|
113
|
+
// Corrupt/empty lock — safe to drop (atomic create means a real
|
|
114
|
+
// holder always has readable meta).
|
|
115
|
+
this.forceRemove(lockPath, null);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (!force && meta.ownerId !== ownerId) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
this.forceRemove(lockPath, meta);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
isLocked(resource) {
|
|
125
|
+
const lockPath = this.getLockPath(resource);
|
|
126
|
+
if (!fs.existsSync(lockPath))
|
|
127
|
+
return null;
|
|
128
|
+
const meta = this.readMeta(lockPath);
|
|
129
|
+
if (!meta)
|
|
130
|
+
return null;
|
|
131
|
+
if (this.isExpired(meta)) {
|
|
132
|
+
this.forceRemove(lockPath, meta);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return meta;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Run `fn` while holding the lock.
|
|
139
|
+
* ALWAYS releases in `finally` even if `fn` throws (disk full, parse errors, etc.).
|
|
140
|
+
* Without this, a leftover `.lock` file would deadlock every subsequent writer until TTL.
|
|
141
|
+
*
|
|
142
|
+
* Prefer this API over bare acquire/release for any mutating work.
|
|
143
|
+
*/
|
|
144
|
+
async withLock(resource, ownerId, fn, options) {
|
|
145
|
+
await this.acquireAsync(resource, ownerId, options);
|
|
146
|
+
try {
|
|
147
|
+
return await fn();
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
// Never let release errors mask the original failure or leave a lock.
|
|
151
|
+
try {
|
|
152
|
+
this.release(resource, ownerId);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
this.forceRemove(this.getLockPath(resource), null);
|
|
156
|
+
}
|
|
157
|
+
// Belt-and-suspenders: if our lock is somehow still present, force drop it.
|
|
158
|
+
const stillHeld = this.isLocked(resource);
|
|
159
|
+
if (stillHeld && stillHeld.ownerId === ownerId) {
|
|
160
|
+
this.forceRemove(this.getLockPath(resource), stillHeld);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Synchronous withLock — same try/finally release guarantee.
|
|
166
|
+
*/
|
|
167
|
+
withLockSync(resource, ownerId, fn, options) {
|
|
168
|
+
const result = this.tryAcquire(resource, ownerId, options?.reason);
|
|
169
|
+
if (!result.acquired) {
|
|
170
|
+
const holder = result.heldBy?.ownerId ?? "unknown";
|
|
171
|
+
throw new Error(`[AtomicFileLock] Could not acquire lock for "${resource}" (held by ${holder})`);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return fn();
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
try {
|
|
178
|
+
this.release(resource, ownerId);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
this.forceRemove(this.getLockPath(resource), null);
|
|
182
|
+
}
|
|
183
|
+
const stillHeld = this.isLocked(resource);
|
|
184
|
+
if (stillHeld && stillHeld.ownerId === ownerId) {
|
|
185
|
+
this.forceRemove(this.getLockPath(resource), stillHeld);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
reclaimIfStale(lockPath) {
|
|
190
|
+
if (!fs.existsSync(lockPath))
|
|
191
|
+
return;
|
|
192
|
+
if (this.isReclaimable(lockPath)) {
|
|
193
|
+
this.forceRemove(lockPath, this.readMeta(lockPath));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* True when an existing lock file may be safely stolen: unparseable/empty
|
|
198
|
+
* (corrupt, e.g. crashed mid-write) or past its TTL.
|
|
199
|
+
*/
|
|
200
|
+
isReclaimable(lockPath) {
|
|
201
|
+
const meta = this.readMeta(lockPath);
|
|
202
|
+
if (!meta)
|
|
203
|
+
return true;
|
|
204
|
+
return this.isExpired(meta);
|
|
205
|
+
}
|
|
206
|
+
isExpired(meta) {
|
|
207
|
+
const exp = Date.parse(meta.expiresAt);
|
|
208
|
+
if (Number.isNaN(exp)) {
|
|
209
|
+
// Fallback: acquiredAt + ttl
|
|
210
|
+
const acq = Date.parse(meta.acquiredAt);
|
|
211
|
+
return Number.isNaN(acq) ? true : Date.now() - acq > this.ttlMs;
|
|
212
|
+
}
|
|
213
|
+
return Date.now() > exp;
|
|
214
|
+
}
|
|
215
|
+
readMeta(lockPath) {
|
|
216
|
+
try {
|
|
217
|
+
const raw = fs.readFileSync(lockPath, "utf8");
|
|
218
|
+
return JSON.parse(raw);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
forceRemove(lockPath, _meta) {
|
|
225
|
+
try {
|
|
226
|
+
fs.unlinkSync(lockPath);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// ignore ENOENT races
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function sleep(ms) {
|
|
234
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
235
|
+
}
|
|
236
|
+
/** Convenience: lock under project `.atabey/locks`. */
|
|
237
|
+
export function createProjectFileLock(projectRoot, options) {
|
|
238
|
+
return new AtomicFileLock({ ...options, projectRoot });
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.js","sourceRoot":"","sources":["../../../src/modules/concurrency/file-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/concurrency/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Queue — concurrency-limited async work dispatcher backed by SQLite database persistence.
|
|
3
|
+
*
|
|
4
|
+
* Prevents unbounded parallel LLM / agent work from ballooning memory
|
|
5
|
+
* or starving the main event loop. Job lifecycles (pending, running, completed, failed)
|
|
6
|
+
* are recorded in the database when initialized, providing persistence and auditability.
|
|
7
|
+
*/
|
|
8
|
+
export type JobStatus = "pending" | "running" | "completed" | "failed";
|
|
9
|
+
export interface JobRecord {
|
|
10
|
+
id: number;
|
|
11
|
+
queue_name: string;
|
|
12
|
+
payload: string;
|
|
13
|
+
status: JobStatus;
|
|
14
|
+
retries: number;
|
|
15
|
+
max_retries: number;
|
|
16
|
+
error_message?: string | null;
|
|
17
|
+
created_at: string;
|
|
18
|
+
updated_at: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EnqueueOptions {
|
|
21
|
+
/** Optional JSON-serializable payload or description of the task */
|
|
22
|
+
payload?: unknown;
|
|
23
|
+
/** Max retries before permanent failure (default: 3) */
|
|
24
|
+
maxRetries?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface JobQueueOptions {
|
|
27
|
+
/** Max concurrent running jobs. Default: os.cpus().length (min 1, max 16). */
|
|
28
|
+
maxConcurrency?: number;
|
|
29
|
+
/** Optional name for diagnostics and DB tracking. */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** Called when a job fails (after the promise rejects to the caller). */
|
|
32
|
+
onError?: (error: unknown, jobId: number) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface JobQueueStats {
|
|
35
|
+
name: string;
|
|
36
|
+
maxConcurrency: number;
|
|
37
|
+
running: number;
|
|
38
|
+
pending: number;
|
|
39
|
+
completed: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class JobQueue {
|
|
43
|
+
private readonly maxConcurrency;
|
|
44
|
+
private readonly name;
|
|
45
|
+
private readonly onError?;
|
|
46
|
+
private readonly pending;
|
|
47
|
+
private running;
|
|
48
|
+
private nextId;
|
|
49
|
+
private completed;
|
|
50
|
+
private failed;
|
|
51
|
+
private closed;
|
|
52
|
+
constructor(options?: JobQueueOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Enqueue an async job. Resolves/rejects with the job result.
|
|
55
|
+
* Back-pressures via wait in the queue — does not spawn unbounded work.
|
|
56
|
+
* If SQLite is initialized, records job creation in the database.
|
|
57
|
+
*/
|
|
58
|
+
enqueue<T>(fn: () => Promise<T>, options?: EnqueueOptions): Promise<T>;
|
|
59
|
+
/** Retrieve a job record from the database by ID. */
|
|
60
|
+
getJobRecord(id: number): JobRecord | null;
|
|
61
|
+
/** Retrieve job records from the database for this queue. */
|
|
62
|
+
getQueueRecords(status?: JobStatus, limit?: number): JobRecord[];
|
|
63
|
+
/** Current queue diagnostics. */
|
|
64
|
+
getStats(): JobQueueStats;
|
|
65
|
+
/** Reject pending jobs and stop accepting new ones. In-flight jobs finish. */
|
|
66
|
+
close(reason?: string): void;
|
|
67
|
+
/** Wait until running + pending reach zero (or timeout). */
|
|
68
|
+
drain(timeoutMs?: number): Promise<void>;
|
|
69
|
+
private pump;
|
|
70
|
+
private execute;
|
|
71
|
+
}
|
|
72
|
+
/** Backward compatibility alias */
|
|
73
|
+
export { JobQueue as InMemoryJobQueue };
|
|
74
|
+
export declare function getAgentJobQueue(options?: JobQueueOptions): JobQueue;
|
|
75
|
+
/** Test helper — reset singleton. */
|
|
76
|
+
export declare function resetAgentJobQueue(): void;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Queue — concurrency-limited async work dispatcher backed by SQLite database persistence.
|
|
3
|
+
*
|
|
4
|
+
* Prevents unbounded parallel LLM / agent work from ballooning memory
|
|
5
|
+
* or starving the main event loop. Job lifecycles (pending, running, completed, failed)
|
|
6
|
+
* are recorded in the database when initialized, providing persistence and auditability.
|
|
7
|
+
*/
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { databaseHolder } from "../storage/database.js";
|
|
10
|
+
export class JobQueue {
|
|
11
|
+
maxConcurrency;
|
|
12
|
+
name;
|
|
13
|
+
onError;
|
|
14
|
+
pending = [];
|
|
15
|
+
running = 0;
|
|
16
|
+
nextId = 1;
|
|
17
|
+
completed = 0;
|
|
18
|
+
failed = 0;
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
const cpus = Math.max(1, os.cpus()?.length || 1);
|
|
22
|
+
const requested = options.maxConcurrency ?? cpus;
|
|
23
|
+
this.maxConcurrency = Math.max(1, Math.min(requested, 16));
|
|
24
|
+
this.name = options.name ?? "default";
|
|
25
|
+
this.onError = options.onError;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Enqueue an async job. Resolves/rejects with the job result.
|
|
29
|
+
* Back-pressures via wait in the queue — does not spawn unbounded work.
|
|
30
|
+
* If SQLite is initialized, records job creation in the database.
|
|
31
|
+
*/
|
|
32
|
+
enqueue(fn, options) {
|
|
33
|
+
if (this.closed) {
|
|
34
|
+
return Promise.reject(new Error(`[JobQueue:${this.name}] Queue is closed; cannot accept new jobs.`));
|
|
35
|
+
}
|
|
36
|
+
let dbId;
|
|
37
|
+
if (databaseHolder.hasDB()) {
|
|
38
|
+
try {
|
|
39
|
+
const db = databaseHolder.getDB();
|
|
40
|
+
const payloadStr = typeof options?.payload === "string"
|
|
41
|
+
? options.payload
|
|
42
|
+
: JSON.stringify(options?.payload ?? null);
|
|
43
|
+
const res = db.prepare(`INSERT INTO job_queue (queue_name, payload, status, retries, max_retries, created_at, updated_at)
|
|
44
|
+
VALUES (?, ?, 'pending', 0, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`).run(this.name, payloadStr, options?.maxRetries ?? 3);
|
|
45
|
+
dbId = Number(res.lastInsertRowid);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Table might not exist yet if running pre-migration
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const item = {
|
|
53
|
+
id: this.nextId++,
|
|
54
|
+
dbId,
|
|
55
|
+
run: fn,
|
|
56
|
+
resolve,
|
|
57
|
+
reject,
|
|
58
|
+
};
|
|
59
|
+
this.pending.push(item);
|
|
60
|
+
this.pump();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Retrieve a job record from the database by ID. */
|
|
64
|
+
getJobRecord(id) {
|
|
65
|
+
if (!databaseHolder.hasDB())
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const db = databaseHolder.getDB();
|
|
69
|
+
return db.prepare("SELECT * FROM job_queue WHERE id = ?").get(id) || null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Retrieve job records from the database for this queue. */
|
|
76
|
+
getQueueRecords(status, limit = 50) {
|
|
77
|
+
if (!databaseHolder.hasDB())
|
|
78
|
+
return [];
|
|
79
|
+
try {
|
|
80
|
+
const db = databaseHolder.getDB();
|
|
81
|
+
if (status) {
|
|
82
|
+
return db.prepare("SELECT * FROM job_queue WHERE queue_name = ? AND status = ? ORDER BY id DESC LIMIT ?").all(this.name, status, limit);
|
|
83
|
+
}
|
|
84
|
+
return db.prepare("SELECT * FROM job_queue WHERE queue_name = ? ORDER BY id DESC LIMIT ?").all(this.name, limit);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Current queue diagnostics. */
|
|
91
|
+
getStats() {
|
|
92
|
+
return {
|
|
93
|
+
name: this.name,
|
|
94
|
+
maxConcurrency: this.maxConcurrency,
|
|
95
|
+
running: this.running,
|
|
96
|
+
pending: this.pending.length,
|
|
97
|
+
completed: this.completed,
|
|
98
|
+
failed: this.failed,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Reject pending jobs and stop accepting new ones. In-flight jobs finish. */
|
|
102
|
+
close(reason = "Queue closed") {
|
|
103
|
+
this.closed = true;
|
|
104
|
+
while (this.pending.length > 0) {
|
|
105
|
+
const item = this.pending.shift();
|
|
106
|
+
item?.reject(new Error(`[JobQueue:${this.name}] ${reason}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Wait until running + pending reach zero (or timeout). */
|
|
110
|
+
async drain(timeoutMs = 30_000) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
while (this.running > 0 || this.pending.length > 0) {
|
|
113
|
+
if (Date.now() - start > timeoutMs) {
|
|
114
|
+
throw new Error(`[JobQueue:${this.name}] drain timed out after ${timeoutMs}ms`);
|
|
115
|
+
}
|
|
116
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
pump() {
|
|
120
|
+
while (this.running < this.maxConcurrency && this.pending.length > 0) {
|
|
121
|
+
const item = this.pending.shift();
|
|
122
|
+
if (!item)
|
|
123
|
+
break;
|
|
124
|
+
this.running++;
|
|
125
|
+
void this.execute(item);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async execute(item) {
|
|
129
|
+
if (item.dbId && databaseHolder.hasDB()) {
|
|
130
|
+
try {
|
|
131
|
+
const db = databaseHolder.getDB();
|
|
132
|
+
db.prepare("UPDATE job_queue SET status = 'running', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.dbId);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Ignore DB error during execution transition
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const result = await item.run();
|
|
140
|
+
this.completed++;
|
|
141
|
+
if (item.dbId && databaseHolder.hasDB()) {
|
|
142
|
+
try {
|
|
143
|
+
const db = databaseHolder.getDB();
|
|
144
|
+
db.prepare("UPDATE job_queue SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.dbId);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Ignore DB error
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
item.resolve(result);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
this.failed++;
|
|
154
|
+
if (item.dbId && databaseHolder.hasDB()) {
|
|
155
|
+
try {
|
|
156
|
+
const db = databaseHolder.getDB();
|
|
157
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
158
|
+
db.prepare("UPDATE job_queue SET status = 'failed', error_message = ?, retries = retries + 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(errMsg, item.dbId);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Ignore DB error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
this.onError?.(error, item.id);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// ignore listener errors
|
|
169
|
+
}
|
|
170
|
+
item.reject(error);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
this.running--;
|
|
174
|
+
this.pump();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Backward compatibility alias */
|
|
179
|
+
export { JobQueue as InMemoryJobQueue };
|
|
180
|
+
/** Shared singleton for agent orchestration (lazy). */
|
|
181
|
+
let agentJobQueue = null;
|
|
182
|
+
export function getAgentJobQueue(options) {
|
|
183
|
+
if (!agentJobQueue) {
|
|
184
|
+
const fromEnv = process.env.ATABEY_JOB_CONCURRENCY
|
|
185
|
+
? parseInt(process.env.ATABEY_JOB_CONCURRENCY, 10)
|
|
186
|
+
: undefined;
|
|
187
|
+
agentJobQueue = new JobQueue({
|
|
188
|
+
name: "agent",
|
|
189
|
+
maxConcurrency: options?.maxConcurrency ?? (Number.isFinite(fromEnv) ? fromEnv : undefined),
|
|
190
|
+
onError: options?.onError,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return agentJobQueue;
|
|
194
|
+
}
|
|
195
|
+
/** Test helper — reset singleton. */
|
|
196
|
+
export function resetAgentJobQueue() {
|
|
197
|
+
agentJobQueue?.close("reset");
|
|
198
|
+
agentJobQueue = null;
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=job-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-queue.js","sourceRoot":"","sources":["../../../src/modules/concurrency/job-queue.ts"],"names":[],"mappings":"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"}
|