@work-shape/catalyst-api-client 0.8.0-rc.2 → 0.8.0-rc.4

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
@@ -127,7 +127,12 @@ type ControlPart<Op extends Operation> = {
127
127
  } ? {
128
128
  /** Called when the server has nothing after the given position and sends no stream. */
129
129
  onCaughtUp?: () => void;
130
- } : unknown);
130
+ } & (Op["response"] extends {
131
+ skip: infer Skip extends z.ZodType;
132
+ } ? {
133
+ /** Called for an event this client does not know, which the stream passes over. */
134
+ onSkipped?: (value: z.output<Skip>) => void;
135
+ } : unknown) : unknown);
131
136
  /**
132
137
  * An operation that takes an `Idempotency-Key` gets a new one with every call. A caller that
133
138
  * sends the same call again after an answer that never arrived passes the key of the first.
@@ -139,7 +144,18 @@ type IdempotencyPart<Op> = Op extends {
139
144
  } ? "Idempotency-Key" extends Header ? {
140
145
  idempotencyKey?: string;
141
146
  } : unknown : unknown;
142
- type InputOf<Op extends Operation> = ParamsPart<Op["path"]> & QueryPart<Op> & BodyPart<Op> & IdempotencyPart<Op> & ControlPart<Op>;
147
+ /**
148
+ * An operation that takes `Catalyst-Unattended` is sent with it when `unattended` is set: nobody
149
+ * watches the caller's terminal, so a call that needs a confirmation waits in the Inbox.
150
+ */
151
+ type UnattendedPart<Op> = Op extends {
152
+ headers: {
153
+ request: readonly (infer Header extends OperationRequestHeader)[];
154
+ };
155
+ } ? "Catalyst-Unattended" extends Header ? {
156
+ unattended?: boolean;
157
+ } : unknown : unknown;
158
+ type InputOf<Op extends Operation> = ParamsPart<Op["path"]> & QueryPart<Op> & BodyPart<Op> & IdempotencyPart<Op> & UnattendedPart<Op> & ControlPart<Op>;
143
159
  /**
144
160
  * How a call of an operation that may wait for an approval came out. A refusal and a failure
145
161
  * are thrown as `ApiError`, as for every operation.
@@ -153,7 +169,10 @@ type OperationOutcome<Output, Run = unknown> = {
153
169
  status: "done";
154
170
  output: Output;
155
171
  }
156
- /** Nothing ran yet: somebody else decides. `location` is where the run is read again. */
172
+ /**
173
+ * Nothing ran yet: somebody decides it, an approver or, for a run `pending_confirmation`, the
174
+ * caller in their Inbox. `location` is where the run is read again.
175
+ */
157
176
  | {
158
177
  status: "pending_approval";
159
178
  run: Run;
@@ -169,6 +188,8 @@ type OutputOf<Op extends Operation> = Op["response"] extends {
169
188
  kind: "sse";
170
189
  schema: infer Schema extends z.ZodType;
171
190
  } ? AsyncIterable<z.output<Schema>> : Op["response"] extends {
191
+ kind: "redirect";
192
+ } ? Promise<undefined> : Op["response"] extends {
172
193
  schema: infer Schema extends z.ZodType;
173
194
  } ? Op extends {
174
195
  accepted: infer Accepted extends z.ZodType;
package/dist/index.js CHANGED
@@ -209,6 +209,9 @@ function createApiTransport(options) {
209
209
  if (operation.headers?.request?.includes("Idempotency-Key")) {
210
210
  headers.set("idempotency-key", request.idempotencyKey ?? createIdempotencyKey());
211
211
  }
212
+ if (request.unattended && operation.headers?.request?.includes("Catalyst-Unattended")) {
213
+ headers.set("catalyst-unattended", "1");
214
+ }
212
215
  const token = await options.getToken?.();
213
216
  if (token) {
214
217
  headers.set("authorization", `Bearer ${token}`);
@@ -311,7 +314,13 @@ function hasEveryOperation(operations, tree) {
311
314
  function callOperation(transport, operation, request) {
312
315
  const { response } = operation;
313
316
  if (response.kind === "sse") {
314
- return streamEvents(transport, operation, response.schema, request);
317
+ return streamEvents(transport, operation, response, request);
318
+ }
319
+ if (response.kind === "redirect") {
320
+ return transport.send(operation, request).then(async (answer) => {
321
+ await answer.body?.cancel();
322
+ return void 0;
323
+ });
315
324
  }
316
325
  if (response.kind === "blob") {
317
326
  return transport.send(operation, request).then((answer) => readBody(() => answer.blob(), request));
@@ -341,7 +350,7 @@ function callOperation(transport, operation, request) {
341
350
  };
342
351
  });
343
352
  }
344
- async function* streamEvents(transport, operation, schema, request) {
353
+ async function* streamEvents(transport, operation, response, request) {
345
354
  const answer = await transport.send(operation, request);
346
355
  if (answer.status === 204) {
347
356
  request.onCaughtUp?.();
@@ -352,7 +361,12 @@ async function* streamEvents(transport, operation, schema, request) {
352
361
  }
353
362
  try {
354
363
  for await (const data of readServerSentEvents(answer.body)) {
355
- yield parseJson(transport, data, schema, answer.status);
364
+ const event = readEvent(transport, data, response, answer.status);
365
+ if (event.skipped) {
366
+ request.onSkipped?.(event.value);
367
+ continue;
368
+ }
369
+ yield event.value;
356
370
  }
357
371
  } catch (error) {
358
372
  throw asCallFailure(error, request);
@@ -368,6 +382,22 @@ async function readBody(read, request) {
368
382
  function asCallFailure(error, request) {
369
383
  return error instanceof ApiError || request.signal?.aborted ? error : transportError(error);
370
384
  }
385
+ function readEvent(transport, text, response, status) {
386
+ const { skip } = response;
387
+ if (skip) {
388
+ let value;
389
+ try {
390
+ value = JSON.parse(text);
391
+ } catch (error) {
392
+ throw unreadableResponseError(status, error);
393
+ }
394
+ const parsed = response.schema.safeParse(value);
395
+ if (parsed.success) return { skipped: false, value: parsed.data };
396
+ const skipped = skip.safeParse(value);
397
+ if (skipped.success) return { skipped: true, value: skipped.data };
398
+ }
399
+ return { skipped: false, value: parseJson(transport, text, response.schema, status) };
400
+ }
371
401
  function parseJson(transport, text, schema, status) {
372
402
  let value;
373
403
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@work-shape/catalyst-api-client",
3
- "version": "0.8.0-rc.2",
3
+ "version": "0.8.0-rc.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "zod": "4.4.3",
32
- "@work-shape/catalyst-api-contract": "0.8.0-rc.2"
32
+ "@work-shape/catalyst-api-contract": "0.8.0-rc.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^24.13.1",