@deepnoodle/mobius 0.0.20 → 0.0.22

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,99 +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 status of a claimed job. */
171
- async completeJob(jobId, req) {
172
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/jobs/${encodeURIComponent(jobId)}/complete`, { 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
- worker_session_token: req.worker_session_token,
193
- attempt: req.attempt,
194
- 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,
195
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;
196
151
  }
197
- async startRun(spec, opts = {}) {
198
- 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" });
199
154
  return (await resp.json());
200
155
  }
201
- async startWorkflowRun(workflowId, opts = {}) {
202
- 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 });
203
166
  return (await resp.json());
204
167
  }
205
168
  async listRuns(opts = {}) {
206
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/runs`, opts);
207
- const resp = await this.request(path, { method: "GET" });
169
+ const resp = await this.request(withQuery("/v1/projects/:project/runs", opts), {
170
+ method: "GET",
171
+ });
208
172
  return (await resp.json());
209
173
  }
210
174
  async getRun(runId) {
211
- 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" });
212
176
  return (await resp.json());
213
177
  }
214
- async cancelRun(runId) {
215
- await this.request(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/cancellations`, { method: "POST" });
216
- }
217
- async resumeRun(runId) {
218
- 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());
219
182
  }
220
- async sendRunSignal(runId, req) {
221
- 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 });
222
189
  return (await resp.json());
223
190
  }
224
191
  async *watchRun(runId, opts = {}) {
225
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/runs/${encodeURIComponent(runId)}/events`, opts.since && opts.since > 0 ? { since: opts.since } : {});
226
- 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), {
227
194
  method: "GET",
228
195
  headers: this.headers,
229
196
  signal: opts.signal,
@@ -249,16 +216,11 @@ export class Client {
249
216
  if (isTerminalRunStatus(run.status))
250
217
  return run;
251
218
  try {
252
- for await (const ev of this.watchRun(runId, {
253
- since,
254
- signal: opts.signal,
255
- })) {
256
- if (ev.seq > since)
257
- since = ev.seq;
258
- if (ev.type !== "run_updated")
259
- continue;
260
- const status = ev.data.status;
261
- 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)) {
262
224
  return await this.getRun(runId);
263
225
  }
264
226
  }
@@ -270,59 +232,6 @@ export class Client {
270
232
  await delay(reconnectDelayMs, opts.signal);
271
233
  }
272
234
  }
273
- async listWorkflows(opts = {}) {
274
- const path = withQuery(`/v1/projects/${encodeURIComponent(this.project)}/workflows`, opts);
275
- const resp = await this.request(path, { method: "GET" });
276
- return (await resp.json());
277
- }
278
- async getWorkflow(workflowId) {
279
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows/${encodeURIComponent(workflowId)}`, { method: "GET" });
280
- return (await resp.json());
281
- }
282
- async createWorkflow(spec, opts = {}) {
283
- const body = createWorkflowRequest(spec, opts);
284
- const resp = await this.request(`/v1/projects/${encodeURIComponent(this.project)}/workflows`, { method: "POST", body });
285
- return (await resp.json());
286
- }
287
- async updateWorkflow(workflowId, opts = {}) {
288
- const body = removeUndefined({
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,133 +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
- if (desired.name && current.name !== desired.name)
417
- update.name = desired.name;
418
- if (desired.description && current.description !== desired.description) {
419
- update.description = desired.description;
420
- }
421
- if (desired.published_as_tool !== undefined &&
422
- current.published_as_tool !== desired.published_as_tool) {
423
- update.published_as_tool = desired.published_as_tool;
424
- }
425
- if (desired.tags !== undefined && !jsonEqual(current.tags, desired.tags)) {
426
- update.tags = desired.tags;
427
- }
428
- if (!jsonEqual(current.spec, spec))
429
- update.spec = spec;
430
- return Object.keys(update).length > 0 ? update : null;
431
- }
432
- function jsonEqual(a, b) {
433
- return stableJsonStringify(a) === stableJsonStringify(b);
434
- }
435
- function stableJsonStringify(value) {
436
- if (value === null || typeof value !== "object") {
437
- return JSON.stringify(value);
438
- }
439
- if (Array.isArray(value)) {
440
- return `[${value.map((item) => stableJsonStringify(item) ?? "null").join(",")}]`;
441
- }
442
- const obj = value;
443
- const entries = Object.keys(obj)
444
- .sort()
445
- .filter((key) => obj[key] !== undefined)
446
- .map((key) => `${JSON.stringify(key)}:${stableJsonStringify(obj[key])}`);
447
- return `{${entries.join(",")}}`;
448
- }
449
290
  function removeUndefined(obj) {
450
- 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
+ });
451
305
  }
452
306
  async function* parseSSE(body) {
453
307
  const reader = body.getReader();
454
308
  const decoder = new TextDecoder();
455
309
  let buffer = "";
456
- let event = "";
457
- let id = "";
458
- let dataLines = [];
459
- const dispatch = () => {
460
- if (dataLines.length === 0) {
461
- event = "";
462
- return null;
463
- }
464
- const out = {
465
- event: event || "message",
466
- id,
467
- data: dataLines.join("\n"),
468
- };
469
- event = "";
470
- dataLines = [];
471
- return out;
472
- };
473
- const processLine = (rawLine) => {
474
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
475
- if (line === "")
476
- return dispatch();
477
- if (line.startsWith(":"))
478
- return null;
479
- const colon = line.indexOf(":");
480
- const field = colon >= 0 ? line.slice(0, colon) : line;
481
- let value = colon >= 0 ? line.slice(colon + 1) : "";
482
- if (value.startsWith(" "))
483
- value = value.slice(1);
484
- if (field === "event")
485
- event = value;
486
- if (field === "id" && !value.includes("\0"))
487
- id = value;
488
- if (field === "data")
489
- dataLines.push(value);
490
- return null;
491
- };
492
- for (;;) {
493
- const { value, done } = await reader.read();
494
- if (done)
495
- break;
496
- buffer += decoder.decode(value, { stream: true });
310
+ try {
497
311
  for (;;) {
498
- const newline = buffer.indexOf("\n");
499
- if (newline < 0)
312
+ const { value, done } = await reader.read();
313
+ if (done)
500
314
  break;
501
- const line = buffer.slice(0, newline);
502
- buffer = buffer.slice(newline + 1);
503
- const evt = processLine(line);
504
- if (evt)
505
- 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
+ }
506
329
  }
507
330
  }
508
- buffer += decoder.decode();
509
- if (buffer.length > 0) {
510
- const evt = processLine(buffer);
511
- if (evt)
512
- yield evt;
331
+ finally {
332
+ reader.releaseLock();
513
333
  }
514
- const evt = dispatch();
515
- if (evt)
516
- yield evt;
517
- }
518
- function delay(ms, signal) {
519
- if (signal?.aborted)
520
- return Promise.reject(signal.reason);
521
- return new Promise((resolve, reject) => {
522
- const timeout = setTimeout(resolve, ms);
523
- signal?.addEventListener("abort", () => {
524
- clearTimeout(timeout);
525
- reject(signal.reason);
526
- }, { once: true });
527
- });
528
334
  }
529
335
  //# sourceMappingURL=client.js.map