optio-api 0.2.9 → 0.2.11

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.
Files changed (54) hide show
  1. package/README.md +39 -7
  2. package/dist/_generated/optio-engine.d.ts +8 -0
  3. package/dist/_generated/optio-engine.d.ts.map +1 -1
  4. package/dist/_generated/optio-engine.js +3 -0
  5. package/dist/_generated/optio-engine.js.map +1 -1
  6. package/dist/access-scope.d.ts +53 -0
  7. package/dist/access-scope.d.ts.map +1 -0
  8. package/dist/access-scope.js +110 -0
  9. package/dist/access-scope.js.map +1 -0
  10. package/dist/adapters/express.d.ts +5 -0
  11. package/dist/adapters/express.d.ts.map +1 -1
  12. package/dist/adapters/express.js +35 -23
  13. package/dist/adapters/express.js.map +1 -1
  14. package/dist/adapters/fastify.d.ts +5 -1
  15. package/dist/adapters/fastify.d.ts.map +1 -1
  16. package/dist/adapters/fastify.js +74 -33
  17. package/dist/adapters/fastify.js.map +1 -1
  18. package/dist/adapters/nextjs-app.d.ts +5 -0
  19. package/dist/adapters/nextjs-app.d.ts.map +1 -1
  20. package/dist/adapters/nextjs-app.js +44 -15
  21. package/dist/adapters/nextjs-app.js.map +1 -1
  22. package/dist/adapters/nextjs-pages.d.ts +5 -0
  23. package/dist/adapters/nextjs-pages.d.ts.map +1 -1
  24. package/dist/adapters/nextjs-pages.js +41 -15
  25. package/dist/adapters/nextjs-pages.js.map +1 -1
  26. package/dist/auth.d.ts +48 -0
  27. package/dist/auth.d.ts.map +1 -1
  28. package/dist/auth.js +44 -0
  29. package/dist/auth.js.map +1 -1
  30. package/dist/discovery.d.ts +7 -1
  31. package/dist/discovery.d.ts.map +1 -1
  32. package/dist/discovery.js +18 -11
  33. package/dist/discovery.js.map +1 -1
  34. package/dist/handlers.d.ts +44 -14
  35. package/dist/handlers.d.ts.map +1 -1
  36. package/dist/handlers.js +78 -20
  37. package/dist/handlers.js.map +1 -1
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/process-change-hub.d.ts +87 -0
  43. package/dist/process-change-hub.d.ts.map +1 -0
  44. package/dist/process-change-hub.js +268 -0
  45. package/dist/process-change-hub.js.map +1 -0
  46. package/dist/process-change-relevance.d.ts +57 -0
  47. package/dist/process-change-relevance.d.ts.map +1 -0
  48. package/dist/process-change-relevance.js +101 -0
  49. package/dist/process-change-relevance.js.map +1 -0
  50. package/dist/stream-poller.d.ts +13 -1
  51. package/dist/stream-poller.d.ts.map +1 -1
  52. package/dist/stream-poller.js +290 -260
  53. package/dist/stream-poller.js.map +1 -1
  54. package/package.json +2 -2
@@ -1,269 +1,312 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { metadataFilterToMongo } from './metadata-filter-query.js';
3
- export function createListPoller(opts) {
4
- const { db, prefix, sendEvent, onError, metadataFilter } = opts;
5
- const col = db.collection(`${prefix}_processes`);
6
- const filter = metadataFilterToMongo(metadataFilter);
7
- let interval = null;
8
- let lastSnapshot = '';
9
- async function poll() {
3
+ import { andScope } from './access-scope.js';
4
+ import { LIST_PROJECTION, listRelevant, treeRelevant, sessionRelevant, } from './process-change-relevance.js';
5
+ import { getProcessChangeHub } from './process-change-hub.js';
6
+ /**
7
+ * Run `read` once on start, then whenever the hub reports a change `rule`
8
+ * finds relevant (the hub throttles to one read a second, and polls once a
9
+ * second where change streams are unavailable). Reads never overlap: a refresh
10
+ * during a read runs one more read after it. While a read is in flight every
11
+ * change counts as relevant, since `rule` judges by the last read's members
12
+ * and the read in flight may have missed the change. A failed read stops the
13
+ * stream and calls `onError`.
14
+ *
15
+ * Spec: docs/2026-10-09-api-change-streams-design.md
16
+ */
17
+ function driveByChanges(opts, read, rule) {
18
+ const hub = opts.hub ?? getProcessChangeHub(opts.db, opts.prefix);
19
+ let stopped = false;
20
+ let reading = false;
21
+ let again = false;
22
+ async function run() {
23
+ if (stopped)
24
+ return;
25
+ if (reading) {
26
+ again = true;
27
+ return;
28
+ }
29
+ reading = true;
10
30
  try {
11
- const allProcs = await col.find(filter).sort({ depth: 1, order: 1, _id: 1 }).toArray();
12
- const snapshot = JSON.stringify(allProcs.map((p) => ({
13
- id: p._id,
14
- state: p.status?.state,
15
- percent: p.progress?.percent,
16
- message: p.progress?.message,
17
- supportsResume: p.supportsResume ?? false,
18
- hasSavedState: p.hasSavedState ?? false,
19
- autoResumeScheduled: p.autoResumeScheduled ?? false,
20
- browserOpenRequests: p.browserOpenRequests ?? [],
21
- })));
22
- if (snapshot !== lastSnapshot) {
23
- lastSnapshot = snapshot;
24
- sendEvent({
25
- type: 'update',
26
- processes: allProcs.map((p) => ({
27
- _id: p._id.toString(),
28
- processId: p.processId,
29
- name: p.name,
30
- status: p.status,
31
- progress: p.progress,
32
- cancellable: p.cancellable,
33
- special: p.special,
34
- warning: p.warning,
35
- metadata: p.metadata,
36
- depth: p.depth ?? 0,
37
- supportsResume: p.supportsResume ?? false,
38
- hasSavedState: p.hasSavedState ?? false,
39
- autoResumeScheduled: p.autoResumeScheduled ?? false,
40
- browserOpenRequests: p.browserOpenRequests ?? [],
41
- })),
42
- });
43
- }
31
+ do {
32
+ again = false;
33
+ await read();
34
+ } while (again && !stopped);
44
35
  }
45
36
  catch {
46
37
  stop();
47
- onError();
38
+ opts.onError();
39
+ }
40
+ finally {
41
+ reading = false;
48
42
  }
49
43
  }
44
+ const sub = {
45
+ isRelevant: (c) => reading || rule(c),
46
+ refresh: () => { void run(); },
47
+ };
50
48
  function start() {
51
- interval = setInterval(poll, 1000);
49
+ stopped = false;
50
+ hub.subscribe(sub).then(() => run(), () => {
51
+ if (stopped)
52
+ return;
53
+ stop();
54
+ opts.onError();
55
+ });
52
56
  }
53
57
  function stop() {
54
- if (interval) {
55
- clearInterval(interval);
56
- interval = null;
57
- }
58
+ if (stopped)
59
+ return;
60
+ stopped = true;
61
+ hub.unsubscribe(sub);
58
62
  }
59
63
  return { start, stop };
60
64
  }
65
+ export function createListPoller(opts) {
66
+ const { db, prefix, sendEvent, metadataFilter, scope } = opts;
67
+ const col = db.collection(`${prefix}_processes`);
68
+ const filter = andScope(metadataFilterToMongo(metadataFilter), scope ?? null);
69
+ let members = new Set();
70
+ let lastSnapshot = '';
71
+ async function read() {
72
+ const allProcs = await col
73
+ .find(filter, { projection: LIST_PROJECTION })
74
+ .sort({ depth: 1, order: 1, _id: 1 })
75
+ .toArray();
76
+ members = new Set(allProcs.map((p) => String(p._id)));
77
+ const snapshot = JSON.stringify(allProcs.map((p) => ({
78
+ id: p._id,
79
+ state: p.status?.state,
80
+ percent: p.progress?.percent,
81
+ message: p.progress?.message,
82
+ supportsResume: p.supportsResume ?? false,
83
+ hasSavedState: p.hasSavedState ?? false,
84
+ supportsResurrect: p.supportsResurrect ?? false,
85
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
86
+ resurrecting: p.resurrecting ?? false,
87
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
88
+ browserOpenRequests: p.browserOpenRequests ?? [],
89
+ })));
90
+ if (snapshot !== lastSnapshot) {
91
+ lastSnapshot = snapshot;
92
+ sendEvent({
93
+ type: 'update',
94
+ processes: allProcs.map((p) => ({
95
+ _id: p._id.toString(),
96
+ processId: p.processId,
97
+ name: p.name,
98
+ status: p.status,
99
+ progress: p.progress,
100
+ cancellable: p.cancellable,
101
+ special: p.special,
102
+ warning: p.warning,
103
+ metadata: p.metadata,
104
+ depth: p.depth ?? 0,
105
+ supportsResume: p.supportsResume ?? false,
106
+ hasSavedState: p.hasSavedState ?? false,
107
+ supportsResurrect: p.supportsResurrect ?? false,
108
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
109
+ resurrecting: p.resurrecting ?? false,
110
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
111
+ browserOpenRequests: p.browserOpenRequests ?? [],
112
+ })),
113
+ });
114
+ }
115
+ }
116
+ return driveByChanges(opts, read, (c) => listRelevant(c, members));
117
+ }
61
118
  export function createTreePoller(opts) {
62
- const { db, prefix, sendEvent, onError, rootId, baseDepth, maxDepth } = opts;
119
+ const { db, prefix, sendEvent, rootId, baseDepth, maxDepth } = opts;
63
120
  const col = db.collection(`${prefix}_processes`);
64
- let interval = null;
121
+ const roots = new Set([rootId]);
122
+ let members = new Set();
65
123
  let lastSnapshot = '';
66
124
  const lastLogCounts = new Map();
67
125
  let firstPoll = true;
68
- async function poll() {
69
- try {
70
- const filter = { rootId: new ObjectId(rootId) };
71
- if (maxDepth !== undefined) {
72
- filter.depth = { $lte: baseDepth + maxDepth };
73
- }
74
- const allProcs = await col.find(filter).sort({ depth: 1, order: 1 }).toArray();
75
- const snapshot = JSON.stringify(allProcs.map((p) => ({
76
- id: p._id, status: p.status, progress: p.progress,
77
- widgetData: p.widgetData, uiWidget: p.uiWidget,
78
- supportsResume: p.supportsResume ?? false,
79
- hasSavedState: p.hasSavedState ?? false,
80
- autoResumeScheduled: p.autoResumeScheduled ?? false,
81
- browserOpenRequests: p.browserOpenRequests ?? [],
82
- metadata: p.metadata,
83
- })));
84
- if (snapshot !== lastSnapshot) {
85
- lastSnapshot = snapshot;
86
- sendEvent({
87
- type: 'update',
88
- processes: allProcs.map((p) => ({
89
- _id: p._id.toString(),
90
- parentId: p.parentId?.toString() ?? null,
91
- rootId: p.rootId?.toString() ?? null,
92
- name: p.name,
93
- status: p.status,
94
- progress: p.progress,
95
- cancellable: p.cancellable ?? false,
96
- depth: p.depth,
97
- order: p.order,
98
- widgetData: p.widgetData,
99
- uiWidget: p.uiWidget,
100
- supportsResume: p.supportsResume ?? false,
101
- hasSavedState: p.hasSavedState ?? false,
102
- autoResumeScheduled: p.autoResumeScheduled ?? false,
103
- browserOpenRequests: p.browserOpenRequests ?? [],
104
- metadata: p.metadata,
105
- })),
106
- });
126
+ async function read() {
127
+ const filter = { rootId: new ObjectId(rootId) };
128
+ if (maxDepth !== undefined) {
129
+ filter.depth = { $lte: baseDepth + maxDepth };
130
+ }
131
+ const allProcs = await col.find(filter).sort({ depth: 1, order: 1 }).toArray();
132
+ members = new Set(allProcs.map((p) => String(p._id)));
133
+ const snapshot = JSON.stringify(allProcs.map((p) => ({
134
+ id: p._id, status: p.status, progress: p.progress,
135
+ widgetData: p.widgetData, uiWidget: p.uiWidget,
136
+ supportsResume: p.supportsResume ?? false,
137
+ hasSavedState: p.hasSavedState ?? false,
138
+ supportsResurrect: p.supportsResurrect ?? false,
139
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
140
+ resurrecting: p.resurrecting ?? false,
141
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
142
+ browserOpenRequests: p.browserOpenRequests ?? [],
143
+ metadata: p.metadata,
144
+ })));
145
+ if (snapshot !== lastSnapshot) {
146
+ lastSnapshot = snapshot;
147
+ sendEvent({
148
+ type: 'update',
149
+ processes: allProcs.map((p) => ({
150
+ _id: p._id.toString(),
151
+ parentId: p.parentId?.toString() ?? null,
152
+ rootId: p.rootId?.toString() ?? null,
153
+ name: p.name,
154
+ status: p.status,
155
+ progress: p.progress,
156
+ cancellable: p.cancellable ?? false,
157
+ depth: p.depth,
158
+ order: p.order,
159
+ widgetData: p.widgetData,
160
+ uiWidget: p.uiWidget,
161
+ supportsResume: p.supportsResume ?? false,
162
+ hasSavedState: p.hasSavedState ?? false,
163
+ supportsResurrect: p.supportsResurrect ?? false,
164
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
165
+ resurrecting: p.resurrecting ?? false,
166
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
167
+ browserOpenRequests: p.browserOpenRequests ?? [],
168
+ metadata: p.metadata,
169
+ })),
170
+ });
171
+ }
172
+ // Detect log changes
173
+ let logCleared = false;
174
+ const newLogEntries = [];
175
+ for (const p of allProcs) {
176
+ const pid = p._id.toString();
177
+ const logLen = (p.log ?? []).length;
178
+ const lastLen = lastLogCounts.get(pid) ?? 0;
179
+ if (logLen < lastLen) {
180
+ logCleared = true;
181
+ lastLogCounts.set(pid, 0);
107
182
  }
108
- // Detect log changes
109
- let logCleared = false;
110
- const newLogEntries = [];
111
- for (const p of allProcs) {
112
- const pid = p._id.toString();
113
- const logLen = (p.log ?? []).length;
114
- const lastLen = lastLogCounts.get(pid) ?? 0;
115
- if (logLen < lastLen) {
116
- logCleared = true;
117
- lastLogCounts.set(pid, 0);
118
- }
119
- const effectiveLastLen = lastLogCounts.get(pid) ?? 0;
120
- if (logLen > effectiveLastLen) {
121
- const entries = (p.log ?? []).slice(firstPoll ? 0 : effectiveLastLen);
122
- for (const entry of entries) {
123
- newLogEntries.push({
124
- ...entry,
125
- processId: pid,
126
- processLabel: p.name,
127
- });
128
- }
129
- lastLogCounts.set(pid, logLen);
183
+ const effectiveLastLen = lastLogCounts.get(pid) ?? 0;
184
+ if (logLen > effectiveLastLen) {
185
+ const entries = (p.log ?? []).slice(firstPoll ? 0 : effectiveLastLen);
186
+ for (const entry of entries) {
187
+ newLogEntries.push({
188
+ ...entry,
189
+ processId: pid,
190
+ processLabel: p.name,
191
+ });
130
192
  }
131
- }
132
- firstPoll = false;
133
- if (logCleared) {
134
- sendEvent({ type: 'log-clear' });
135
- }
136
- if (newLogEntries.length > 0) {
137
- newLogEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
138
- sendEvent({ type: 'log', entries: newLogEntries });
193
+ lastLogCounts.set(pid, logLen);
139
194
  }
140
195
  }
141
- catch {
142
- stop();
143
- onError();
196
+ firstPoll = false;
197
+ if (logCleared) {
198
+ sendEvent({ type: 'log-clear' });
144
199
  }
145
- }
146
- function start() {
147
- interval = setInterval(poll, 1000);
148
- }
149
- function stop() {
150
- if (interval) {
151
- clearInterval(interval);
152
- interval = null;
200
+ if (newLogEntries.length > 0) {
201
+ newLogEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
202
+ sendEvent({ type: 'log', entries: newLogEntries });
153
203
  }
154
204
  }
155
- return { start, stop };
205
+ return driveByChanges(opts, read, (c) => treeRelevant(c, roots, members));
156
206
  }
157
207
  export function createMultiTreePoller(opts) {
158
- const { db, prefix, sendEvent, onError, treeRoots, flatIds, maxDepth } = opts;
208
+ const { db, prefix, sendEvent, treeRoots, flatIds, maxDepth } = opts;
159
209
  const col = db.collection(`${prefix}_processes`);
160
- let interval = null;
210
+ const roots = new Set(treeRoots.map((r) => r.rootId.toString()));
211
+ let members = new Set();
161
212
  let lastSnapshot = '';
162
213
  const lastLogCounts = new Map();
163
214
  let firstPoll = true;
164
- async function poll() {
165
- try {
166
- const branches = [];
167
- if (treeRoots.length > 0) {
168
- branches.push({
169
- $or: treeRoots.map((r) => {
170
- const f = { rootId: r.rootId };
171
- if (maxDepth !== undefined) {
172
- f.depth = { $lte: r.baseDepth + maxDepth };
173
- }
174
- return f;
175
- }),
176
- });
177
- }
178
- if (flatIds.length > 0) {
179
- branches.push({ _id: { $in: flatIds } });
215
+ async function read() {
216
+ const branches = [];
217
+ if (treeRoots.length > 0) {
218
+ branches.push({
219
+ $or: treeRoots.map((r) => {
220
+ const f = { rootId: r.rootId };
221
+ if (maxDepth !== undefined) {
222
+ f.depth = { $lte: r.baseDepth + maxDepth };
223
+ }
224
+ return f;
225
+ }),
226
+ });
227
+ }
228
+ if (flatIds.length > 0) {
229
+ branches.push({ _id: { $in: flatIds } });
230
+ }
231
+ if (branches.length === 0)
232
+ return;
233
+ const filter = branches.length === 1 ? branches[0] : { $or: branches };
234
+ const allProcs = await col.find(filter).sort({ depth: 1, order: 1 }).toArray();
235
+ members = new Set([...allProcs.map((p) => String(p._id)), ...flatIds.map(String)]);
236
+ const snapshot = JSON.stringify(allProcs.map((p) => ({
237
+ id: p._id, status: p.status, progress: p.progress,
238
+ widgetData: p.widgetData, uiWidget: p.uiWidget,
239
+ supportsResume: p.supportsResume ?? false,
240
+ hasSavedState: p.hasSavedState ?? false,
241
+ supportsResurrect: p.supportsResurrect ?? false,
242
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
243
+ resurrecting: p.resurrecting ?? false,
244
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
245
+ browserOpenRequests: p.browserOpenRequests ?? [],
246
+ metadata: p.metadata,
247
+ })));
248
+ if (snapshot !== lastSnapshot) {
249
+ lastSnapshot = snapshot;
250
+ sendEvent({
251
+ type: 'update',
252
+ processes: allProcs.map((p) => ({
253
+ _id: p._id.toString(),
254
+ parentId: p.parentId?.toString() ?? null,
255
+ rootId: p.rootId?.toString() ?? null,
256
+ processId: p.processId,
257
+ name: p.name,
258
+ status: p.status,
259
+ progress: p.progress,
260
+ cancellable: p.cancellable ?? false,
261
+ depth: p.depth,
262
+ order: p.order,
263
+ widgetData: p.widgetData,
264
+ uiWidget: p.uiWidget,
265
+ supportsResume: p.supportsResume ?? false,
266
+ hasSavedState: p.hasSavedState ?? false,
267
+ supportsResurrect: p.supportsResurrect ?? false,
268
+ hasUnsavedWork: p.hasUnsavedWork ?? false,
269
+ resurrecting: p.resurrecting ?? false,
270
+ autoResumeScheduled: p.autoResumeScheduled ?? false,
271
+ browserOpenRequests: p.browserOpenRequests ?? [],
272
+ metadata: p.metadata,
273
+ })),
274
+ });
275
+ }
276
+ const logClearedRoots = new Set();
277
+ const newLogEntries = [];
278
+ for (const p of allProcs) {
279
+ const pid = p._id.toString();
280
+ const logLen = (p.log ?? []).length;
281
+ const lastLen = lastLogCounts.get(pid) ?? 0;
282
+ if (logLen < lastLen) {
283
+ logClearedRoots.add(p.rootId?.toString() ?? '');
284
+ lastLogCounts.set(pid, 0);
180
285
  }
181
- if (branches.length === 0)
182
- return;
183
- const filter = branches.length === 1 ? branches[0] : { $or: branches };
184
- const allProcs = await col.find(filter).sort({ depth: 1, order: 1 }).toArray();
185
- const snapshot = JSON.stringify(allProcs.map((p) => ({
186
- id: p._id, status: p.status, progress: p.progress,
187
- widgetData: p.widgetData, uiWidget: p.uiWidget,
188
- supportsResume: p.supportsResume ?? false,
189
- hasSavedState: p.hasSavedState ?? false,
190
- autoResumeScheduled: p.autoResumeScheduled ?? false,
191
- browserOpenRequests: p.browserOpenRequests ?? [],
192
- metadata: p.metadata,
193
- })));
194
- if (snapshot !== lastSnapshot) {
195
- lastSnapshot = snapshot;
196
- sendEvent({
197
- type: 'update',
198
- processes: allProcs.map((p) => ({
199
- _id: p._id.toString(),
200
- parentId: p.parentId?.toString() ?? null,
286
+ const effectiveLastLen = lastLogCounts.get(pid) ?? 0;
287
+ if (logLen > effectiveLastLen) {
288
+ const entries = (p.log ?? []).slice(firstPoll ? 0 : effectiveLastLen);
289
+ for (const entry of entries) {
290
+ newLogEntries.push({
291
+ ...entry,
292
+ processId: pid,
293
+ processLabel: p.name,
201
294
  rootId: p.rootId?.toString() ?? null,
202
- processId: p.processId,
203
- name: p.name,
204
- status: p.status,
205
- progress: p.progress,
206
- cancellable: p.cancellable ?? false,
207
- depth: p.depth,
208
- order: p.order,
209
- widgetData: p.widgetData,
210
- uiWidget: p.uiWidget,
211
- supportsResume: p.supportsResume ?? false,
212
- hasSavedState: p.hasSavedState ?? false,
213
- autoResumeScheduled: p.autoResumeScheduled ?? false,
214
- browserOpenRequests: p.browserOpenRequests ?? [],
215
- metadata: p.metadata,
216
- })),
217
- });
218
- }
219
- const logClearedRoots = new Set();
220
- const newLogEntries = [];
221
- for (const p of allProcs) {
222
- const pid = p._id.toString();
223
- const logLen = (p.log ?? []).length;
224
- const lastLen = lastLogCounts.get(pid) ?? 0;
225
- if (logLen < lastLen) {
226
- logClearedRoots.add(p.rootId?.toString() ?? '');
227
- lastLogCounts.set(pid, 0);
228
- }
229
- const effectiveLastLen = lastLogCounts.get(pid) ?? 0;
230
- if (logLen > effectiveLastLen) {
231
- const entries = (p.log ?? []).slice(firstPoll ? 0 : effectiveLastLen);
232
- for (const entry of entries) {
233
- newLogEntries.push({
234
- ...entry,
235
- processId: pid,
236
- processLabel: p.name,
237
- rootId: p.rootId?.toString() ?? null,
238
- });
239
- }
240
- lastLogCounts.set(pid, logLen);
295
+ });
241
296
  }
242
- }
243
- firstPoll = false;
244
- for (const rid of logClearedRoots) {
245
- sendEvent({ type: 'log-clear', rootId: rid });
246
- }
247
- if (newLogEntries.length > 0) {
248
- newLogEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
249
- sendEvent({ type: 'log', entries: newLogEntries });
297
+ lastLogCounts.set(pid, logLen);
250
298
  }
251
299
  }
252
- catch {
253
- stop();
254
- onError();
300
+ firstPoll = false;
301
+ for (const rid of logClearedRoots) {
302
+ sendEvent({ type: 'log-clear', rootId: rid });
255
303
  }
256
- }
257
- function start() {
258
- interval = setInterval(poll, 1000);
259
- }
260
- function stop() {
261
- if (interval) {
262
- clearInterval(interval);
263
- interval = null;
304
+ if (newLogEntries.length > 0) {
305
+ newLogEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
306
+ sendEvent({ type: 'log', entries: newLogEntries });
264
307
  }
265
308
  }
266
- return { start, stop };
309
+ return driveByChanges(opts, read, (c) => treeRelevant(c, roots, members));
267
310
  }
268
311
  /**
269
312
  * Poll-backed session-events feed. Each ~1s tick reads processes whose
@@ -271,44 +314,31 @@ export function createMultiTreePoller(opts) {
271
314
  * sessionEvents (deduped by length high-water mark per process). Read-only.
272
315
  */
273
316
  export function createSessionEventsPoller(opts) {
274
- const { db, prefix, sessionId, sendEvent, onError } = opts;
317
+ const { db, prefix, sessionId, sendEvent, scope } = opts;
275
318
  const col = db.collection(`${prefix}_processes`);
276
- let interval = null;
319
+ const filter = andScope({ originatingSessionId: sessionId }, scope ?? null);
320
+ let members = new Set();
277
321
  const lastCounts = new Map();
278
- async function poll() {
279
- try {
280
- const procs = await col
281
- .find({ originatingSessionId: sessionId })
282
- .project({ sessionEvents: 1 })
283
- .toArray();
284
- for (const p of procs) {
285
- const pid = p._id.toString();
286
- const events = (p.sessionEvents ?? []);
287
- const seen = lastCounts.get(pid) ?? 0;
288
- if (events.length > seen) {
289
- sendEvent({
290
- type: 'session-events',
291
- processId: pid,
292
- events: events.slice(seen),
293
- });
294
- lastCounts.set(pid, events.length);
295
- }
322
+ async function read() {
323
+ const procs = await col
324
+ .find(filter)
325
+ .project({ sessionEvents: 1 })
326
+ .toArray();
327
+ members = new Set(procs.map((p) => String(p._id)));
328
+ for (const p of procs) {
329
+ const pid = p._id.toString();
330
+ const events = (p.sessionEvents ?? []);
331
+ const seen = lastCounts.get(pid) ?? 0;
332
+ if (events.length > seen) {
333
+ sendEvent({
334
+ type: 'session-events',
335
+ processId: pid,
336
+ events: events.slice(seen),
337
+ });
338
+ lastCounts.set(pid, events.length);
296
339
  }
297
340
  }
298
- catch {
299
- stop();
300
- onError();
301
- }
302
- }
303
- function start() {
304
- interval = setInterval(poll, 1000);
305
341
  }
306
- function stop() {
307
- if (interval) {
308
- clearInterval(interval);
309
- interval = null;
310
- }
311
- }
312
- return { start, stop };
342
+ return driveByChanges(opts, read, (c) => sessionRelevant(c, sessionId, members));
313
343
  }
314
344
  //# sourceMappingURL=stream-poller.js.map