@volter/supercode-teams 0.3.185 → 0.3.187

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.
@@ -0,0 +1,78 @@
1
+ import { connect } from 'node:net';
2
+ import { copyFile, lstat, mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ const json = async path => { try { return JSON.parse(await readFile(path, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
6
+ const present = async path => { try { return await lstat(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } };
7
+ const answers = socket => new Promise(done => {
8
+ if (typeof socket !== 'string') return done(false);
9
+ const probe = connect(socket), timer = setTimeout(() => { probe.destroy(); done(false); }, 200);
10
+ probe.once('connect', () => { clearTimeout(timer); probe.destroy(); done(true); });
11
+ probe.once('error', () => { clearTimeout(timer); done(false); });
12
+ });
13
+
14
+ export async function checkpoint(home, directory) {
15
+ const path = join(home, 'mail.sqlite');
16
+ let database = false;
17
+ if (await present(path)) {
18
+ const { DatabaseSync } = await import('node:sqlite');
19
+ const reader = new DatabaseSync(path, { readOnly: true });
20
+ try { reader.prepare('VACUUM INTO ?').run(join(directory, 'mail.sqlite')); database = true; }
21
+ finally { reader.close(); }
22
+ }
23
+ const legacy = await present(join(home, 'mail'));
24
+ if (legacy && !legacy.isDirectory()) throw new Error('startup checkpoint requires a regular original Maildir');
25
+ const before = { database, legacy: legacy ? { device: legacy.dev, inode: legacy.ino } : null };
26
+ await writeFile(join(directory, 'before.json'), JSON.stringify(before) + '\n', { mode: 0o600 });
27
+ return before;
28
+ }
29
+
30
+ async function mergeLegacy(source, target) {
31
+ // Older writers can recreate the path during startup. Merge only records
32
+ // that do not conflict; keep both complete trees if a choice is required.
33
+ for (const item of await readdir(source, { withFileTypes: true })) {
34
+ const from = join(source, item.name), to = join(target, item.name), saved = await present(to);
35
+ if (item.isDirectory()) {
36
+ if (saved && !saved.isDirectory()) throw new Error('startup recovery found conflicting legacy record types');
37
+ if (!saved) await mkdir(to, { mode: 0o700 });
38
+ await mergeLegacy(from, to);
39
+ } else if (item.isFile()) {
40
+ if (!saved) await copyFile(from, to);
41
+ else if (!saved.isFile() || !(await readFile(from)).equals(await readFile(to))) throw new Error('startup recovery found conflicting legacy record bytes; both originals are retained');
42
+ } else throw new Error('startup recovery found a non-regular legacy record');
43
+ }
44
+ }
45
+
46
+ export async function restore(home, directory, before, teamsHome) {
47
+ const serving = await json(join(teamsHome, 'machine.json'));
48
+ if (serving && await answers(serving.socket)) throw new Error('startup recovery refused: another daemon is serving this home');
49
+ const path = join(home, 'mail.sqlite');
50
+ if (before.legacy) {
51
+ const root = join(home, 'mail'), current = await present(root);
52
+ const original = item => item?.isDirectory() && item.dev === before.legacy.device && item.ino === before.legacy.inode;
53
+ if (!original(current)) {
54
+ const archives = join(home, 'mail-retired');
55
+ let matches = [];
56
+ if (await present(archives)) for (const name of await readdir(archives)) {
57
+ const path = join(archives, name);
58
+ if (original(await present(path))) matches.push(path);
59
+ }
60
+ // Locate the pre-start inode even if a failed candidate's database cannot
61
+ // be opened or its metadata transaction never committed.
62
+ if (matches.length !== 1) throw new Error('startup recovery cannot locate its retained original Maildir');
63
+ if (current) {
64
+ if (!current.isDirectory()) throw new Error('startup recovery found a replaced Maildir path');
65
+ await mergeLegacy(root, matches[0]);
66
+ await rename(root, join(directory, 'recreated-mail'));
67
+ }
68
+ await rename(matches[0], root);
69
+ }
70
+ }
71
+ // Startup failure never served a caller. Preserve the failed candidate's
72
+ // database bytes for diagnosis before restoring the previous schema snapshot.
73
+ for (const suffix of ['', '-wal', '-shm']) {
74
+ if (await present(path + suffix)) await rename(path + suffix, join(directory, 'failed-mail.sqlite' + suffix));
75
+ }
76
+ if (before.database) await copyFile(join(directory, 'mail.sqlite'), path);
77
+ }
78
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.185",
3
+ "version": "0.3.187",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",