@deepnoodle/mobius 0.0.21 → 0.0.23

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/client.js CHANGED
@@ -3,74 +3,24 @@ export { RateLimitError } from "./retry.js";
3
3
  export const DEFAULT_BASE_URL = "https://api.mobiusops.ai";
4
4
  export const DEFAULT_PROJECT = "default";
5
5
  export const DEFAULT_NAMESPACE = DEFAULT_PROJECT;
6
- export class LeaseLostError extends Error {
7
- constructor(jobId) {
8
- super(`lease lost for job ${jobId}`);
9
- this.jobId = jobId;
10
- this.name = "LeaseLostError";
11
- }
12
- }
13
- /**
14
- * Thrown when the server returns HTTP 401 on a worker-loop request.
15
- * The credential has been revoked mid-execution; the process needs
16
- * to restart under a fresh credential. Distinct from
17
- * {@link LeaseLostError} (409 — lease reclaimed by scheduler) because
18
- * the remedy is operational, not workflow-level.
19
- */
20
6
  export class AuthRevokedError extends Error {
21
- constructor(jobId) {
22
- super(jobId
23
- ? `mobius: credential revoked (job ${jobId})`
24
- : "mobius: credential revoked");
25
- this.jobId = jobId;
7
+ constructor() {
8
+ super("mobius: credential revoked");
26
9
  this.name = "AuthRevokedError";
27
10
  }
28
11
  }
29
- /**
30
- * Thrown when the server returns HTTP 409 with `worker_instance_conflict`
31
- * on a claim. Another live process has already registered this
32
- * `worker_instance_id` in the project under a different session token.
33
- * Surfaces from {@link Worker.run} as a hard error so the operator
34
- * notices the misconfiguration instead of the worker silently retrying —
35
- * fix by configuring a unique instance ID per process or by relying on
36
- * the SDK's auto-detection.
37
- */
38
- export class WorkerInstanceConflictError extends Error {
39
- constructor(workerInstanceId, projectHandle, message) {
40
- super(message ??
41
- (workerInstanceId
42
- ? `mobius: worker_instance_id ${JSON.stringify(workerInstanceId)} is already registered in project ${JSON.stringify(projectHandle)} by another live process; configure a unique instance ID per process or rely on auto-detection`
43
- : "mobius: worker instance conflict"));
44
- this.workerInstanceId = workerInstanceId;
45
- this.projectHandle = projectHandle;
46
- this.name = "WorkerInstanceConflictError";
47
- }
48
- }
49
- /**
50
- * Thrown from {@link Client} construction when the API key or project
51
- * options are malformed (e.g. a project-pinned key whose handle prefix
52
- * doesn't match the server's handle regex, or a handle conflict
53
- * between `WithProjectHandle` and the handle embedded in the key).
54
- */
55
12
  export class ConfigError extends Error {
56
13
  constructor(message) {
57
14
  super(message);
58
15
  this.name = "ConfigError";
59
16
  }
60
17
  }
61
- // Mirrors the server-side handle regex (domain/validate.go) so the
62
- // extracted prefix is rejected here rather than as a 403 on first
63
- // request.
64
- const HANDLE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
65
- function extractHandleFromApiKey(apiKey) {
66
- const slash = apiKey.indexOf("/");
67
- if (slash < 0)
68
- return null;
69
- const handle = apiKey.slice(0, slash);
70
- if (!HANDLE_RE.test(handle)) {
71
- throw new ConfigError(`invalid project handle prefix in API key: ${JSON.stringify(handle)}`);
18
+ export class LeaseLostError extends Error {
19
+ constructor(jobId) {
20
+ super(`lease lost for job ${jobId}`);
21
+ this.jobId = jobId;
22
+ this.name = "LeaseLostError";
72
23
  }
73
- return handle;
74
24
  }
75
25
  export class PayloadTooLargeError extends Error {
76
26
  constructor(jobId) {
@@ -79,12 +29,6 @@ export class PayloadTooLargeError extends Error {
79
29
  this.name = "PayloadTooLargeError";
80
30
  }
81
31
  }
82
- /**
83
- * Legacy per-job rate-limit error raised by {@link Client.emitJobEvents}.
84
- * Subclass of {@link RateLimitError} so callers catching the newer,
85
- * transport-raised {@link RateLimitError} also catch this. New code
86
- * should prefer {@link RateLimitError}.
87
- */
88
32
  export class RateLimitedError extends RateLimitError {
89
33
  constructor(jobId, retryAfter) {
90
34
  super({
@@ -97,21 +41,34 @@ export class RateLimitedError extends RateLimitError {
97
41
  this.jobId = jobId;
98
42
  }
99
43
  }
100
- /**
101
- * Low-level Mobius runtime API client. Prefer {@link Worker} in worker.ts
102
- * rather than calling these methods directly.
103
- *
104
- * Request and response shapes mirror the OpenAPI spec exactly (snake_case).
105
- * A worker claims individual *jobs* — one action invocation on behalf of
106
- * a workflow run — and reports each job's result back via this client.
107
- */
44
+ export class WorkerInstanceConflictError extends Error {
45
+ constructor(workerInstanceId, projectHandle, message) {
46
+ super(message ??
47
+ (workerInstanceId
48
+ ? `mobius: worker_instance_id ${JSON.stringify(workerInstanceId)} is already registered in project ${JSON.stringify(projectHandle)} by another live process`
49
+ : "mobius: worker instance conflict"));
50
+ this.workerInstanceId = workerInstanceId;
51
+ this.projectHandle = projectHandle;
52
+ this.name = "WorkerInstanceConflictError";
53
+ }
54
+ }
55
+ const HANDLE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
56
+ function extractHandleFromApiKey(apiKey) {
57
+ if (!apiKey.startsWith("mbx_") && !apiKey.startsWith("mbc_"))
58
+ return null;
59
+ const dot = apiKey.lastIndexOf(".");
60
+ if (dot < 0 || dot === apiKey.length - 1)
61
+ return null;
62
+ const handle = apiKey.slice(dot + 1);
63
+ if (!HANDLE_RE.test(handle)) {
64
+ throw new ConfigError(`invalid project handle suffix in API key: ${JSON.stringify(handle)}`);
65
+ }
66
+ return handle;
67
+ }
108
68
  export class Client {
109
69
  constructor(opts) {
110
70
  this.baseURL = (opts.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
111
71
  const explicitProject = opts.project ?? opts.namespace;
112
- // Project-pinned keys arrive as "<handle>/mbx_<secret>". Split the
113
- // handle off and use it as the project; any explicit project option
114
- // must either match or be the default sentinel.
115
72
  const handleInKey = extractHandleFromApiKey(opts.apiKey);
116
73
  if (handleInKey != null) {
117
74
  if (explicitProject != null &&
@@ -131,98 +88,109 @@ export class Client {
131
88
  this.timeoutMs = opts.timeoutMs ?? 60000;
132
89
  this.fetchFn = wrapFetchWithRetry((input, init) => globalThis.fetch(input, init), { maxRetries: opts.retry ?? DEFAULT_MAX_RETRIES });
133
90
  }
134
- /**
135
- * Long-poll for the next claimable job. Returns null when the poll
136
- * window closes without a job being available.
137
- */
138
- async claimJob(req, signal) {
139
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/jobs/claim`, { method: "POST", body: req, signal });
140
- if (resp.status === 204)
141
- return null;
142
- if (resp.status === 401)
143
- throw new AuthRevokedError();
144
- if (resp.status === 409) {
145
- const text = await resp.text().catch(() => "");
146
- // The backend wraps errors as {"error":{"code","message"}}.
147
- let body = {};
148
- try {
149
- body = text ? JSON.parse(text) : {};
150
- }
151
- catch {
152
- body = {};
153
- }
154
- if (body.error?.code === "worker_instance_conflict") {
155
- throw new WorkerInstanceConflictError(req.worker_instance_id, this.project, body.error.message);
156
- }
157
- throw new Error(`mobius API POST /jobs/claim: HTTP 409: ${text || "(no body)"}`);
158
- }
91
+ workerSocketURL() {
92
+ const url = new URL(this.baseURL);
93
+ if (url.protocol === "http:")
94
+ url.protocol = "ws:";
95
+ if (url.protocol === "https:")
96
+ url.protocol = "wss:";
97
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(this.project)}/workers/socket`;
98
+ url.search = "";
99
+ return url.toString();
100
+ }
101
+ async listAutomations(opts = {}) {
102
+ const resp = await this.request(withQuery("/v1/projects/:project/automations", opts), {
103
+ method: "GET",
104
+ });
105
+ return (await resp.json());
106
+ }
107
+ async getAutomation(handle) {
108
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}`, { method: "GET" });
109
+ return (await resp.json());
110
+ }
111
+ async createAutomation(opts) {
112
+ const body = removeUndefined({
113
+ name: opts.name,
114
+ handle: opts.handle,
115
+ description: opts.description,
116
+ default_agent_id: opts.default_agent_id,
117
+ default_inputs: opts.default_inputs,
118
+ settings: opts.settings,
119
+ tags: opts.tags,
120
+ triggers: opts.triggers,
121
+ });
122
+ const resp = await this.request("/v1/projects/:project/automations", {
123
+ method: "POST",
124
+ body,
125
+ });
159
126
  return (await resp.json());
160
127
  }
161
- /** Refresh the lease on a claimed job. */
162
- async heartbeatJob(jobId, req) {
163
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/jobs/${encodeURIComponent(jobId)}/heartbeat`, { method: "POST", body: req });
164
- if (resp.status === 401)
165
- throw new AuthRevokedError(jobId);
166
- if (resp.status === 409)
167
- throw new LeaseLostError(jobId);
128
+ async updateAutomation(handle, opts) {
129
+ const body = removeUndefined(opts);
130
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}`, { method: "PATCH", body });
168
131
  return (await resp.json());
169
132
  }
170
- /** Report the terminal outcome(s) of a claimed job. */
171
- async reportJob(jobId, req) {
172
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/jobs/${encodeURIComponent(jobId)}/report`, { method: "POST", body: req });
173
- if (resp.status === 401)
174
- throw new AuthRevokedError(jobId);
175
- if (resp.status === 409)
176
- throw new LeaseLostError(jobId);
133
+ async deleteAutomation(handle) {
134
+ await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}`, { method: "DELETE" });
177
135
  }
178
- async emitJobEvents(jobId, req) {
179
- // 429 responses surface as RateLimitError (thrown from the retry
180
- // transport below). 401/409/413 are non-retryable and handled here.
181
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/jobs/${encodeURIComponent(jobId)}/events`, { method: "POST", body: req });
182
- if (resp.status === 401)
183
- throw new AuthRevokedError(jobId);
184
- if (resp.status === 409)
185
- throw new LeaseLostError(jobId);
186
- if (resp.status === 413)
187
- throw new PayloadTooLargeError(jobId);
136
+ async listAutomationVersions(handle) {
137
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}/versions`, { method: "GET" });
138
+ return (await resp.json());
188
139
  }
189
- async emitJobEvent(jobId, req) {
190
- await this.emitJobEvents(jobId, {
191
- worker_instance_id: req.worker_instance_id,
192
- lease_token: req.lease_token,
193
- events: [{ type: req.type, payload: req.payload }],
140
+ async createAutomationVersion(handle, spec, opts = {}) {
141
+ const body = removeUndefined({
142
+ spec: spec,
143
+ compiled_plan: opts.compiled_plan,
194
144
  });
145
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}/versions`, { method: "POST", body });
146
+ const version = (await resp.json());
147
+ if (opts.publish) {
148
+ await this.publishAutomationVersion(handle, version.version);
149
+ }
150
+ return version;
195
151
  }
196
- async startRun(spec, opts = {}) {
197
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs`, { method: "POST", body: { mode: "inline", spec, ...opts } });
152
+ async publishAutomationVersion(handle, version) {
153
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(handle)}/versions/${version}/publication`, { method: "POST" });
198
154
  return (await resp.json());
199
155
  }
200
- async startWorkflowRun(workflowId, opts = {}) {
201
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows/${encodeURIComponent(workflowId)}/runs`, { method: "POST", body: opts });
156
+ async startRun(automationHandle, opts = {}) {
157
+ return this.startAutomationRun(automationHandle, opts);
158
+ }
159
+ async startAutomationRun(automationHandle, opts = {}) {
160
+ const body = removeUndefined({
161
+ inputs: opts.inputs,
162
+ source: opts.source,
163
+ external_id: opts.external_id,
164
+ });
165
+ const resp = await this.request(`/v1/projects/:project/automations/${encodeURIComponent(automationHandle)}/runs`, { method: "POST", body });
202
166
  return (await resp.json());
203
167
  }
204
168
  async listRuns(opts = {}) {
205
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/runs`, opts);
206
- const resp = await this.request(path, { method: "GET" });
169
+ const resp = await this.request(withQuery("/v1/projects/:project/runs", opts), {
170
+ method: "GET",
171
+ });
207
172
  return (await resp.json());
208
173
  }
209
174
  async getRun(runId) {
210
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}`, { method: "GET" });
175
+ const resp = await this.request(`/v1/projects/:project/runs/${encodeURIComponent(runId)}`, { method: "GET" });
211
176
  return (await resp.json());
212
177
  }
213
- async cancelRun(runId) {
214
- await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/cancellations`, { method: "POST" });
215
- }
216
- async resumeRun(runId) {
217
- await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/resumptions`, { method: "POST" });
178
+ async cancelRun(runId, reason) {
179
+ const body = removeUndefined({ reason });
180
+ const resp = await this.request(`/v1/projects/:project/runs/${encodeURIComponent(runId)}/cancellations`, { method: "POST", body });
181
+ return (await resp.json());
218
182
  }
219
- async sendRunSignal(runId, req) {
220
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/signals`, { method: "POST", body: req });
183
+ async signalRun(runId, stepKey, result) {
184
+ const body = removeUndefined({
185
+ step_key: stepKey,
186
+ result,
187
+ });
188
+ const resp = await this.request(`/v1/projects/:project/runs/${encodeURIComponent(runId)}/signals`, { method: "POST", body });
221
189
  return (await resp.json());
222
190
  }
223
191
  async *watchRun(runId, opts = {}) {
224
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/events`, opts.since && opts.since > 0 ? { since: opts.since } : {});
225
- const resp = await this.fetchFn(this.baseURL + path, {
192
+ const path = withQuery(`/v1/projects/:project/runs/${encodeURIComponent(runId)}/events.stream`, opts.since && opts.since > 0 ? { since_sequence: opts.since } : {});
193
+ const resp = await this.fetchFn(this.url(path), {
226
194
  method: "GET",
227
195
  headers: this.headers,
228
196
  signal: opts.signal,
@@ -248,16 +216,11 @@ export class Client {
248
216
  if (isTerminalRunStatus(run.status))
249
217
  return run;
250
218
  try {
251
- for await (const ev of this.watchRun(runId, {
252
- since,
253
- signal: opts.signal,
254
- })) {
255
- if (ev.seq > since)
256
- since = ev.seq;
257
- if (ev.type !== "run_updated")
258
- continue;
259
- const status = ev.data.status;
260
- if (status === "completed" || status === "failed") {
219
+ for await (const ev of this.watchRun(runId, { ...opts, since })) {
220
+ if (ev.sequence > since)
221
+ since = ev.sequence;
222
+ const status = ev.payload?.status;
223
+ if (typeof status === "string" && isTerminalRunStatus(status)) {
261
224
  return await this.getRun(runId);
262
225
  }
263
226
  }
@@ -269,60 +232,6 @@ export class Client {
269
232
  await delay(reconnectDelayMs, opts.signal);
270
233
  }
271
234
  }
272
- async listWorkflows(opts = {}) {
273
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/workflows`, opts);
274
- const resp = await this.request(path, { method: "GET" });
275
- return (await resp.json());
276
- }
277
- async getWorkflow(workflowId) {
278
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows/${encodeURIComponent(workflowId)}`, { method: "GET" });
279
- return (await resp.json());
280
- }
281
- async createWorkflow(spec, opts = {}) {
282
- const body = createWorkflowRequest(spec, opts);
283
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows`, { method: "POST", body });
284
- return (await resp.json());
285
- }
286
- async updateWorkflow(workflowId, opts) {
287
- const body = removeUndefined({
288
- expected_version: opts.expected_version,
289
- name: opts.name,
290
- description: opts.description,
291
- published_as_tool: opts.published_as_tool,
292
- spec: opts.spec,
293
- tags: opts.tags,
294
- });
295
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows/${encodeURIComponent(workflowId)}`, { method: "PATCH", body });
296
- return (await resp.json());
297
- }
298
- async ensureWorkflow(spec, opts = {}) {
299
- const desired = normalizeWorkflowOptions(spec, opts);
300
- const existing = await this.findWorkflow(desired);
301
- if (!existing) {
302
- return {
303
- definition: await this.createWorkflow(spec, desired),
304
- created: true,
305
- updated: false,
306
- };
307
- }
308
- const current = await this.getWorkflow(existing.id);
309
- const update = workflowUpdateForDiff(current, spec, desired);
310
- if (!update) {
311
- return { definition: current, created: false, updated: false };
312
- }
313
- return {
314
- definition: await this.updateWorkflow(current.id, update),
315
- created: false,
316
- updated: true,
317
- };
318
- }
319
- async syncWorkflows(defs) {
320
- const results = [];
321
- for (const def of defs) {
322
- results.push(await this.ensureWorkflow(def.spec, def.options ?? {}));
323
- }
324
- return results;
325
- }
326
235
  async request(path, opts) {
327
236
  const timeout = AbortSignal.timeout(this.timeoutMs);
328
237
  const signal = opts.signal ? anySignal(opts.signal, timeout) : timeout;
@@ -331,56 +240,37 @@ export class Client {
331
240
  headers: this.headers,
332
241
  signal,
333
242
  };
334
- if (opts.body != null) {
243
+ if (opts.body != null)
335
244
  init.body = JSON.stringify(opts.body);
336
- }
337
- const resp = await this.fetchFn(this.baseURL + path, init);
338
- if (!resp.ok &&
339
- resp.status !== 204 &&
340
- resp.status !== 401 &&
341
- resp.status !== 409 &&
342
- resp.status !== 413 &&
343
- resp.status !== 429) {
245
+ const resp = await this.fetchFn(this.url(path), init);
246
+ if (!resp.ok && resp.status !== 204) {
247
+ if (resp.status === 401)
248
+ throw new AuthRevokedError();
344
249
  const text = await resp.text().catch(() => "");
345
250
  throw new Error(`mobius API ${opts.method} ${path}: HTTP ${resp.status}: ${text}`);
346
251
  }
347
252
  return resp;
348
253
  }
349
- async findWorkflow(desired) {
350
- if (!desired.handle && !desired.name) {
351
- throw new Error("mobius: ensure workflow requires a handle, name, or spec name");
352
- }
353
- let cursor = "";
354
- for (;;) {
355
- const page = await this.listWorkflows({ cursor, limit: 100 });
356
- for (const item of page.items) {
357
- if (desired.handle && item.handle === desired.handle)
358
- return item;
359
- if (!desired.handle && item.name === desired.name)
360
- return item;
361
- }
362
- if (!page.has_more || !page.next_cursor)
363
- return null;
364
- cursor = page.next_cursor;
365
- }
254
+ url(path) {
255
+ return this.baseURL + path.replace(":project", encodeURIComponent(this.project));
366
256
  }
367
257
  }
258
+ export function isTerminalRunStatus(status) {
259
+ return status === "completed" || status === "failed" || status === "cancelled";
260
+ }
368
261
  function anySignal(...signals) {
369
262
  const controller = new AbortController();
370
- for (const s of signals) {
371
- if (s.aborted) {
372
- controller.abort(s.reason);
263
+ for (const signal of signals) {
264
+ if (signal.aborted) {
265
+ controller.abort(signal.reason);
373
266
  break;
374
267
  }
375
- s.addEventListener("abort", () => controller.abort(s.reason), {
268
+ signal.addEventListener("abort", () => controller.abort(signal.reason), {
376
269
  once: true,
377
270
  });
378
271
  }
379
272
  return controller.signal;
380
273
  }
381
- export function isTerminalRunStatus(status) {
382
- return status === "completed" || status === "failed";
383
- }
384
274
  function withQuery(path, params) {
385
275
  const query = new URLSearchParams();
386
276
  for (const [key, value] of Object.entries(params)) {
@@ -397,143 +287,49 @@ function withQuery(path, params) {
397
287
  const qs = query.toString();
398
288
  return qs ? `${path}?${qs}` : path;
399
289
  }
400
- function createWorkflowRequest(spec, opts) {
401
- const normalized = normalizeWorkflowOptions(spec, opts);
402
- return removeUndefined({
403
- name: normalized.name,
404
- handle: normalized.handle,
405
- description: normalized.description,
406
- published_as_tool: normalized.published_as_tool,
407
- spec,
408
- tags: normalized.tags,
409
- });
410
- }
411
- function normalizeWorkflowOptions(spec, opts) {
412
- return { ...opts, name: opts.name || spec.name };
413
- }
414
- function workflowUpdateForDiff(current, spec, desired) {
415
- const update = {
416
- expected_version: current.latest_version,
417
- };
418
- let changed = false;
419
- if (desired.name && current.name !== desired.name) {
420
- update.name = desired.name;
421
- changed = true;
422
- }
423
- if (desired.description && current.description !== desired.description) {
424
- update.description = desired.description;
425
- changed = true;
426
- }
427
- if (desired.published_as_tool !== undefined &&
428
- current.published_as_tool !== desired.published_as_tool) {
429
- update.published_as_tool = desired.published_as_tool;
430
- changed = true;
431
- }
432
- if (desired.tags !== undefined && !jsonEqual(current.tags, desired.tags)) {
433
- update.tags = desired.tags;
434
- changed = true;
435
- }
436
- if (!jsonEqual(current.spec, spec)) {
437
- update.spec = spec;
438
- changed = true;
439
- }
440
- return changed ? update : null;
441
- }
442
- function jsonEqual(a, b) {
443
- return stableJsonStringify(a) === stableJsonStringify(b);
444
- }
445
- function stableJsonStringify(value) {
446
- if (value === null || typeof value !== "object") {
447
- return JSON.stringify(value);
448
- }
449
- if (Array.isArray(value)) {
450
- return `[${value.map((item) => stableJsonStringify(item) ?? "null").join(",")}]`;
451
- }
452
- const obj = value;
453
- const entries = Object.keys(obj)
454
- .sort()
455
- .filter((key) => obj[key] !== undefined)
456
- .map((key) => `${JSON.stringify(key)}:${stableJsonStringify(obj[key])}`);
457
- return `{${entries.join(",")}}`;
458
- }
459
290
  function removeUndefined(obj) {
460
- return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
291
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
292
+ }
293
+ async function delay(ms, signal) {
294
+ if (ms <= 0)
295
+ return;
296
+ await new Promise((resolve, reject) => {
297
+ const timer = setTimeout(resolve, ms);
298
+ if (signal) {
299
+ signal.addEventListener("abort", () => {
300
+ clearTimeout(timer);
301
+ reject(signal.reason ?? new Error("aborted"));
302
+ }, { once: true });
303
+ }
304
+ });
461
305
  }
462
306
  async function* parseSSE(body) {
463
307
  const reader = body.getReader();
464
308
  const decoder = new TextDecoder();
465
309
  let buffer = "";
466
- let event = "";
467
- let id = "";
468
- let dataLines = [];
469
- const dispatch = () => {
470
- if (dataLines.length === 0) {
471
- event = "";
472
- return null;
473
- }
474
- const out = {
475
- event: event || "message",
476
- id,
477
- data: dataLines.join("\n"),
478
- };
479
- event = "";
480
- dataLines = [];
481
- return out;
482
- };
483
- const processLine = (rawLine) => {
484
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
485
- if (line === "")
486
- return dispatch();
487
- if (line.startsWith(":"))
488
- return null;
489
- const colon = line.indexOf(":");
490
- const field = colon >= 0 ? line.slice(0, colon) : line;
491
- let value = colon >= 0 ? line.slice(colon + 1) : "";
492
- if (value.startsWith(" "))
493
- value = value.slice(1);
494
- if (field === "event")
495
- event = value;
496
- if (field === "id" && !value.includes("\0"))
497
- id = value;
498
- if (field === "data")
499
- dataLines.push(value);
500
- return null;
501
- };
502
- for (;;) {
503
- const { value, done } = await reader.read();
504
- if (done)
505
- break;
506
- buffer += decoder.decode(value, { stream: true });
310
+ try {
507
311
  for (;;) {
508
- const newline = buffer.indexOf("\n");
509
- if (newline < 0)
312
+ const { value, done } = await reader.read();
313
+ if (done)
510
314
  break;
511
- const line = buffer.slice(0, newline);
512
- buffer = buffer.slice(newline + 1);
513
- const evt = processLine(line);
514
- if (evt)
515
- yield evt;
315
+ buffer += decoder.decode(value, { stream: true });
316
+ for (;;) {
317
+ const match = /\r?\n\r?\n/.exec(buffer);
318
+ if (!match)
319
+ break;
320
+ const raw = buffer.slice(0, match.index);
321
+ buffer = buffer.slice(match.index + match[0].length);
322
+ const data = raw
323
+ .split(/\r?\n/)
324
+ .filter((line) => line.startsWith("data:"))
325
+ .map((line) => line.slice(5).trimStart())
326
+ .join("\n");
327
+ yield { data };
328
+ }
516
329
  }
517
330
  }
518
- buffer += decoder.decode();
519
- if (buffer.length > 0) {
520
- const evt = processLine(buffer);
521
- if (evt)
522
- yield evt;
331
+ finally {
332
+ reader.releaseLock();
523
333
  }
524
- const evt = dispatch();
525
- if (evt)
526
- yield evt;
527
- }
528
- function delay(ms, signal) {
529
- if (signal?.aborted)
530
- return Promise.reject(signal.reason);
531
- return new Promise((resolve, reject) => {
532
- const timeout = setTimeout(resolve, ms);
533
- signal?.addEventListener("abort", () => {
534
- clearTimeout(timeout);
535
- reject(signal.reason);
536
- }, { once: true });
537
- });
538
334
  }
539
335
  //# sourceMappingURL=client.js.map