@requence/service 0.8.3-edge.1190 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # @requence/service
2
2
 
3
+ ## 0.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 621b48b: A callback that carries no data is now called with no argument:
8
+
9
+ ```ts
10
+ await context.input.mail.delete();
11
+ ```
12
+
13
+ The `null` it used to need was never the caller's to write. It is how the
14
+ **output schema** spells "this position carries nothing": `RequenceCallback<void>`
15
+ compiles to nothing typescript-json-schema has a root type for, and
16
+ `RequenceCallback<undefined>` compiles but silently drops the `callback` marker
17
+ along with the argument — so `RequenceCallback<null>` is the only declaration
18
+ available, and every call site paid for that. A callback that does carry data is
19
+ untouched, and a forgotten argument is still an error.
20
+
21
+ `RequenceCallback<T>` makes the argument optional exactly when `null extends T`,
22
+ through a conditional **tuple**: a conditional on the parameter type alone cannot
23
+ change the ARITY, and the arity is the whole point. Writing the `null` out still
24
+ works and still means the same thing.
25
+
26
+ Optionality at the call site is not enough by itself. The operator validates
27
+ every invocation against the callback's declared argument schema, which for this
28
+ kind of callback is `{ type: 'null' }` — so both reconstruction sites, the
29
+ `deserialize` in `@requence/callbacks` and its Python mirror, default an omitted
30
+ argument to `null` rather than letting `undefined` reach the wire. That default
31
+ is unconditional: a boundary holds a reference, never the emitting node's output
32
+ schema, and a callback that DOES declare data fails the same validation either
33
+ way — turning `undefined` into `null` there only changes which of the two it is
34
+ refused as.
35
+
36
+ The declaration is written out four times — `@requence/helpers` for both SDKs,
37
+ `RequenceTypes.ts` for the logic-node editor, and the two assignability
38
+ harnesses. The fifth copy, in `createJsonSchema.ts`, is deliberately an object
39
+ shape with no call signature and does not move with them. The editor's copy is
40
+ now asserted through the real language service, since it is the one that decides
41
+ whether a script compiles.
42
+
43
+ - f71e500: A durable callback that could not be run is now reported on the node that emitted
44
+ it, instead of only in one service process's log.
45
+
46
+ Two things can go wrong once an invocation has been consumed: the deployed version
47
+ no longer declares the name, or the handler throws. Both were logged by the service
48
+ and acked — a requeue would rerun a side-effecting callback forever — and that log
49
+ is nowhere a template author looks. Combined with imapflow's `messageDelete`
50
+ returning `true` when the _server_ accepted the command rather than when it matched
51
+ a mail, a failed `mail.delete()` was indistinguishable from a delete that did
52
+ nothing. That silence is what made the stale computed-configuration bug beside it
53
+ cost an afternoon.
54
+
55
+ The counterpart already existed for an invocation **nobody consumed**
56
+ (`DURABLE_CALLBACK_INVOCATION_HEADER`, reported by the operator off the returned
57
+ copy). This is the other half, and only the consuming instance can see it, so both
58
+ SDKs publish it: `DURABLE_CALLBACK_ERROR_HEADER`, carrying the emitting node, the
59
+ declaration name and the error's **message** — not its stack, which belongs in the
60
+ service's log — cut at 500 characters at the source as well as in the update
61
+ schema.
62
+
63
+ It rides the ordinary result exchange with **no correlation id and no index**,
64
+ exactly like a surface interaction failure and a resume announcement: the handler
65
+ ran for no dispatch, so it has no message to be a result FOR, and every other arm
66
+ of the result path would have failed the emitting node for a callback the graph
67
+ chose to call.
68
+
69
+ The operator turns it into a `NODE_DEBUG` on that node — `severity: 'system'`
70
+ because the operator is speaking, `event: null` because nothing was re-sent and no
71
+ run ended. The node lookup, the "nowhere to report" arm and the update's shape are
72
+ shared with the undelivered path through one `reportCallbackOnNode`, since only the
73
+ sentence differs. Every failure of that lookup stays "nowhere to report": a run can
74
+ end while an invocation of it is still in flight, and a throw escaping there nacks a
75
+ delivery at the head of a single-active-consumer FIFO queue, where it wedges every
76
+ task on that shard.
77
+
78
+ **This makes a failure visible, not catchable.** The graph called the callback and
79
+ moved on long before the handler ran, so there is no call site left to throw at. A
80
+ callback is still `(arg) => void` in the call direction.
81
+
3
82
  ## 0.8.2
4
83
 
5
84
  ### Patch Changes
package/build/index.js CHANGED
@@ -205,8 +205,9 @@ function isCallbackObject(data) {
205
205
  function createCallback(callback) {
206
206
  const { promise, resolve: resolve2 } = Promise.withResolvers();
207
207
  const executor = (arg) => {
208
- resolve2(arg);
209
- callback?.(arg);
208
+ const value = arg === undefined ? null : arg;
209
+ resolve2(value);
210
+ callback?.(value);
210
211
  };
211
212
  return Object.assign(executor, {
212
213
  response() {
@@ -251,6 +252,13 @@ var durableCallbackInvocationSchema = z5.object({
251
252
  arg: z5.unknown().optional(),
252
253
  configuration: z5.unknown().optional()
253
254
  });
255
+ var MAX_DURABLE_CALLBACK_ERROR_CHARS = 500;
256
+ var DURABLE_CALLBACK_ERROR_HEADER = "durableCallbackError";
257
+ var durableCallbackErrorReportSchema = z5.object({
258
+ nodeId: z5.string(),
259
+ name: z5.string(),
260
+ message: z5.string()
261
+ });
254
262
 
255
263
  // ../helpers/src/utils/serviceRender.ts
256
264
  import { z as z6 } from "zod/v4";
@@ -1084,7 +1092,7 @@ function createRequenceCallbackHandler() {
1084
1092
  },
1085
1093
  isReference: (value) => isCallbackObject(value),
1086
1094
  deserialize(ref, ctx) {
1087
- return (arg) => {
1095
+ return (arg = null) => {
1088
1096
  ctx.onCallbackInvoke?.({ ...ref, arg });
1089
1097
  };
1090
1098
  }
@@ -2000,7 +2008,7 @@ function createConnection({
2000
2008
  clientProperties: {
2001
2009
  ...connectionOptions?.clientProperties,
2002
2010
  ...buildServiceClientProperties({
2003
- libVersion: "javascript@0.8.3-edge.1190",
2011
+ libVersion: "javascript@0.8.3",
2004
2012
  serviceVersion: version,
2005
2013
  connectionId,
2006
2014
  prefetch,
@@ -2142,6 +2150,28 @@ function createConnection({
2142
2150
  simple_default.error(`Requence: could not report a failed surface interaction on |${params.surface}|:`, error instanceof Error ? error.message : error);
2143
2151
  });
2144
2152
  };
2153
+ const reportCallbackFailure = (payload, error) => {
2154
+ const parsed = durableCallbackInvocationSchema.safeParse(payload);
2155
+ if (!parsed.success) {
2156
+ simple_default.error("Requence: cannot report a failed durable callback — the invocation names no task and node");
2157
+ return;
2158
+ }
2159
+ const { taskId, nodeId, name: name2 } = parsed.data;
2160
+ channelWrapper.publish(exchange, "", Buffer.from(JSON.stringify({
2161
+ nodeId,
2162
+ name: name2,
2163
+ message: (error instanceof Error ? error.message : String(error)).slice(0, MAX_DURABLE_CALLBACK_ERROR_CHARS)
2164
+ })), {
2165
+ contentType: "application/json",
2166
+ persistent: true,
2167
+ headers: {
2168
+ taskId,
2169
+ [DURABLE_CALLBACK_ERROR_HEADER]: true
2170
+ }
2171
+ }).catch((publishError) => {
2172
+ simple_default.error(`Requence: could not report the failed durable callback |${name2}| of task |${taskId}|:`, publishError instanceof Error ? publishError.message : publishError);
2173
+ });
2174
+ };
2145
2175
  const announceResume = (params) => {
2146
2176
  channelWrapper.publish(exchange, "", Buffer.from(JSON.stringify({ messageId: params.messageId })), {
2147
2177
  correlationId: params.messageId,
@@ -2252,10 +2282,13 @@ function createConnection({
2252
2282
  if (!message) {
2253
2283
  return;
2254
2284
  }
2285
+ let payload;
2255
2286
  try {
2256
- await runDurableCallbackInvocation(JSON.parse(message.content.toString()));
2287
+ payload = JSON.parse(message.content.toString());
2288
+ await runDurableCallbackInvocation(payload);
2257
2289
  } catch (error) {
2258
2290
  simple_default.error("Requence: durable callback threw:", error instanceof Error ? error.stack ?? error.message : error);
2291
+ reportCallbackFailure(payload, error);
2259
2292
  }
2260
2293
  safeAck(channel, message);
2261
2294
  });
@@ -2536,7 +2569,7 @@ function createConnection({
2536
2569
  begin: true,
2537
2570
  epoch,
2538
2571
  ...buildServiceIdentityHeaders({
2539
- libVersion: "javascript@0.8.3-edge.1190",
2572
+ libVersion: "javascript@0.8.3",
2540
2573
  name,
2541
2574
  holdsSlot: !(isGeneratorHandler && mode === "CONTINUOUS")
2542
2575
  })
@@ -3107,5 +3140,5 @@ export {
3107
3140
  PayloadTooLargeError
3108
3141
  };
3109
3142
 
3110
- //# debugId=29AC99BF6424ED2964756E2164756E21
3143
+ //# debugId=A75FE0293DB162D764756E2164756E21
3111
3144
  //# sourceMappingURL=index.js.map