@camerontaylor/paseo-client 0.8.0-fork.1 → 0.9.0-beta.2.fork.1

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/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { OwnedSubscription } from "./connection/index.js";
2
+ export type { OwnedSubscription, SubscriptionObserver } from "./connection/index.js";
1
3
  import type { DaemonClientConfig } from "./daemon-client.js";
2
4
  import type { AgentPermissionResponse } from "@camerontaylor/paseo-protocol/agent-types";
3
5
  import type { AgentSnapshotPayload, CreationSnapshot, CreateAgentRequestMessage, FetchWorkspacesRequestMessage, FetchWorkspacesResponseMessage, GetProvidersSnapshotResponseMessage, ListAvailableProvidersResponse, ListCommandsResponse, ListProviderFeaturesRequestMessage, ListProviderFeaturesResponseMessage, ListProviderModelsResponseMessage, ProjectListRequestMessage, ProjectListResponseMessage, ListProviderModesResponseMessage, MutableDaemonConfig, MutableDaemonConfigPatch, ProviderDiagnosticResponseMessage, ProviderUsageListResponseMessage, ProjectPlacementPayload, WorkspaceProjectDescriptorPayload, RefreshProvidersSnapshotResponseMessage, SendAgentMessageRequest, SessionOutboundMessage, WorkspaceDescriptorPayload, WorkspaceCreateRequest } from "@camerontaylor/paseo-protocol/messages";
@@ -61,6 +63,7 @@ export type PaseoProjectUpdate = Extract<SessionOutboundMessage, {
61
63
  }>["payload"];
62
64
  export type PaseoProjectUpdateHandler = (update: PaseoProjectUpdate) => void;
63
65
  export interface PaseoAgentListResult {
66
+ subscription?: OwnedSubscription<PaseoAgentListResult>;
64
67
  requestId: string;
65
68
  subscriptionId?: string | null;
66
69
  entries: FetchAgentsEntry[];
@@ -70,6 +73,7 @@ export type PaseoWorkspaceListOptions = Omit<FetchWorkspacesRequestMessage, "typ
70
73
  requestId?: string;
71
74
  };
72
75
  export interface PaseoWorkspaceListResult {
76
+ subscription?: OwnedSubscription<PaseoWorkspaceListResult>;
73
77
  requestId: string;
74
78
  subscriptionId?: string | null;
75
79
  entries: PaseoWorkspace[];
@@ -125,6 +129,12 @@ export interface PaseoProjectActions {
125
129
  subscribe(handler: PaseoProjectUpdateHandler): () => void;
126
130
  }
127
131
  export interface PaseoWorkspaceActions {
132
+ list(options: PaseoWorkspaceListOptions & {
133
+ subscribe: {};
134
+ }): Promise<PaseoWorkspaceListResult & {
135
+ subscriptionId: string;
136
+ subscription: OwnedSubscription<PaseoWorkspaceListResult>;
137
+ }>;
128
138
  list(options?: PaseoWorkspaceListOptions): Promise<PaseoWorkspaceListResult>;
129
139
  ref(workspace: string | PaseoWorkspace): PaseoWorkspaceHandle;
130
140
  open(input: string | PaseoWorkspaceOpenOptions, requestId?: string): Promise<PaseoWorkspaceHandle>;
@@ -217,6 +227,18 @@ export type PaseoAgentTimelineEvent = PaseoAgentStream | {
217
227
  type: "replacement";
218
228
  epoch: string;
219
229
  };
230
+ } | {
231
+ agentId: string;
232
+ subscriptionId: string;
233
+ event: {
234
+ type: "subscription_restored";
235
+ };
236
+ } | {
237
+ agentId: string;
238
+ event: {
239
+ type: "error";
240
+ error: string;
241
+ };
220
242
  };
221
243
  export type PaseoAgentTimelineSubscription = ReturnType<DaemonClient["subscribeAgentTimeline"]>;
222
244
  export interface PaseoAgentTimelineHandle {
@@ -231,8 +253,10 @@ export interface PaseoAgentTimelineHandle {
231
253
  */
232
254
  refetch(options?: PaseoAgentTimelineRefetchOptions): Promise<FetchAgentTimelinePayload>;
233
255
  /**
234
- * Subscribe to this agent and restore demand after reconnect. A replacement
235
- * event invalidates previously fetched history; refetch the page you need.
256
+ * Delivers live events only. After reconnect, subscription_restored precedes
257
+ * subsequent updates. History may have been missed; use refetch() to request
258
+ * the range you need. No history is fetched automatically. A replacement event
259
+ * invalidates the previous epoch. Subscription errors release this observation.
236
260
  * Await the returned unsubscribe function's `ready` promise before starting
237
261
  * work that must be observed. It rejects if establishment fails.
238
262
  */
@@ -283,6 +307,12 @@ export interface PaseoAgentHandle {
283
307
  subscribe(handler: (update: PaseoAgentUpdate) => void): () => void;
284
308
  }
285
309
  export interface PaseoAgentActions {
310
+ list(options: PaseoAgentListOptions & {
311
+ subscribe: {};
312
+ }): Promise<PaseoAgentListResult & {
313
+ subscriptionId: string;
314
+ subscription: OwnedSubscription<PaseoAgentListResult>;
315
+ }>;
286
316
  list(options?: PaseoAgentListOptions): Promise<PaseoAgentListResult>;
287
317
  ref(agent: string | PaseoAgent): PaseoAgentHandle;
288
318
  create(options: PaseoAgentCreateOptions): Promise<PaseoAgentHandle>;
@@ -365,6 +395,8 @@ export interface PaseoConfigActions {
365
395
  }>;
366
396
  }
367
397
  export interface PaseoApi {
398
+ dispose(): Promise<void>;
399
+ observeEvents: DaemonClient["observeEvents"];
368
400
  readonly terminals: PaseoTerminalActions;
369
401
  readonly workspaces: PaseoWorkspaceActions;
370
402
  readonly projects: PaseoProjectActions;
@@ -379,5 +411,7 @@ export interface PaseoClient extends PaseoApi {
379
411
  getConnectionState(): ConnectionState;
380
412
  }
381
413
  export declare function createPaseoClient(config: PaseoClientConfig): PaseoClient;
382
- export declare function createPaseoApi(daemonClient: DaemonClient): PaseoApi;
414
+ export declare function createPaseoApi(daemonClient: DaemonClient, scopeOptions?: {
415
+ signal?: AbortSignal;
416
+ }): PaseoApi;
383
417
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -11,10 +11,18 @@ export function createPaseoClient(config) {
11
11
  clientId: config.clientId ?? createGeneratedClientId(),
12
12
  clientType: "cli",
13
13
  });
14
+ const api = createPaseoApi(daemonClient);
14
15
  return {
15
- ...createPaseoApi(daemonClient),
16
+ ...api,
16
17
  connect: () => daemonClient.connect(),
17
- close: () => daemonClient.close(),
18
+ close: async () => {
19
+ try {
20
+ await api.dispose();
21
+ }
22
+ finally {
23
+ await daemonClient.close();
24
+ }
25
+ },
18
26
  ensureConnected: () => daemonClient.ensureConnected(),
19
27
  getConnectionState: () => daemonClient.getConnectionState(),
20
28
  };
@@ -38,8 +46,40 @@ function toDaemonAgentCreateOptions(options, placement) {
38
46
  ...(prompt !== undefined ? { initialPrompt: prompt } : {}),
39
47
  };
40
48
  }
41
- export function createPaseoApi(daemonClient) {
42
- const createAgentHandle = createAgentHandleFactory(daemonClient);
49
+ export function createPaseoApi(daemonClient, scopeOptions) {
50
+ const handles = new Set();
51
+ const agentListeners = new Set();
52
+ const workspaceListeners = new Set();
53
+ const lifetime = new AbortController();
54
+ const own = (create) => {
55
+ if (lifetime.signal.aborted)
56
+ throw new Error("Paseo API is disposed");
57
+ const handle = create();
58
+ handles.add(handle);
59
+ const release = handle.release.bind(handle);
60
+ handle.release = async () => {
61
+ await release();
62
+ handles.delete(handle);
63
+ };
64
+ return handle;
65
+ };
66
+ const listenAgents = (handler) => {
67
+ if (lifetime.signal.aborted)
68
+ throw new Error("Paseo API is disposed");
69
+ agentListeners.add(handler);
70
+ return () => {
71
+ agentListeners.delete(handler);
72
+ };
73
+ };
74
+ const listenWorkspaces = (handler) => {
75
+ if (lifetime.signal.aborted)
76
+ throw new Error("Paseo API is disposed");
77
+ workspaceListeners.add(handler);
78
+ return () => {
79
+ workspaceListeners.delete(handler);
80
+ };
81
+ };
82
+ const createAgentHandle = createAgentHandleFactory(daemonClient, listenAgents, (agentId, handler) => own(() => daemonClient.subscribeAgentTimeline(agentId, handler)));
43
83
  const createAgent = async (options, placement) => {
44
84
  const agent = await daemonClient.createAgent(toDaemonAgentCreateOptions(options, placement));
45
85
  return createAgentHandle(agent);
@@ -51,17 +91,88 @@ export function createPaseoApi(daemonClient) {
51
91
  }
52
92
  return workspace.workspaceDirectory;
53
93
  });
54
- const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent, terminals);
94
+ const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent, terminals, listenWorkspaces);
95
+ let disposal = null;
96
+ const dispose = () => {
97
+ if (disposal)
98
+ return disposal;
99
+ lifetime.abort();
100
+ scopeOptions?.signal?.removeEventListener("abort", abort);
101
+ agentListeners.clear();
102
+ workspaceListeners.clear();
103
+ disposal = Promise.allSettled([...handles].map((handle) => handle.release())).then((results) => {
104
+ handles.clear();
105
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
106
+ if (failures.length)
107
+ throw new AggregateError(failures, "Failed to release API subscriptions");
108
+ return undefined;
109
+ });
110
+ return disposal;
111
+ };
112
+ const abort = () => {
113
+ void dispose().catch((error) => console.error("API subscription cleanup failed", error));
114
+ };
115
+ if (scopeOptions?.signal?.aborted)
116
+ abort();
117
+ else
118
+ scopeOptions?.signal?.addEventListener("abort", abort, { once: true });
119
+ const observeEvents = (events, options) => own(() => daemonClient.observeEvents(events, options));
120
+ const subscribeEvent = (event, update) => {
121
+ const observation = observeEvents([event]);
122
+ observation.subscribe({ snapshot: () => { }, update });
123
+ return () => {
124
+ void observation
125
+ .release()
126
+ .catch((error) => console.error("Event subscription cleanup failed", error));
127
+ };
128
+ };
129
+ async function listWorkspaces(options) {
130
+ if (!options?.subscribe)
131
+ return daemonClient.fetchWorkspaces(options);
132
+ if (options.subscribe.subscriptionId !== undefined)
133
+ throw new Error("Subscription IDs are assigned by the host");
134
+ const subscription = own(() => daemonClient.observeWorkspaces(options));
135
+ subscription.subscribe({
136
+ snapshot: () => { },
137
+ update: (message) => {
138
+ if (message.type === "workspace_update")
139
+ for (const listener of workspaceListeners)
140
+ listener(message.payload);
141
+ },
142
+ });
143
+ return { ...(await subscription.ready), subscription };
144
+ }
145
+ async function listAgents(options) {
146
+ if (!options?.subscribe)
147
+ return daemonClient.fetchAgents(options);
148
+ if (options.subscribe.subscriptionId !== undefined)
149
+ throw new Error("Subscription IDs are assigned by the host");
150
+ const subscription = own(() => daemonClient.observeAgents(options));
151
+ subscription.subscribe({
152
+ snapshot: () => { },
153
+ update: (message) => {
154
+ if (message.type === "agent_update")
155
+ for (const listener of agentListeners)
156
+ listener(message.payload);
157
+ },
158
+ });
159
+ return { ...(await subscription.ready), subscription };
160
+ }
55
161
  return {
162
+ dispose,
163
+ observeEvents,
56
164
  terminals,
57
165
  projects: {
58
166
  list: (options) => daemonClient.listProjects(options),
59
- subscribe: (handler) => daemonClient.on("project.update", (message) => {
60
- handler(message.payload);
61
- }),
167
+ subscribe: (handler) => {
168
+ return subscribeEvent("project.update", (message) => {
169
+ if (message.type === "project.update")
170
+ handler(message.payload);
171
+ });
172
+ },
62
173
  },
63
174
  workspaces: {
64
- list: (options) => daemonClient.fetchWorkspaces(options),
175
+ list: listWorkspaces,
65
176
  ref: (workspace) => createWorkspaceHandle(workspace),
66
177
  open: (input, requestId) => openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
67
178
  create: async ({ requestId, agent, ...options }) => {
@@ -72,17 +183,13 @@ export function createPaseoApi(daemonClient) {
72
183
  return createWorkspaceHandle(result.workspace);
73
184
  },
74
185
  archive: (workspace, requestId) => daemonClient.archiveWorkspace(resolveWorkspaceId(workspace), requestId),
75
- subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
76
- handler(message.payload);
77
- }),
186
+ subscribe: listenWorkspaces,
78
187
  },
79
188
  agents: {
80
- list: (options) => daemonClient.fetchAgents(options),
189
+ list: listAgents,
81
190
  ref: (agent) => createAgentHandle(agent),
82
191
  create: (options) => createAgent(options),
83
- subscribe: (handler) => daemonClient.on("agent_update", (message) => {
84
- handler(message.payload);
85
- }),
192
+ subscribe: listenAgents,
86
193
  },
87
194
  providers: {
88
195
  listModels: (provider, options) => daemonClient.listProviderModels(provider, options),
@@ -93,13 +200,16 @@ export function createPaseoApi(daemonClient) {
93
200
  },
94
201
  listAvailable: (options) => daemonClient.listAvailableProviders(options),
95
202
  snapshot: (options) => daemonClient.getProvidersSnapshot(options),
96
- waitForReady: (options) => waitForProvidersReady(daemonClient, options),
203
+ waitForReady: (options) => waitForProvidersReady(daemonClient, observeEvents(["providers_snapshot_update"]), lifetime.signal, options),
97
204
  refresh: (options) => daemonClient.refreshProvidersSnapshot(options),
98
205
  diagnostic: (provider, options) => daemonClient.getProviderDiagnostic(provider, options),
99
206
  listUsage: (options) => listProviderUsage(daemonClient, options),
100
- subscribe: (handler) => daemonClient.on("providers_snapshot_update", (message) => {
101
- handler(message.payload);
102
- }),
207
+ subscribe: (handler) => {
208
+ return subscribeEvent("providers_snapshot_update", (message) => {
209
+ if (message.type === "providers_snapshot_update")
210
+ handler(message.payload);
211
+ });
212
+ },
103
213
  },
104
214
  config: {
105
215
  get: (requestId) => daemonClient.getDaemonConfig(requestId),
@@ -107,7 +217,7 @@ export function createPaseoApi(daemonClient) {
107
217
  },
108
218
  };
109
219
  }
110
- function createWorkspaceHandleFactory(daemonClient, createAgent, terminals) {
220
+ function createWorkspaceHandleFactory(daemonClient, createAgent, terminals, listen) {
111
221
  return (workspace) => {
112
222
  const id = typeof workspace === "string" ? workspace : workspace.id;
113
223
  let current = typeof workspace === "string" ? null : workspace;
@@ -167,21 +277,19 @@ function createWorkspaceHandleFactory(daemonClient, createAgent, terminals) {
167
277
  }
168
278
  return result;
169
279
  },
170
- subscribe: (handler) => daemonClient.on("workspace_update", (message) => {
171
- const update = message.payload;
280
+ subscribe: (handler) => listen((update) => {
172
281
  if (update.kind === "upsert" && update.workspace.id === id) {
173
282
  current = update.workspace;
174
283
  handler(update);
175
284
  }
176
285
  if (update.kind === "remove" && update.id === id) {
177
- current = null;
178
286
  handler(update);
179
287
  }
180
288
  }),
181
289
  };
182
290
  };
183
291
  }
184
- function createAgentHandleFactory(daemonClient) {
292
+ function createAgentHandleFactory(daemonClient, listen, subscribeTimeline) {
185
293
  return (agent) => {
186
294
  const id = typeof agent === "string" ? agent : agent.id;
187
295
  let current = typeof agent === "string" ? null : agent;
@@ -196,13 +304,27 @@ function createAgentHandleFactory(daemonClient) {
196
304
  }
197
305
  return result;
198
306
  },
199
- subscribe: (handler) => daemonClient.subscribeAgentTimeline(id, (message) => {
200
- handler(message.type === "agent_stream"
201
- ? message.payload
202
- : {
203
- agentId: message.payload.agentId,
204
- event: { type: "replacement", epoch: message.payload.epoch },
205
- });
307
+ subscribe: (handler) => subscribeTimeline(id, (message) => {
308
+ switch (message.type) {
309
+ case "agent_stream":
310
+ return handler(message.payload);
311
+ case "agent.timeline.subscription_restored":
312
+ return handler({
313
+ agentId: id,
314
+ subscriptionId: message.payload.subscriptionId,
315
+ event: { type: "subscription_restored" },
316
+ });
317
+ case "agent.timeline.error":
318
+ return handler({
319
+ agentId: id,
320
+ event: { type: "error", error: message.payload.error },
321
+ });
322
+ case "agent.timeline.replacement":
323
+ return handler({
324
+ agentId: id,
325
+ event: { type: "replacement", epoch: message.payload.epoch },
326
+ });
327
+ }
206
328
  }),
207
329
  },
208
330
  get workspaceId() {
@@ -280,14 +402,12 @@ function createAgentHandleFactory(daemonClient) {
280
402
  detach: async () => {
281
403
  await daemonClient.detachAgent(id);
282
404
  },
283
- subscribe: (handler) => daemonClient.on("agent_update", (message) => {
284
- const update = message.payload;
405
+ subscribe: (handler) => listen((update) => {
285
406
  if (update.kind === "upsert" && update.agent.id === id) {
286
407
  current = update.agent;
287
408
  handler(update);
288
409
  }
289
410
  if (update.kind === "remove" && update.agentId === id) {
290
- current = null;
291
411
  handler(update);
292
412
  }
293
413
  }),
@@ -326,77 +446,88 @@ function listProviderUsage(daemonClient, options) {
326
446
  }
327
447
  return daemonClient.listProviderUsage(options);
328
448
  }
329
- function waitForProvidersReady(daemonClient, options = {}) {
330
- // COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
331
- if (daemonClient.getLastServerInfoMessage()?.features?.providersSnapshotCwd !== true) {
332
- return Promise.reject(new Error("Update the host to wait for provider discovery."));
333
- }
449
+ async function waitForProvidersReady(daemonClient, observation, signal, options = {}) {
334
450
  const { timeoutMs = 60000, ...snapshotOptions } = options;
335
- return new Promise((resolve, reject) => {
336
- let settled = false;
337
- let requestId = null;
338
- let snapshotCwd;
339
- const pendingUpdates = new Map();
340
- let latestEntries = [];
341
- const cleanup = () => {
342
- clearTimeout(timeout);
343
- unsubscribe();
344
- };
345
- const finish = (snapshot) => {
346
- if (settled)
347
- return;
348
- settled = true;
349
- cleanup();
350
- resolve(snapshot);
351
- };
352
- const fail = (error) => {
353
- if (settled)
354
- return;
355
- settled = true;
356
- cleanup();
357
- reject(error instanceof Error ? error : new Error(String(error)));
358
- };
359
- const updateMatches = (update) => update.cwd === snapshotCwd;
360
- const unsubscribe = daemonClient.on("providers_snapshot_update", (message) => {
361
- const update = message.payload;
362
- if (!requestId) {
363
- pendingUpdates.set(update.cwd, update);
364
- return;
365
- }
366
- if (!updateMatches(update))
367
- return;
368
- latestEntries = update.entries;
369
- if (update.entries.some((entry) => entry.status === "loading"))
370
- return;
371
- finish({ ...update, requestId });
451
+ try {
452
+ await observation.ready;
453
+ signal.throwIfAborted();
454
+ return await new Promise((resolve, reject) => {
455
+ let settled = false;
456
+ let requestId = null;
457
+ let snapshotCwd;
458
+ const pendingUpdates = new Map();
459
+ let latestEntries = [];
460
+ const cleanup = () => {
461
+ clearTimeout(timeout);
462
+ unsubscribe();
463
+ signal.removeEventListener("abort", abort);
464
+ };
465
+ const finish = (snapshot) => {
466
+ if (settled)
467
+ return;
468
+ settled = true;
469
+ cleanup();
470
+ resolve(snapshot);
471
+ };
472
+ const fail = (error) => {
473
+ if (settled)
474
+ return;
475
+ settled = true;
476
+ cleanup();
477
+ reject(error instanceof Error ? error : new Error(String(error)));
478
+ };
479
+ const updateMatches = (update) => update.cwd === snapshotCwd;
480
+ const unsubscribe = observation.subscribe({
481
+ snapshot: () => { },
482
+ update: (message) => {
483
+ if (message.type !== "providers_snapshot_update")
484
+ return;
485
+ const update = message.payload;
486
+ if (!requestId) {
487
+ pendingUpdates.set(update.cwd, update);
488
+ return;
489
+ }
490
+ if (!updateMatches(update))
491
+ return;
492
+ latestEntries = update.entries;
493
+ if (update.entries.some((entry) => entry.status === "loading"))
494
+ return;
495
+ finish({ ...update, requestId });
496
+ },
497
+ });
498
+ const abort = () => fail(new Error("Paseo API is disposed"));
499
+ signal.addEventListener("abort", abort, { once: true });
500
+ const timeout = setTimeout(() => {
501
+ const loading = latestEntries
502
+ .filter((entry) => entry.status === "loading")
503
+ .map((entry) => entry.provider)
504
+ .join(", ");
505
+ fail(new Error(loading
506
+ ? `Timed out waiting for providers: ${loading}`
507
+ : "Timed out waiting for provider discovery"));
508
+ }, timeoutMs);
509
+ void daemonClient
510
+ .getProvidersSnapshot(snapshotOptions)
511
+ .then((snapshot) => {
512
+ requestId = snapshot.requestId;
513
+ snapshotCwd = snapshot.cwd;
514
+ latestEntries = snapshot.entries;
515
+ if (!snapshot.entries.some((entry) => entry.status === "loading")) {
516
+ finish(snapshot);
517
+ return;
518
+ }
519
+ const pendingUpdate = pendingUpdates.get(snapshotCwd);
520
+ if (pendingUpdate && !pendingUpdate.entries.some((entry) => entry.status === "loading")) {
521
+ finish({ ...pendingUpdate, requestId });
522
+ }
523
+ return undefined;
524
+ })
525
+ .catch(fail);
372
526
  });
373
- const timeout = setTimeout(() => {
374
- const loading = latestEntries
375
- .filter((entry) => entry.status === "loading")
376
- .map((entry) => entry.provider)
377
- .join(", ");
378
- fail(new Error(loading
379
- ? `Timed out waiting for providers: ${loading}`
380
- : "Timed out waiting for provider discovery"));
381
- }, timeoutMs);
382
- void daemonClient
383
- .getProvidersSnapshot(snapshotOptions)
384
- .then((snapshot) => {
385
- requestId = snapshot.requestId;
386
- snapshotCwd = snapshot.cwd;
387
- latestEntries = snapshot.entries;
388
- if (!snapshot.entries.some((entry) => entry.status === "loading")) {
389
- finish(snapshot);
390
- return;
391
- }
392
- const pendingUpdate = pendingUpdates.get(snapshotCwd);
393
- if (pendingUpdate && !pendingUpdate.entries.some((entry) => entry.status === "loading")) {
394
- finish({ ...pendingUpdate, requestId });
395
- }
396
- return undefined;
397
- })
398
- .catch(fail);
399
- });
527
+ }
528
+ finally {
529
+ await observation.release();
530
+ }
400
531
  }
401
532
  function createGeneratedClientId() {
402
533
  const randomId = typeof globalThis.crypto?.randomUUID === "function"
@@ -7,15 +7,13 @@ type Snapshot = GetProvidersSnapshotResponseMessage["payload"];
7
7
  export declare class ProviderSnapshotUpdates {
8
8
  private readonly host;
9
9
  private pending;
10
- private paused;
11
10
  constructor(host: {
11
+ active(update: Update): boolean;
12
12
  fetch(cwd: string | undefined): Promise<Snapshot>;
13
13
  emit(update: Update): void;
14
14
  failed(error: unknown): void;
15
15
  });
16
16
  clear(): void;
17
- pause(): void;
18
- resume(): void;
19
17
  receive(update: Update): void;
20
18
  private resolve;
21
19
  }
@@ -3,57 +3,53 @@ export class ProviderSnapshotUpdates {
3
3
  constructor(host) {
4
4
  this.host = host;
5
5
  this.pending = new Map();
6
- this.paused = false;
7
6
  }
8
7
  clear() {
9
8
  this.pending.clear();
10
9
  }
11
- pause() {
12
- this.paused = true;
13
- // Invalidate old responses while retaining the announcements they would resolve.
14
- for (const [cwd, pending] of this.pending) {
15
- this.pending.set(cwd, { latest: pending.latest, running: false });
16
- }
17
- }
18
- resume() {
19
- this.paused = false;
20
- for (const pending of this.pending.values())
21
- this.receive(pending.latest);
22
- }
23
10
  receive(update) {
24
- const cwd = update.payload.cwd;
25
- const pending = this.pending.get(cwd) ?? { latest: update, running: false };
11
+ if (!this.host.active(update))
12
+ return;
13
+ const key = JSON.stringify([update.payload.subscriptionId, update.payload.cwd]);
14
+ const pending = this.pending.get(key) ?? { latest: update, running: false };
26
15
  pending.latest = update;
27
- this.pending.set(cwd, pending);
28
- if (this.paused || pending.running)
16
+ this.pending.set(key, pending);
17
+ if (pending.running)
29
18
  return;
30
19
  pending.running = true;
31
- void this.resolve(cwd, pending);
20
+ void this.resolve(key, pending);
32
21
  }
33
- async resolve(cwd, pending) {
22
+ async resolve(key, pending) {
34
23
  try {
35
- while (this.pending.get(cwd) === pending) {
24
+ while (this.pending.get(key) === pending) {
36
25
  const announced = pending.latest;
37
- const snapshot = await this.host.fetch(cwd);
38
- if (this.pending.get(cwd) !== pending)
26
+ if (!this.host.active(announced))
27
+ return;
28
+ const snapshot = await this.host.fetch(announced.payload.cwd);
29
+ if (!this.host.active(announced))
30
+ return;
31
+ if (this.pending.get(key) !== pending)
39
32
  return;
40
33
  // A change arriving while the body was in flight needs the current body.
41
34
  // If that response already covers it, no second fetch is necessary.
42
35
  if (pending.latest !== announced &&
43
36
  pending.latest.payload.snapshotHash !== snapshot.snapshotHash)
44
37
  continue;
45
- this.pending.delete(cwd);
46
- this.host.emit({ type: "providers_snapshot_update", payload: snapshot });
38
+ this.pending.delete(key);
39
+ this.host.emit({
40
+ type: "providers_snapshot_update",
41
+ payload: { ...snapshot, subscriptionId: announced.payload.subscriptionId },
42
+ });
47
43
  return;
48
44
  }
49
45
  }
50
46
  catch (error) {
51
- if (this.pending.get(cwd) === pending)
47
+ if (this.pending.get(key) === pending)
52
48
  this.host.failed(error);
53
49
  }
54
50
  finally {
55
- if (this.pending.get(cwd) === pending)
56
- this.pending.delete(cwd);
51
+ if (this.pending.get(key) === pending)
52
+ this.pending.delete(key);
57
53
  }
58
54
  }
59
55
  }
@@ -1,28 +1,25 @@
1
1
  import { type TerminalStreamFrame } from "@camerontaylor/paseo-protocol/binary-frames/index";
2
- import type { TerminalInput, TerminalState } from "@camerontaylor/paseo-protocol/messages";
2
+ import type { TerminalState } from "@camerontaylor/paseo-protocol/messages";
3
3
  export type TerminalStreamEvent = {
4
4
  terminalId: string;
5
+ subscriptionId: string;
6
+ } & ({
5
7
  type: "output";
6
8
  data: Uint8Array;
7
9
  } | {
8
- terminalId: string;
9
10
  type: "snapshot";
10
11
  state: TerminalState;
11
12
  } | {
12
- terminalId: string;
13
13
  type: "restore";
14
14
  data: Uint8Array;
15
- };
15
+ });
16
16
  export declare class TerminalStreamRouter {
17
- private readonly terminalSlots;
18
- private readonly slotTerminals;
17
+ private readonly slots;
19
18
  private readonly listeners;
19
+ /** Passive wire diagnostics. App state consumes the registration's callback. */
20
20
  onEvent(handler: (event: TerminalStreamEvent) => void): () => void;
21
- setSlot(terminalId: string, slot: number): void;
22
- removeTerminal(terminalId: string): void;
21
+ attach(subscriptionId: string, terminalId: string, slot: number, receive: (event: TerminalStreamEvent) => void): () => void;
23
22
  clearSlots(): void;
24
- encodeInput(terminalId: string, message: TerminalInput["message"]): Uint8Array | null;
25
23
  handleFrame(frame: TerminalStreamFrame): void;
26
- private emit;
27
24
  }
28
25
  //# sourceMappingURL=terminal-stream-router.d.ts.map