@mmnto/totem 0.44.0 → 1.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/dist/ast-classifier.d.ts +8 -0
- package/dist/ast-classifier.d.ts.map +1 -1
- package/dist/ast-classifier.js +2 -2
- package/dist/ast-classifier.js.map +1 -1
- package/dist/ast-query.d.ts +18 -0
- package/dist/ast-query.d.ts.map +1 -0
- package/dist/ast-query.js +177 -0
- package/dist/ast-query.js.map +1 -0
- package/dist/ast-query.test.d.ts +2 -0
- package/dist/ast-query.test.d.ts.map +1 -0
- package/dist/ast-query.test.js +79 -0
- package/dist/ast-query.test.js.map +1 -0
- package/dist/compiler.d.ts +27 -11
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +94 -5
- package/dist/compiler.js.map +1 -1
- package/dist/compiler.test.js +2 -2
- package/dist/compiler.test.js.map +1 -1
- package/dist/config-schema.d.ts +8 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +4 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/embedders/ollama-embedder.d.ts.map +1 -1
- package/dist/embedders/ollama-embedder.js +5 -1
- package/dist/embedders/ollama-embedder.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/ingest/pipeline.d.ts +11 -0
- package/dist/ingest/pipeline.d.ts.map +1 -1
- package/dist/ingest/pipeline.js +64 -0
- package/dist/ingest/pipeline.js.map +1 -1
- package/dist/ingest/sync.d.ts +1 -1
- package/dist/ingest/sync.d.ts.map +1 -1
- package/dist/ingest/sync.js +1 -1
- package/dist/ingest/sync.js.map +1 -1
- package/dist/lock.d.ts +18 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +192 -0
- package/dist/lock.js.map +1 -0
- package/dist/lock.test.d.ts +2 -0
- package/dist/lock.test.d.ts.map +1 -0
- package/dist/lock.test.js +94 -0
- package/dist/lock.test.js.map +1 -0
- package/package.json +1 -1
package/dist/lock.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight filesystem lock for preventing concurrent mutations
|
|
3
|
+
* to .totem/lessons/ and .lancedb/ directories.
|
|
4
|
+
*
|
|
5
|
+
* Uses a lockfile with PID + timestamp. Stale locks (>30s) are
|
|
6
|
+
* automatically cleaned up to handle crashed processes.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
const LOCK_FILE = 'sync.lock';
|
|
11
|
+
const STALE_THRESHOLD_MS = 120_000; // 2 minutes — sync can take 30-60s on large repos
|
|
12
|
+
const MAX_RETRIES = 20;
|
|
13
|
+
const BASE_DELAY_MS = 500;
|
|
14
|
+
/**
|
|
15
|
+
* Check if a process is still running via signal 0 (no-op probe).
|
|
16
|
+
* Returns true if alive, false if dead (ESRCH), true on permission errors (assume alive).
|
|
17
|
+
*/
|
|
18
|
+
function isProcessAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
// Signal 0 does not terminate — it only checks if the process exists
|
|
21
|
+
process.kill(pid, 0); // totem-ignore: signal-0 probe, not a kill
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return err.code !== 'ESRCH';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function lockPath(totemDir) {
|
|
29
|
+
return path.join(totemDir, LOCK_FILE);
|
|
30
|
+
}
|
|
31
|
+
function isStale(data) {
|
|
32
|
+
return Date.now() - data.timestamp > STALE_THRESHOLD_MS;
|
|
33
|
+
}
|
|
34
|
+
const MAX_BACKOFF_EXPONENT = 5;
|
|
35
|
+
function backoffDelay(attempt) {
|
|
36
|
+
return BASE_DELAY_MS * Math.pow(2, Math.min(attempt, MAX_BACKOFF_EXPONENT));
|
|
37
|
+
}
|
|
38
|
+
function readLock(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
const data = JSON.parse(raw);
|
|
42
|
+
if (data !== null &&
|
|
43
|
+
typeof data === 'object' &&
|
|
44
|
+
typeof data.pid === 'number' &&
|
|
45
|
+
typeof data.timestamp === 'number') {
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Attempt to acquire the sync lock.
|
|
56
|
+
* Returns a release function on success, or throws after retries are exhausted.
|
|
57
|
+
*/
|
|
58
|
+
export async function acquireLock(totemDir, onWarn) {
|
|
59
|
+
const file = lockPath(totemDir);
|
|
60
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
61
|
+
const existing = readLock(file);
|
|
62
|
+
// Corrupted lockfile (empty, bad JSON) — remove if not mid-write
|
|
63
|
+
if (!existing && fs.existsSync(file)) {
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(file);
|
|
66
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
67
|
+
if (ageMs > 1000) {
|
|
68
|
+
// File is old enough to be genuinely corrupted, not mid-write
|
|
69
|
+
fs.unlinkSync(file);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// File may be mid-write by another process — wait and retry
|
|
73
|
+
const delay = backoffDelay(attempt);
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Another process may have cleaned it up
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (existing) {
|
|
83
|
+
if (isStale(existing)) {
|
|
84
|
+
// Stale timestamp is the final authority — delete regardless of PID liveness.
|
|
85
|
+
// Per lesson-5b1929d1: PID checks fail across container namespaces.
|
|
86
|
+
onWarn?.(`Removing stale lock from PID ${existing.pid} (${Math.round((Date.now() - existing.timestamp) / 1000)}s old)`);
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(file);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Another process may have cleaned it up
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (!isProcessAlive(existing.pid)) {
|
|
95
|
+
// Lock is fresh but owning process is dead (e.g., Ctrl+C during sync)
|
|
96
|
+
onWarn?.(`Removing orphaned lock from dead PID ${existing.pid} (${Math.round((Date.now() - existing.timestamp) / 1000)}s old)`);
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(file);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Another process may have cleaned it up
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Lock is held by a live process — wait and retry
|
|
106
|
+
if (attempt === 0) {
|
|
107
|
+
onWarn?.(`Waiting for sync lock (held by PID ${existing.pid})...`);
|
|
108
|
+
}
|
|
109
|
+
const delay = backoffDelay(attempt);
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Try to acquire (ensure directory exists)
|
|
115
|
+
const lockData = { pid: process.pid, timestamp: Date.now() };
|
|
116
|
+
try {
|
|
117
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
118
|
+
fs.writeFileSync(file, JSON.stringify(lockData), { flag: 'wx' });
|
|
119
|
+
// Acquired — return release function
|
|
120
|
+
return () => {
|
|
121
|
+
try {
|
|
122
|
+
fs.unlinkSync(file);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Already cleaned up
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// EEXIST = another process grabbed it between our read and write
|
|
131
|
+
const code = err.code;
|
|
132
|
+
if (code === 'EEXIST') {
|
|
133
|
+
const delay = backoffDelay(attempt);
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`[Totem Error] Could not acquire sync lock after ${MAX_RETRIES} attempts. ` +
|
|
141
|
+
`Another totem process may be running. Check ${file} or delete it manually.`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Execute a function while holding the sync lock.
|
|
145
|
+
* Automatically releases the lock when done (even on error).
|
|
146
|
+
*/
|
|
147
|
+
export async function withLock(totemDir, fn, onWarn) {
|
|
148
|
+
const release = await acquireLock(totemDir, onWarn);
|
|
149
|
+
// Best-effort cleanup on process termination (Ctrl+C, kill)
|
|
150
|
+
const removeHandlers = () => {
|
|
151
|
+
process.removeListener('SIGINT', onSigint);
|
|
152
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
153
|
+
if (process.platform !== 'win32') {
|
|
154
|
+
process.removeListener('SIGHUP', onSighup);
|
|
155
|
+
process.removeListener('SIGQUIT', onSigquit);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const onSigint = () => {
|
|
159
|
+
release();
|
|
160
|
+
removeHandlers();
|
|
161
|
+
process.kill(process.pid, 'SIGINT'); // totem-ignore: re-raising caught signal
|
|
162
|
+
};
|
|
163
|
+
const onSigterm = () => {
|
|
164
|
+
release();
|
|
165
|
+
removeHandlers();
|
|
166
|
+
process.kill(process.pid, 'SIGTERM'); // totem-ignore: re-raising caught signal
|
|
167
|
+
};
|
|
168
|
+
const onSighup = () => {
|
|
169
|
+
release();
|
|
170
|
+
removeHandlers();
|
|
171
|
+
process.kill(process.pid, 'SIGHUP'); // totem-ignore: re-raising caught signal
|
|
172
|
+
};
|
|
173
|
+
const onSigquit = () => {
|
|
174
|
+
release();
|
|
175
|
+
removeHandlers();
|
|
176
|
+
process.kill(process.pid, 'SIGQUIT'); // totem-ignore: re-raising caught signal
|
|
177
|
+
};
|
|
178
|
+
process.on('SIGINT', onSigint);
|
|
179
|
+
process.on('SIGTERM', onSigterm);
|
|
180
|
+
if (process.platform !== 'win32') {
|
|
181
|
+
process.on('SIGHUP', onSighup);
|
|
182
|
+
process.on('SIGQUIT', onSigquit);
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return await fn();
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
removeHandlers();
|
|
189
|
+
release();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=lock.js.map
|
package/dist/lock.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.test.d.ts","sourceRoot":"","sources":["../src/lock.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { acquireLock, withLock } from './lock.js';
|
|
6
|
+
describe('acquireLock', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-lock-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('acquires and releases a lock', async () => {
|
|
15
|
+
const release = await acquireLock(tmpDir);
|
|
16
|
+
expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(true);
|
|
17
|
+
release();
|
|
18
|
+
expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('second caller waits and succeeds after first releases', async () => {
|
|
21
|
+
const release1 = await acquireLock(tmpDir);
|
|
22
|
+
// Start second acquisition (will wait)
|
|
23
|
+
let acquired2 = false;
|
|
24
|
+
const promise2 = acquireLock(tmpDir).then((release) => {
|
|
25
|
+
acquired2 = true;
|
|
26
|
+
return release;
|
|
27
|
+
});
|
|
28
|
+
// Second should still be waiting
|
|
29
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
30
|
+
expect(acquired2).toBe(false);
|
|
31
|
+
// Release first — second should acquire
|
|
32
|
+
release1();
|
|
33
|
+
const release2 = await promise2;
|
|
34
|
+
expect(acquired2).toBe(true);
|
|
35
|
+
release2();
|
|
36
|
+
});
|
|
37
|
+
it('cleans up stale locks from dead processes', async () => {
|
|
38
|
+
// Write a stale lock with a PID guaranteed not to exist (PID 1 is init/system, use a very high PID)
|
|
39
|
+
const lockPath = path.join(tmpDir, 'sync.lock');
|
|
40
|
+
const deadPid = 2_147_483_647; // max 32-bit PID — virtually guaranteed to be unused
|
|
41
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: deadPid, timestamp: Date.now() - 130_000 }));
|
|
42
|
+
const warnings = [];
|
|
43
|
+
const release = await acquireLock(tmpDir, (msg) => warnings.push(msg));
|
|
44
|
+
expect(warnings.some((w) => w.includes('stale') || w.includes('dead'))).toBe(true);
|
|
45
|
+
release();
|
|
46
|
+
});
|
|
47
|
+
it('withLock releases on success', async () => {
|
|
48
|
+
const result = await withLock(tmpDir, async () => 42);
|
|
49
|
+
expect(result).toBe(42);
|
|
50
|
+
expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('withLock releases on error', async () => {
|
|
53
|
+
await expect(withLock(tmpDir, async () => {
|
|
54
|
+
throw new Error('boom');
|
|
55
|
+
})).rejects.toThrow('boom');
|
|
56
|
+
expect(fs.existsSync(path.join(tmpDir, 'sync.lock'))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('withLock serializes concurrent operations', async () => {
|
|
59
|
+
const order = [];
|
|
60
|
+
const op = (id, ms) => withLock(tmpDir, async () => {
|
|
61
|
+
order.push(id);
|
|
62
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
63
|
+
order.push(id * 10);
|
|
64
|
+
});
|
|
65
|
+
// Launch two operations concurrently — second must wait for first
|
|
66
|
+
await Promise.all([op(1, 100), op(2, 50)]);
|
|
67
|
+
// First operation should fully complete before second starts
|
|
68
|
+
expect(order[0]).toBe(1);
|
|
69
|
+
expect(order[1]).toBe(10);
|
|
70
|
+
expect(order[2]).toBe(2);
|
|
71
|
+
expect(order[3]).toBe(20);
|
|
72
|
+
});
|
|
73
|
+
it('recovers from corrupted lockfile', async () => {
|
|
74
|
+
const lockFile = path.join(tmpDir, 'sync.lock');
|
|
75
|
+
fs.writeFileSync(lockFile, 'not valid json');
|
|
76
|
+
const release = await acquireLock(tmpDir);
|
|
77
|
+
expect(fs.existsSync(lockFile)).toBe(true);
|
|
78
|
+
release();
|
|
79
|
+
});
|
|
80
|
+
it('recovers from empty lockfile', async () => {
|
|
81
|
+
const lockFile = path.join(tmpDir, 'sync.lock');
|
|
82
|
+
fs.writeFileSync(lockFile, '');
|
|
83
|
+
const release = await acquireLock(tmpDir);
|
|
84
|
+
expect(fs.existsSync(lockFile)).toBe(true);
|
|
85
|
+
release();
|
|
86
|
+
});
|
|
87
|
+
it('creates totemDir if it does not exist', async () => {
|
|
88
|
+
const nested = path.join(tmpDir, 'deep', 'nested', '.totem');
|
|
89
|
+
const release = await acquireLock(nested);
|
|
90
|
+
expect(fs.existsSync(path.join(nested, 'sync.lock'))).toBe(true);
|
|
91
|
+
release();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=lock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.test.js","sourceRoot":"","sources":["../src/lock.test.ts"],"names":[],"mappings":"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"}
|