@volter/supercode-orchestrator 0.5.161 → 0.5.162
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/board/record-view.mjs +53 -20
- package/package.json +2 -2
package/board/record-view.mjs
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
// Resident local consumer of the daemon's committed record rows. Disconnects
|
|
2
2
|
// preserve the last applied cursor, but held data is stale until caught up.
|
|
3
3
|
export class RecordView {
|
|
4
|
-
constructor(open) {
|
|
4
|
+
constructor(open, { progressMs = 10_000 } = {}) {
|
|
5
5
|
this.open = open; this.records = {}; this.positions = new Map();
|
|
6
6
|
this.cursor = null; this.stream = null; this.waiters = new Set();
|
|
7
|
+
this.pending = [];
|
|
7
8
|
this.ready = false; this.heardAt = 0;
|
|
9
|
+
this.progressMs = progressMs; this.progressTimer = null;
|
|
8
10
|
}
|
|
9
11
|
async read(timeoutMs) {
|
|
10
12
|
const deadline = Date.now() + timeoutMs;
|
|
11
13
|
if (this.stream && Date.now() - this.heardAt > 45_000) this.stream.close('its record stream went quiet');
|
|
12
|
-
if (!this.stream
|
|
13
|
-
this.opening = this.open({ after: this.cursor }).then((stream) => {
|
|
14
|
-
this.stream = stream;
|
|
15
|
-
this.ready = false;
|
|
16
|
-
this.heardAt = Date.now();
|
|
17
|
-
stream.on('data', (frame) => {
|
|
18
|
-
try { this.apply(frame); }
|
|
19
|
-
catch (error) { this.fail(error); stream.close(error.message); }
|
|
20
|
-
});
|
|
21
|
-
stream.on('close', (reason) => {
|
|
22
|
-
if (this.stream !== stream) return;
|
|
23
|
-
this.stream = null; this.ready = false;
|
|
24
|
-
this.fail(new Error(reason ?? 'the local record stream closed'));
|
|
25
|
-
});
|
|
26
|
-
}).finally(() => { this.opening = null; });
|
|
27
|
-
}
|
|
28
|
-
if (this.opening) await this.opening;
|
|
14
|
+
if (!this.stream) await this.connect();
|
|
29
15
|
if (this.ready) return this.records;
|
|
30
16
|
if (!this.stream) throw this.failure ?? new Error('the local record stream ended before its snapshot');
|
|
31
17
|
return new Promise((resolve, reject) => {
|
|
@@ -37,6 +23,44 @@ export class RecordView {
|
|
|
37
23
|
this.waiters.add(waiter);
|
|
38
24
|
});
|
|
39
25
|
}
|
|
26
|
+
connect() {
|
|
27
|
+
if (this.stream) return Promise.resolve();
|
|
28
|
+
return this.opening ??= Promise.resolve().then(() => this.open({ after: this.cursor })).then((stream) => {
|
|
29
|
+
this.stream = stream; this.ready = false; this.heardAt = Date.now();
|
|
30
|
+
stream.on('data', (frame) => {
|
|
31
|
+
if (this.stream !== stream) return;
|
|
32
|
+
try { this.apply(frame); }
|
|
33
|
+
catch (error) { this.fail(error); stream.close(error.message); }
|
|
34
|
+
});
|
|
35
|
+
stream.on('receive-progress', ({ bytes, type }) => {
|
|
36
|
+
if (this.stream !== stream || bytes <= 0 || (type !== 'snapshot' && type !== 'changes')) return;
|
|
37
|
+
this.heardAt = Date.now(); this.watchProgress();
|
|
38
|
+
});
|
|
39
|
+
stream.on('close', (reason) => {
|
|
40
|
+
if (this.stream !== stream) return;
|
|
41
|
+
clearTimeout(this.progressTimer); this.progressTimer = null;
|
|
42
|
+
this.stream = null; this.ready = false; this.pending = [];
|
|
43
|
+
this.fail(new Error(reason ?? 'the local record stream closed'));
|
|
44
|
+
});
|
|
45
|
+
this.watchProgress();
|
|
46
|
+
}).finally(() => { this.opening = null; });
|
|
47
|
+
}
|
|
48
|
+
// Beats prove the door is alive, not that its snapshot or unfinished page is advancing. Record frames and bytes
|
|
49
|
+
// arriving inside them renew this deadline. Expiry reopens at the applied cursor, never its held prefix.
|
|
50
|
+
watchProgress() {
|
|
51
|
+
clearTimeout(this.progressTimer); this.progressTimer = null;
|
|
52
|
+
if (this.ready || !this.stream) return;
|
|
53
|
+
const stream = this.stream;
|
|
54
|
+
this.progressTimer = setTimeout(() => {
|
|
55
|
+
if (this.stream !== stream) return;
|
|
56
|
+
this.progressTimer = null; this.stream = null; this.ready = false; this.pending = [];
|
|
57
|
+
const error = new Error('the local record stream stopped making snapshot or cursor catch-up progress');
|
|
58
|
+
this.fail(error);
|
|
59
|
+
try { stream.close(error.message); } catch { /* the failed stream is already detached */ }
|
|
60
|
+
this.connect().catch((error) => this.fail(error));
|
|
61
|
+
}, this.progressMs);
|
|
62
|
+
this.progressTimer.unref?.();
|
|
63
|
+
}
|
|
40
64
|
fail(error) {
|
|
41
65
|
this.failure = error;
|
|
42
66
|
for (const waiter of this.waiters) { clearTimeout(waiter.timer); waiter.reject(error); }
|
|
@@ -46,13 +70,21 @@ export class RecordView {
|
|
|
46
70
|
apply(frame) {
|
|
47
71
|
if (frame.type === 'beat') { this.heardAt = Date.now(); return; }
|
|
48
72
|
if (frame.type === 'snapshot') {
|
|
49
|
-
this.self = frame.machine; this.records = frame.records; this.positions.clear();
|
|
73
|
+
this.self = frame.machine; this.records = frame.records; this.positions.clear(); this.pending = [];
|
|
50
74
|
for (const [machine, held] of Object.entries(this.records)) this.positions.set(machine, new Map((held.record?.sessions ?? []).map((row, at) => [row.address, at])));
|
|
51
75
|
this.cursor = frame.cursor; this.ready = true;
|
|
52
76
|
} else if (frame.type === 'changes') {
|
|
53
77
|
if (!this.cursor || frame.cursor?.id !== this.cursor.id) throw new Error('record row changes name a different database; a new snapshot is required');
|
|
78
|
+
// Row-sized transport frames belong to one logical page. Neither the applied state nor its resumable cursor
|
|
79
|
+
// advances until that page's final frame arrives; a disconnect discards the unfinished page.
|
|
80
|
+
if (frame.partial) {
|
|
81
|
+
this.ready = false; this.pending.push(...(frame.changes ?? [])); this.heardAt = Date.now();
|
|
82
|
+
if (frame.changes?.length || !this.progressTimer) this.watchProgress();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const changes = this.pending.concat(frame.changes ?? []); this.pending = [];
|
|
54
86
|
const moved = new Set();
|
|
55
|
-
for (const change of
|
|
87
|
+
for (const change of changes) {
|
|
56
88
|
if (change.sequence <= this.cursor.sequence || !this.own(change.key)) continue;
|
|
57
89
|
const { machine, section, key, position } = change.key;
|
|
58
90
|
if (change.topic === 'record-head') {
|
|
@@ -85,6 +117,7 @@ export class RecordView {
|
|
|
85
117
|
this.cursor = frame.cursor; this.ready = !frame.more;
|
|
86
118
|
} else throw new Error(`unrecognized record-view frame ${frame.type}`);
|
|
87
119
|
this.heardAt = Date.now();
|
|
120
|
+
this.watchProgress();
|
|
88
121
|
if (this.ready) {
|
|
89
122
|
for (const waiter of this.waiters) { clearTimeout(waiter.timer); waiter.resolve(this.records); }
|
|
90
123
|
this.waiters.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.162",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@volter/supercode-harness-sdk": "^0.3.98",
|
|
28
|
-
"@volter/teams": "0.2.
|
|
28
|
+
"@volter/teams": "0.2.66",
|
|
29
29
|
"@volter/ztrack": "^2.4.23",
|
|
30
30
|
"yaml": "^2.9.0"
|
|
31
31
|
},
|