@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.
@@ -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 && !this.opening) {
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 frame.changes ?? []) {
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.161",
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.65",
28
+ "@volter/teams": "0.2.66",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },