@lostgradient/weft 0.2.1 → 0.3.0

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.
Files changed (188) hide show
  1. package/README.md +47 -22
  2. package/dist/cli/generated/operation-client.generated.d.ts +28 -1
  3. package/dist/cli/generated/operation-client.generated.js +2 -0
  4. package/dist/cli-main.js +79 -79
  5. package/dist/client/handle-delegation.d.ts +4 -0
  6. package/dist/client/handle-delegation.js +6 -0
  7. package/dist/client/http-client-requests.d.ts +2 -0
  8. package/dist/client/http-client-requests.js +3 -0
  9. package/dist/client/http-client.d.ts +4 -1
  10. package/dist/client/http-client.js +9 -1
  11. package/dist/client/interface.d.ts +57 -2
  12. package/dist/client/local.d.ts +4 -1
  13. package/dist/client/local.js +7 -0
  14. package/dist/client/start-body.d.ts +7 -1
  15. package/dist/client/start-body.js +13 -4
  16. package/dist/core/codec/extension-codec.js +4 -2
  17. package/dist/core/codec/index.d.ts +1 -0
  18. package/dist/core/codec/index.js +1 -0
  19. package/dist/core/codec/serializer-registry.d.ts +122 -0
  20. package/dist/core/codec/serializer-registry.js +51 -0
  21. package/dist/core/context/index.d.ts +9 -0
  22. package/dist/core/context/internals.d.ts +9 -0
  23. package/dist/core/context/internals.js +3 -0
  24. package/dist/core/context/run-operation.d.ts +16 -3
  25. package/dist/core/context/run-operation.js +16 -7
  26. package/dist/core/engine/bulk-operations.js +1 -1
  27. package/dist/core/engine/construction.d.ts +0 -1
  28. package/dist/core/engine/construction.js +10 -1
  29. package/dist/core/engine/disposal.js +12 -0
  30. package/dist/core/engine/engine-create-types.d.ts +0 -14
  31. package/dist/core/engine/engine-internal-types.d.ts +12 -0
  32. package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
  33. package/dist/core/engine/engine-leak-warnings.js +4 -0
  34. package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
  35. package/dist/core/engine/engine-runtime-helpers.js +26 -5
  36. package/dist/core/engine/errors.d.ts +74 -0
  37. package/dist/core/engine/errors.js +25 -1
  38. package/dist/core/engine/handle-result.js +1 -1
  39. package/dist/core/engine/handles.d.ts +89 -40
  40. package/dist/core/engine/handles.js +25 -27
  41. package/dist/core/engine/index.d.ts +96 -4
  42. package/dist/core/engine/index.js +75 -4
  43. package/dist/core/engine/inline-launch-queue.d.ts +14 -0
  44. package/dist/core/engine/inline-launch-queue.js +32 -7
  45. package/dist/core/engine/internals.d.ts +18 -10
  46. package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
  47. package/dist/core/engine/lifecycle/persist.js +5 -20
  48. package/dist/core/engine/lifecycle/resume.js +25 -4
  49. package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
  50. package/dist/core/engine/lifecycle/start-commit.js +27 -0
  51. package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
  52. package/dist/core/engine/lifecycle/start-exec.js +38 -0
  53. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
  54. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
  55. package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
  56. package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
  57. package/dist/core/engine/lifecycle/start.d.ts +3 -3
  58. package/dist/core/engine/lifecycle/start.js +31 -37
  59. package/dist/core/engine/lifecycle.d.ts +3 -2
  60. package/dist/core/engine/lifecycle.js +9 -2
  61. package/dist/core/engine/listing.js +1 -1
  62. package/dist/core/engine/persisted-data-version.d.ts +5 -9
  63. package/dist/core/engine/persisted-data-version.js +4 -5
  64. package/dist/core/engine/schedule-handle.d.ts +45 -0
  65. package/dist/core/engine/schedule-handle.js +26 -0
  66. package/dist/core/engine/schedules.d.ts +1 -1
  67. package/dist/core/engine/schedules.js +7 -3
  68. package/dist/core/engine/second-instance-detector.d.ts +96 -0
  69. package/dist/core/engine/second-instance-detector.js +108 -0
  70. package/dist/core/engine/signals.d.ts +22 -0
  71. package/dist/core/engine/signals.js +15 -0
  72. package/dist/core/engine/termination/cleanup.d.ts +25 -0
  73. package/dist/core/engine/termination/cleanup.js +19 -1
  74. package/dist/core/engine/termination/complete.js +4 -3
  75. package/dist/core/engine/termination/suspend.d.ts +68 -0
  76. package/dist/core/engine/termination/suspend.js +41 -0
  77. package/dist/core/engine/termination.d.ts +4 -2
  78. package/dist/core/engine/termination.js +2 -0
  79. package/dist/core/engine/validation.js +25 -1
  80. package/dist/core/engine/workflow-feed.d.ts +5 -3
  81. package/dist/core/events/event-map.d.ts +2 -1
  82. package/dist/core/events/workflow-events.d.ts +23 -0
  83. package/dist/core/events/workflow-events.js +9 -0
  84. package/dist/core/list-filter-validation.js +2 -1
  85. package/dist/core/start-workflow-validation.d.ts +22 -0
  86. package/dist/core/start-workflow-validation.js +11 -1
  87. package/dist/core/step-context.d.ts +10 -6
  88. package/dist/core/step-context.js +7 -15
  89. package/dist/core/types/activity.d.ts +6 -3
  90. package/dist/core/types/identity.d.ts +8 -1
  91. package/dist/core/types/launch-metadata.d.ts +33 -0
  92. package/dist/core/types/launch-metadata.js +0 -0
  93. package/dist/core/types/message-handles.d.ts +25 -0
  94. package/dist/core/types/options.d.ts +48 -54
  95. package/dist/core/types/reviews.d.ts +2 -1
  96. package/dist/core/types/services-resolution.d.ts +47 -0
  97. package/dist/core/types/services-resolution.js +0 -0
  98. package/dist/core/types/state.d.ts +11 -11
  99. package/dist/core/types/workflow-builder.d.ts +5 -4
  100. package/dist/core/types/workflow-function.d.ts +17 -0
  101. package/dist/core/types/workflow-snapshot.d.ts +29 -0
  102. package/dist/core/types/workflow-snapshot.js +0 -0
  103. package/dist/core/types.d.ts +3 -0
  104. package/dist/core/types.js +3 -0
  105. package/dist/core/weft-error.d.ts +1 -1
  106. package/dist/core/weft-error.js +3 -1
  107. package/dist/diagnostics/doctor.js +6 -3
  108. package/dist/diagnostics/format.js +2 -2
  109. package/dist/diagnostics/types.d.ts +1 -0
  110. package/dist/diagnostics/version-check.js +6 -4
  111. package/dist/index.d.ts +4 -4
  112. package/dist/index.js +10 -1
  113. package/dist/json-schema.js +1 -1
  114. package/dist/mcp/cli.js +35 -35
  115. package/dist/mcp/list-filter.js +2 -1
  116. package/dist/mcp/session.js +1 -0
  117. package/dist/observability/index.js +2 -2
  118. package/dist/server/handler.js +30 -30
  119. package/dist/server/index.js +33 -33
  120. package/dist/server/interactive-operations.js +1 -0
  121. package/dist/server/operations/resume-workflow.js +2 -2
  122. package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
  123. package/dist/server/operations/start-or-signal-workflow.js +140 -0
  124. package/dist/server/operations/start-workflow-options.d.ts +32 -0
  125. package/dist/server/operations/start-workflow-options.js +63 -0
  126. package/dist/server/operations/start-workflow.js +7 -69
  127. package/dist/server/operations/suspend-workflow.d.ts +13 -0
  128. package/dist/server/operations/suspend-workflow.js +36 -0
  129. package/dist/server/rest-binding.d.ts +18 -7
  130. package/dist/server/rest-bindings.js +12 -0
  131. package/dist/server/runtime/task-dispatch.js +5 -3
  132. package/dist/server/runtime/task-polling.d.ts +16 -2
  133. package/dist/server/runtime/task-polling.js +20 -5
  134. package/dist/server/runtime/websocket-worker.js +8 -0
  135. package/dist/server/serve-internals.d.ts +8 -0
  136. package/dist/server/serve-internals.js +4 -2
  137. package/dist/server/task-state.d.ts +8 -0
  138. package/dist/service-worker/index.js +28 -28
  139. package/dist/storage/capabilities.d.ts +10 -2
  140. package/dist/storage/capabilities.js +2 -2
  141. package/dist/storage/http.js +2 -2
  142. package/dist/storage/index.d.ts +6 -1
  143. package/dist/storage/indexeddb.js +1 -1
  144. package/dist/storage/interface.d.ts +26 -0
  145. package/dist/storage/interface.js +1 -1
  146. package/dist/storage/key-prefixes.d.ts +1 -1
  147. package/dist/storage/key-prefixes.js +2 -0
  148. package/dist/storage/lmdb.js +1 -1
  149. package/dist/storage/memory.js +1 -1
  150. package/dist/storage/neon-value-mapping.d.ts +47 -0
  151. package/dist/storage/neon-value-mapping.js +11 -0
  152. package/dist/storage/neon.d.ts +108 -0
  153. package/dist/storage/neon.js +10 -0
  154. package/dist/storage/node-sqlite-loader.d.ts +71 -0
  155. package/dist/storage/node-sqlite-loader.js +41 -0
  156. package/dist/storage/node-sqlite.d.ts +1 -19
  157. package/dist/storage/node-sqlite.js +38 -32
  158. package/dist/storage/postgres-key-value-queries.d.ts +79 -0
  159. package/dist/storage/postgres-key-value-queries.js +63 -0
  160. package/dist/storage/resolve.d.ts +2 -165
  161. package/dist/storage/resolve.js +1 -1
  162. package/dist/storage/scoped-storage.js +1 -1
  163. package/dist/storage/storage-configuration.d.ts +209 -0
  164. package/dist/storage/storage-configuration.js +0 -0
  165. package/dist/storage/text-value-store.d.ts +9 -9
  166. package/dist/storage/turso.js +2 -2
  167. package/dist/storage/typed-storage.js +1 -1
  168. package/dist/storage/web-extension.js +1 -1
  169. package/dist/testing/index.js +33 -33
  170. package/dist/version.d.ts +1 -1
  171. package/dist/version.js +1 -1
  172. package/dist/worker/index.js +9 -5
  173. package/dist/worker/long-poll.js +4 -0
  174. package/dist/worker/protocol-messages.d.ts +20 -0
  175. package/dist/worker/protocol-schemas.d.ts +32 -0
  176. package/dist/worker/protocol-schemas.js +8 -4
  177. package/dist/worker/protocol-task-result.d.ts +28 -0
  178. package/dist/worker/protocol-task-result.js +76 -0
  179. package/dist/worker/protocol.d.ts +4 -15
  180. package/dist/worker/protocol.js +1 -1
  181. package/dist/worker/registry/fair-share.d.ts +29 -0
  182. package/dist/worker/registry/fair-share.js +30 -0
  183. package/dist/worker/registry/routing.d.ts +18 -0
  184. package/dist/worker/registry/routing.js +14 -0
  185. package/dist/worker/registry/types.d.ts +7 -0
  186. package/dist/worker/registry.d.ts +16 -1
  187. package/dist/worker/registry.js +24 -36
  188. package/package.json +17 -4
package/dist/version.d.ts CHANGED
@@ -9,4 +9,4 @@
9
9
  * console.log(`Running Weft ${VERSION}`);
10
10
  * ```
11
11
  */
12
- export declare const VERSION = "0.2.1";
12
+ export declare const VERSION = "0.3.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.2.1";
1
+ export const VERSION = "0.3.0";
@@ -225,13 +225,14 @@ export class RemoteWorker {
225
225
  this.#failSocket();
226
226
  return;
227
227
  }
228
- const activityFunction = this.#activityTable[task.activityName];
228
+ const tokenEcho = task.attemptToken !== void 0 ? { attemptToken: task.attemptToken } : {}, activityFunction = this.#activityTable[task.activityName];
229
229
  if (activityFunction === void 0) {
230
230
  this.#sendTaskResult({
231
231
  type: "taskResult",
232
232
  operationId: task.operationId,
233
233
  status: "failed",
234
- error: `Unknown activity: ${task.activityName}`
234
+ error: `Unknown activity: ${task.activityName}`,
235
+ ...tokenEcho
235
236
  });
236
237
  return;
237
238
  }
@@ -244,7 +245,8 @@ export class RemoteWorker {
244
245
  type: "taskResult",
245
246
  operationId: task.operationId,
246
247
  status: "completed",
247
- value: normalizeWorkerJsonValue(result)
248
+ value: normalizeWorkerJsonValue(result),
249
+ ...tokenEcho
248
250
  });
249
251
  } catch (error) {
250
252
  if (taskAbortController.signal.aborted)
@@ -253,14 +255,16 @@ export class RemoteWorker {
253
255
  operationId: task.operationId,
254
256
  status: "cancelled",
255
257
  cancelled: !0,
256
- error: "Task cancelled"
258
+ error: "Task cancelled",
259
+ ...tokenEcho
257
260
  });
258
261
  else
259
262
  this.#sendTaskResult({
260
263
  type: "taskResult",
261
264
  operationId: task.operationId,
262
265
  status: "failed",
263
- error: error instanceof Error ? error.message : String(error)
266
+ error: error instanceof Error ? error.message : String(error),
267
+ ...tokenEcho
264
268
  });
265
269
  } finally {
266
270
  this.#taskAbortControllers.delete(task.operationId);
@@ -85,6 +85,7 @@ export class LongPollWorker {
85
85
  }
86
86
  async#executeTask(task, resultUrl) {
87
87
  this.#inFlight += 1;
88
+ const tokenEcho = task.attemptToken !== void 0 ? { attemptToken: task.attemptToken } : {};
88
89
  try {
89
90
  const activityFunction = this.#options.activities[task.activityName];
90
91
  if (activityFunction === void 0) {
@@ -94,6 +95,7 @@ export class LongPollWorker {
94
95
  body: JSON.stringify({
95
96
  operationId: task.operationId,
96
97
  workerId: task.workerId,
98
+ ...tokenEcho,
97
99
  status: "failed",
98
100
  error: `Unknown activity: ${task.activityName}`
99
101
  }),
@@ -108,6 +110,7 @@ export class LongPollWorker {
108
110
  body: JSON.stringify({
109
111
  operationId: task.operationId,
110
112
  workerId: task.workerId,
113
+ ...tokenEcho,
111
114
  status: "completed",
112
115
  value: result
113
116
  }),
@@ -121,6 +124,7 @@ export class LongPollWorker {
121
124
  body: JSON.stringify({
122
125
  operationId: task.operationId,
123
126
  workerId: task.workerId,
127
+ ...tokenEcho,
124
128
  status: "failed",
125
129
  error: error instanceof Error ? error.message : String(error)
126
130
  }),
@@ -100,6 +100,12 @@ export type CompletedTaskResultMessage = {
100
100
  readonly operationId: string;
101
101
  readonly status: 'completed';
102
102
  readonly value: RemoteWorkerJsonValue;
103
+ /**
104
+ * The per-dispatch token echoed from the {@link TaskMessage}. Optional on the
105
+ * wire so older workers still parse, but the server's completion handler
106
+ * rejects a result whose token does not match the current attempt.
107
+ */
108
+ readonly attemptToken?: string;
103
109
  };
104
110
  /**
105
111
  * Failed activity result message.
@@ -121,6 +127,8 @@ export type FailedTaskResultMessage = {
121
127
  readonly operationId: string;
122
128
  readonly status: 'failed';
123
129
  readonly error: string;
130
+ /** The per-dispatch token echoed from the {@link TaskMessage}. See {@link CompletedTaskResultMessage.attemptToken}. */
131
+ readonly attemptToken?: string;
124
132
  };
125
133
  /**
126
134
  * Cancelled activity result message.
@@ -144,6 +152,8 @@ export type CancelledTaskResultMessage = {
144
152
  readonly status: 'cancelled';
145
153
  readonly error: string;
146
154
  readonly cancelled?: true;
155
+ /** The per-dispatch token echoed from the {@link TaskMessage}. See {@link CompletedTaskResultMessage.attemptToken}. */
156
+ readonly attemptToken?: string;
147
157
  };
148
158
  /**
149
159
  * Registration acknowledgement sent after a worker is accepted.
@@ -224,6 +234,7 @@ export type ProtocolErrorMessage = {
224
234
  * operationId: 'op-1',
225
235
  * activityName: 'sendEmail',
226
236
  * input: { to: 'user@example.com' },
237
+ * attemptToken: '550e8400-e29b-41d4-a716-446655440000',
227
238
  * };
228
239
  * ```
229
240
  */
@@ -234,6 +245,15 @@ export type TaskMessage = {
234
245
  readonly input: RemoteWorkerJsonValue;
235
246
  readonly attempt?: number;
236
247
  readonly headers?: Readonly<Record<string, string>>;
248
+ /**
249
+ * Unique, unguessable token identifying this specific dispatch attempt. The
250
+ * current server always stamps one, and the worker must echo it back on the
251
+ * {@link CompletedTaskResultMessage} (or failed/cancelled variant) so the server
252
+ * can reject a stale completion from an earlier attempt that was reassigned to
253
+ * the same worker. Optional on the type because a frame from an older server may
254
+ * omit it; the worker simply has no token to echo in that case.
255
+ */
256
+ readonly attemptToken?: string;
237
257
  };
238
258
  /**
239
259
  * Activity cancellation request sent by the server.
@@ -105,6 +105,10 @@ export declare const REMOTE_WORKER_MESSAGE_SCHEMAS: {
105
105
  readonly const: "completed";
106
106
  };
107
107
  readonly value: JsonSchemaObject;
108
+ readonly attemptToken: {
109
+ readonly type: "string";
110
+ readonly minLength: 1;
111
+ };
108
112
  };
109
113
  }, {
110
114
  readonly type: "object";
@@ -124,6 +128,10 @@ export declare const REMOTE_WORKER_MESSAGE_SCHEMAS: {
124
128
  readonly error: {
125
129
  readonly type: "string";
126
130
  };
131
+ readonly attemptToken: {
132
+ readonly type: "string";
133
+ readonly minLength: 1;
134
+ };
127
135
  };
128
136
  }, {
129
137
  readonly type: "object";
@@ -146,6 +154,10 @@ export declare const REMOTE_WORKER_MESSAGE_SCHEMAS: {
146
154
  readonly cancelled: {
147
155
  readonly const: true;
148
156
  };
157
+ readonly attemptToken: {
158
+ readonly type: "string";
159
+ readonly minLength: 1;
160
+ };
149
161
  };
150
162
  }];
151
163
  };
@@ -171,6 +183,10 @@ export declare const REMOTE_WORKER_MESSAGE_SCHEMAS: {
171
183
  readonly minimum: 1;
172
184
  };
173
185
  readonly headers: JsonSchemaObject;
186
+ readonly attemptToken: {
187
+ readonly type: "string";
188
+ readonly minLength: 1;
189
+ };
174
190
  };
175
191
  };
176
192
  readonly cancel: {
@@ -391,6 +407,10 @@ export declare const REMOTE_WORKER_PROTOCOL_JSON_SCHEMA: {
391
407
  readonly const: "completed";
392
408
  };
393
409
  readonly value: JsonSchemaObject;
410
+ readonly attemptToken: {
411
+ readonly type: "string";
412
+ readonly minLength: 1;
413
+ };
394
414
  };
395
415
  }, {
396
416
  readonly type: "object";
@@ -410,6 +430,10 @@ export declare const REMOTE_WORKER_PROTOCOL_JSON_SCHEMA: {
410
430
  readonly error: {
411
431
  readonly type: "string";
412
432
  };
433
+ readonly attemptToken: {
434
+ readonly type: "string";
435
+ readonly minLength: 1;
436
+ };
413
437
  };
414
438
  }, {
415
439
  readonly type: "object";
@@ -432,6 +456,10 @@ export declare const REMOTE_WORKER_PROTOCOL_JSON_SCHEMA: {
432
456
  readonly cancelled: {
433
457
  readonly const: true;
434
458
  };
459
+ readonly attemptToken: {
460
+ readonly type: "string";
461
+ readonly minLength: 1;
462
+ };
435
463
  };
436
464
  }];
437
465
  };
@@ -457,6 +485,10 @@ export declare const REMOTE_WORKER_PROTOCOL_JSON_SCHEMA: {
457
485
  readonly minimum: 1;
458
486
  };
459
487
  readonly headers: JsonSchemaObject;
488
+ readonly attemptToken: {
489
+ readonly type: "string";
490
+ readonly minLength: 1;
491
+ };
460
492
  };
461
493
  };
462
494
  readonly cancel: {
@@ -52,7 +52,8 @@ export const REMOTE_WORKER_MESSAGE_SCHEMAS = {
52
52
  type: { const: "taskResult" },
53
53
  operationId: { type: "string", minLength: 1 },
54
54
  status: { const: "completed" },
55
- value: jsonValueSchema
55
+ value: jsonValueSchema,
56
+ attemptToken: { type: "string", minLength: 1 }
56
57
  }
57
58
  },
58
59
  {
@@ -63,7 +64,8 @@ export const REMOTE_WORKER_MESSAGE_SCHEMAS = {
63
64
  type: { const: "taskResult" },
64
65
  operationId: { type: "string", minLength: 1 },
65
66
  status: { const: "failed" },
66
- error: { type: "string" }
67
+ error: { type: "string" },
68
+ attemptToken: { type: "string", minLength: 1 }
67
69
  }
68
70
  },
69
71
  {
@@ -75,7 +77,8 @@ export const REMOTE_WORKER_MESSAGE_SCHEMAS = {
75
77
  operationId: { type: "string", minLength: 1 },
76
78
  status: { const: "cancelled" },
77
79
  error: { type: "string" },
78
- cancelled: { const: !0 }
80
+ cancelled: { const: !0 },
81
+ attemptToken: { type: "string", minLength: 1 }
79
82
  }
80
83
  }
81
84
  ]
@@ -90,7 +93,8 @@ export const REMOTE_WORKER_MESSAGE_SCHEMAS = {
90
93
  activityName: { type: "string", minLength: 1 },
91
94
  input: jsonValueSchema,
92
95
  attempt: { type: "number", minimum: 1 },
93
- headers: stringMapSchema
96
+ headers: stringMapSchema,
97
+ attemptToken: { type: "string", minLength: 1 }
94
98
  }
95
99
  },
96
100
  cancel: {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Parser for the worker-to-server `taskResult` message.
3
+ *
4
+ * `taskResult` is a discriminated union over status `completed | failed |
5
+ * cancelled`. Variant dispatch uses a `satisfies Record<TaskResultStatus, …>`
6
+ * lookup so adding a new variant becomes a compile-time error. This cluster is
7
+ * split out of `./protocol.ts` so the canonical parser module stays focused on
8
+ * the top-level schema-to-guard dispatch a reviewer audits.
9
+ *
10
+ * @module worker/protocol-task-result
11
+ */
12
+ import type { RemoteWorkerProtocolParseResult } from './protocol-internals.ts';
13
+ import type { CancelledTaskResultMessage, CompletedTaskResultMessage, FailedTaskResultMessage } from './protocol-messages.ts';
14
+ /**
15
+ * Worker-to-server task result message. Discriminated union over `status`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import type { TaskResultMessage } from '@lostgradient/weft/worker-protocol';
20
+ *
21
+ * const message: TaskResultMessage = {
22
+ * type: 'taskResult', operationId: 'op-1', status: 'completed', value: { ok: true },
23
+ * };
24
+ * ```
25
+ */
26
+ export type TaskResultMessage = CompletedTaskResultMessage | FailedTaskResultMessage | CancelledTaskResultMessage;
27
+ /** Parse and validate a worker-to-server `taskResult` message into its typed variant. */
28
+ export declare function parseTaskResultMessage(record: Record<string, unknown>): RemoteWorkerProtocolParseResult<TaskResultMessage>;
@@ -0,0 +1,76 @@
1
+ import {
2
+ isNonEmptyString,
3
+ isRemoteWorkerJsonValue,
4
+ protocolFailure
5
+ } from "./protocol-internals.js";
6
+ function parseEchoedAttemptToken(record) {
7
+ const attemptToken = record.attemptToken;
8
+ if (attemptToken === void 0)
9
+ return { ok: !0, message: {} };
10
+ if (!isNonEmptyString(attemptToken))
11
+ return protocolFailure("invalid_message", "taskResult.attemptToken must be a non-empty string when present");
12
+ return { ok: !0, message: { attemptToken } };
13
+ }
14
+ function parseCompletedTaskResult(operationId, record) {
15
+ const value = record.value;
16
+ if (!isRemoteWorkerJsonValue(value))
17
+ return protocolFailure("invalid_message", "completed taskResult.value must be valid JSON");
18
+ const token = parseEchoedAttemptToken(record);
19
+ if (!token.ok)
20
+ return token;
21
+ return {
22
+ ok: !0,
23
+ message: { type: "taskResult", operationId, status: "completed", value, ...token.message }
24
+ };
25
+ }
26
+ function parseFailedTaskResult(operationId, record) {
27
+ const error = record.error;
28
+ if (typeof error !== "string")
29
+ return protocolFailure("invalid_message", "failed taskResult.error must be a string");
30
+ const token = parseEchoedAttemptToken(record);
31
+ if (!token.ok)
32
+ return token;
33
+ return {
34
+ ok: !0,
35
+ message: { type: "taskResult", operationId, status: "failed", error, ...token.message }
36
+ };
37
+ }
38
+ function parseCancelledTaskResult(operationId, record) {
39
+ const error = record.error;
40
+ if (typeof error !== "string")
41
+ return protocolFailure("invalid_message", "cancelled taskResult.error must be a string");
42
+ const cancelled = record.cancelled;
43
+ if (cancelled !== void 0 && cancelled !== !0)
44
+ return protocolFailure("invalid_message", "taskResult.cancelled must be true when present");
45
+ const token = parseEchoedAttemptToken(record);
46
+ if (!token.ok)
47
+ return token;
48
+ return {
49
+ ok: !0,
50
+ message: {
51
+ type: "taskResult",
52
+ operationId,
53
+ status: "cancelled",
54
+ error,
55
+ ...cancelled === !0 ? { cancelled } : {},
56
+ ...token.message
57
+ }
58
+ };
59
+ }
60
+ const TASK_RESULT_VARIANT_PARSERS = {
61
+ completed: parseCompletedTaskResult,
62
+ failed: parseFailedTaskResult,
63
+ cancelled: parseCancelledTaskResult
64
+ };
65
+ function isTaskResultStatus(value) {
66
+ return value === "completed" || value === "failed" || value === "cancelled";
67
+ }
68
+ export function parseTaskResultMessage(record) {
69
+ const operationId = record.operationId;
70
+ if (!isNonEmptyString(operationId))
71
+ return protocolFailure("invalid_message", "taskResult.operationId must be a non-empty string");
72
+ const status = record.status;
73
+ if (!isTaskResultStatus(status))
74
+ return protocolFailure("invalid_message", "taskResult.status must be completed, failed, or cancelled");
75
+ return TASK_RESULT_VARIANT_PARSERS[status](operationId, record);
76
+ }
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * This module owns the parser trust boundary: every guard below maps to one
5
5
  * documented schema field. Wire-shape types live in `./protocol-messages.ts`,
6
- * JSON Schema documents in `./protocol-schemas.ts`, and internal helpers in
6
+ * JSON Schema documents in `./protocol-schemas.ts`, the `taskResult` variant
7
+ * parser in `./protocol-task-result.ts`, and internal helpers in
7
8
  * `./protocol-internals.ts`. All are re-exported so the public surface
8
9
  * `@lostgradient/weft/worker-protocol` stays a single import path.
9
10
  *
@@ -13,23 +14,11 @@ import type { RemoteWorkerProtocolFailure, RemoteWorkerProtocolParseResult } fro
13
14
  import { isRemoteWorkerJsonValue } from './protocol-internals.ts';
14
15
  import type { CancelMessage, CancelledTaskResultMessage, CompletedTaskResultMessage, FailedTaskResultMessage, HeartbeatMessage, ProtocolErrorMessage, RegisterAckMessage, RegisterErrorMessage, RegisterMessage, RemoteWorkerCapabilities, RemoteWorkerJsonValue, ShutdownMessage, TaskMessage } from './protocol-messages.ts';
15
16
  import { REMOTE_WORKER_MESSAGE_SCHEMAS, REMOTE_WORKER_PROTOCOL_JSON_SCHEMA } from './protocol-schemas.ts';
17
+ import type { TaskResultMessage } from './protocol-task-result.ts';
16
18
  import type { RemoteWorkerProtocolVersion } from './protocol-version.ts';
17
19
  import { REMOTE_WORKER_MAX_PROTOCOL_VERSION, REMOTE_WORKER_MIN_PROTOCOL_VERSION, REMOTE_WORKER_PROTOCOL_VERSION, REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS } from './protocol-version.ts';
18
20
  export { REMOTE_WORKER_MAX_PROTOCOL_VERSION, REMOTE_WORKER_MESSAGE_SCHEMAS, REMOTE_WORKER_MIN_PROTOCOL_VERSION, REMOTE_WORKER_PROTOCOL_JSON_SCHEMA, REMOTE_WORKER_PROTOCOL_VERSION, REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS, isRemoteWorkerJsonValue, };
19
- export type { CancelMessage, CancelledTaskResultMessage, CompletedTaskResultMessage, FailedTaskResultMessage, HeartbeatMessage, ProtocolErrorMessage, RegisterAckMessage, RegisterErrorMessage, RegisterMessage, RemoteWorkerCapabilities, RemoteWorkerJsonValue, RemoteWorkerProtocolFailure, RemoteWorkerProtocolParseResult, RemoteWorkerProtocolVersion, ShutdownMessage, TaskMessage, };
20
- /**
21
- * Worker-to-server task result message. Discriminated union over `status`.
22
- *
23
- * @example
24
- * ```ts
25
- * import type { TaskResultMessage } from '@lostgradient/weft/worker-protocol';
26
- *
27
- * const message: TaskResultMessage = {
28
- * type: 'taskResult', operationId: 'op-1', status: 'completed', value: { ok: true },
29
- * };
30
- * ```
31
- */
32
- export type TaskResultMessage = CompletedTaskResultMessage | FailedTaskResultMessage | CancelledTaskResultMessage;
21
+ export type { CancelMessage, CancelledTaskResultMessage, CompletedTaskResultMessage, FailedTaskResultMessage, HeartbeatMessage, ProtocolErrorMessage, RegisterAckMessage, RegisterErrorMessage, RegisterMessage, RemoteWorkerCapabilities, RemoteWorkerJsonValue, RemoteWorkerProtocolFailure, RemoteWorkerProtocolParseResult, RemoteWorkerProtocolVersion, ShutdownMessage, TaskMessage, TaskResultMessage, };
33
22
  /**
34
23
  * Messages accepted from a worker stream client.
35
24
  *
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var h=Object.defineProperty;var q=(z)=>z;function T(z,B){this[z]=q.bind(null,B)}var a=(z,B)=>{for(var D in B)h(z,D,{get:B[D],enumerable:!0,configurable:!0,set:T.bind(B,D)})};var o=(z,B)=>()=>(z&&(B=z(z=0)),B);function F(z){return z!==null&&typeof z==="object"&&!Array.isArray(z)}function x(z){if(z===null||typeof z==="string"||typeof z==="boolean"||typeof z==="number"&&Number.isFinite(z))return!0;if(Array.isArray(z))return z.every(x);if(!F(z))return!1;return Object.values(z).every(x)}function U(z){return typeof z==="string"&&z.length>0}function L(z){return Array.isArray(z)&&z.every(U)}function K(z){if(!F(z))return!1;return Object.values(z).every((B)=>typeof B==="string")}function f(z){if(!F(z))return!1;return Object.values(z).every(x)}function G(z){return typeof z==="number"&&Number.isFinite(z)}function Q(z,B,D){return{ok:!1,error:{code:z,message:B,...D!==void 0?{requestedProtocolVersion:D}:{}}}}function A(z,B,D){let X={};for(let[Y,$,b,C]of D){let H=B[Y];if(H===void 0){if($)return{ok:!1,error:Q(z,C)};continue}if(!b(H))return{ok:!1,error:Q(z,C)};X[Y]=H}return{ok:!0,values:X}}var Z=2,W=2,k=2,I=[2];var j={$ref:"#/$defs/jsonValue"},w={type:"object",additionalProperties:j},M={type:"object",additionalProperties:{type:"string"}},J={const:Z},N={register:{type:"object",additionalProperties:!1,required:["type","protocolVersion","workerId","activities"],properties:{type:{const:"register"},protocolVersion:J,workerId:{type:"string",minLength:1},activities:{type:"array",items:{type:"string",minLength:1}},concurrency:{type:"number",minimum:1,maximum:1000},queue:{type:"string",minLength:1},deploymentName:{type:"string",minLength:1},buildId:{type:"string",minLength:1},runtimeVersion:{type:"string",minLength:1},gitSha:{type:"string",minLength:1},startedAt:{type:"number"},capabilities:w}},heartbeat:{type:"object",additionalProperties:!1,required:["type","workerId"],properties:{type:{const:"heartbeat"},workerId:{type:"string",minLength:1}}},taskResult:{oneOf:[{type:"object",additionalProperties:!1,required:["type","operationId","status","value"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"completed"},value:j}},{type:"object",additionalProperties:!1,required:["type","operationId","status","error"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"failed"},error:{type:"string"}}},{type:"object",additionalProperties:!1,required:["type","operationId","status","error"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"cancelled"},error:{type:"string"},cancelled:{const:!0}}}]},task:{type:"object",additionalProperties:!1,required:["type","operationId","activityName","input"],properties:{type:{const:"task"},operationId:{type:"string",minLength:1},activityName:{type:"string",minLength:1},input:j,attempt:{type:"number",minimum:1},headers:M}},cancel:{type:"object",additionalProperties:!1,required:["type","operationId"],properties:{type:{const:"cancel"},operationId:{type:"string",minLength:1}}},shutdown:{type:"object",additionalProperties:!1,required:["type"],properties:{type:{const:"shutdown"}}},registerAck:{type:"object",additionalProperties:!1,required:["type","protocolVersion","workerId","queue","activities","concurrency"],properties:{type:{const:"registerAck"},protocolVersion:J,workerId:{type:"string",minLength:1},queue:{type:"string",minLength:1},activities:{type:"array",items:{type:"string",minLength:1}},concurrency:{type:"number",minimum:1,maximum:1000}}},registerError:{type:"object",additionalProperties:!1,required:["type","code","message","supportedProtocolVersions"],properties:{type:{const:"registerError"},code:{enum:["invalid_registration","unsupported_protocol_version"]},message:{type:"string"},supportedProtocolVersions:{type:"array",items:J},requestedProtocolVersion:{type:"number"}}},protocolError:{type:"object",additionalProperties:!1,required:["type","code","message"],properties:{type:{const:"protocolError"},code:{enum:["invalid_json","invalid_message","unknown_message_type","registration_required"]},message:{type:"string"}}}},P={$schema:"https://json-schema.org/draft/2020-12/schema",$id:"https://weft.dev/schemas/remote-worker-protocol.v1.json",title:"Weft RemoteWorker Protocol v1",oneOf:Object.keys(N).map((z)=>({$ref:`#/$defs/messages/${z}`})),$defs:{jsonValue:{oneOf:[{type:"null"},{type:"boolean"},{type:"number"},{type:"string"},{type:"array",items:{$ref:"#/$defs/jsonValue"}},{type:"object",additionalProperties:{$ref:"#/$defs/jsonValue"}}]},jsonObject:w,messages:N}};var g=new Set(["register","heartbeat","taskResult"]),_=new Set(["registerAck","registerError","protocolError","task","cancel","shutdown"]);function y(z){if(z===Z)return{ok:!0,message:z};let B=G(z)?z:void 0;return Q("unsupported_protocol_version",`Unsupported RemoteWorker protocol version: ${String(z)}`,B)}var S=[["workerId",!0,U,"register.workerId must be a non-empty string"],["activities",!0,L,"register.activities must be an array of non-empty strings"],["concurrency",!1,G,"register.concurrency must be a finite number"],["queue",!1,U,"register.queue must be a non-empty string"],["deploymentName",!1,U,"register.deploymentName must be a non-empty string when present"],["buildId",!1,U,"register.buildId must be a non-empty string when present"],["runtimeVersion",!1,U,"register.runtimeVersion must be a non-empty string when present"],["gitSha",!1,U,"register.gitSha must be a non-empty string when present"],["startedAt",!1,G,"register.startedAt must be a finite number when present"],["capabilities",!1,f,"register.capabilities must be a JSON object when present"]];function E(z){let B=y(z.protocolVersion);if(!B.ok)return B;let D=A("invalid_registration",z,S);if(!D.ok)return D.error;return{ok:!0,message:{type:"register",protocolVersion:B.message,...D.values}}}function V(z){let B=z.workerId;if(!U(B))return Q("invalid_message","heartbeat.workerId must be a non-empty string");return{ok:!0,message:{type:"heartbeat",workerId:B}}}function O(z,B){let D=B.value;if(!x(D))return Q("invalid_message","completed taskResult.value must be valid JSON");return{ok:!0,message:{type:"taskResult",operationId:z,status:"completed",value:D}}}function m(z,B){let D=B.error;if(typeof D!=="string")return Q("invalid_message","failed taskResult.error must be a string");return{ok:!0,message:{type:"taskResult",operationId:z,status:"failed",error:D}}}function R(z,B){let D=B.error;if(typeof D!=="string")return Q("invalid_message","cancelled taskResult.error must be a string");let X=B.cancelled;if(X!==void 0&&X!==!0)return Q("invalid_message","taskResult.cancelled must be true when present");return{ok:!0,message:{type:"taskResult",operationId:z,status:"cancelled",error:D,...X===!0?{cancelled:X}:{}}}}var p={completed:O,failed:m,cancelled:R};function n(z){return z==="completed"||z==="failed"||z==="cancelled"}function i(z){let B=z.operationId;if(!U(B))return Q("invalid_message","taskResult.operationId must be a non-empty string");let D=z.status;if(!n(D))return Q("invalid_message","taskResult.status must be completed, failed, or cancelled");return p[D](B,z)}var v=[["operationId",!0,U,"task.operationId must be a non-empty string"],["activityName",!0,U,"task.activityName must be a non-empty string"],["input",!0,x,"task.input must be valid JSON"],["attempt",!1,G,"task.attempt must be a finite number"],["headers",!1,K,"task.headers must be a string map"]];function t(z){let B=A("invalid_message",z,v);if(!B.ok)return B.error;return{ok:!0,message:{type:"task",...B.values}}}function d(z){let B=z.operationId;if(!U(B))return Q("invalid_message","cancel.operationId must be a non-empty string");return{ok:!0,message:{type:"cancel",operationId:B}}}function s(){return{ok:!0,message:{type:"shutdown"}}}function u(z){let B=z.protocolVersion;if(B!==Z)return Q("invalid_message",`registerAck.protocolVersion must be ${String(Z)}`);let{workerId:D,queue:X,activities:Y,concurrency:$}=z;if(!U(D))return Q("invalid_message","registerAck.workerId must be a non-empty string");if(!U(X))return Q("invalid_message","registerAck.queue must be a non-empty string");if(!L(Y))return Q("invalid_message","registerAck.activities must be a string array");if(typeof $!=="number"||!Number.isFinite($))return Q("invalid_message","registerAck.concurrency must be a finite number");return{ok:!0,message:{type:"registerAck",protocolVersion:B,workerId:D,queue:X,activities:Y,concurrency:$}}}function c(z){let{code:B,message:D,supportedProtocolVersions:X,requestedProtocolVersion:Y}=z;if(B!=="invalid_registration"&&B!=="unsupported_protocol_version")return Q("invalid_message","registerError.code is not recognized");if(typeof D!=="string")return Q("invalid_message","registerError.message must be a string");if(!Array.isArray(X)||!X.every((b)=>b===Z))return Q("invalid_message","registerError.supportedProtocolVersions is invalid");if(Y!==void 0&&!G(Y))return Q("invalid_message","registerError.requestedProtocolVersion must be a finite number");return{ok:!0,message:{type:"registerError",code:B,message:D,supportedProtocolVersions:X,...Y!==void 0?{requestedProtocolVersion:Y}:{}}}}function l(z){let{code:B,message:D}=z;if(B!=="invalid_json"&&B!=="invalid_message"&&B!=="unknown_message_type"&&B!=="registration_required")return Q("invalid_message","protocolError.code is not recognized");if(typeof D!=="string")return Q("invalid_message","protocolError.message must be a string");return{ok:!0,message:{type:"protocolError",code:B,message:D}}}function Yz(z){if(!F(z))return Q("invalid_message","Worker protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return Q("invalid_message","Worker protocol message.type must be a string");if(!g.has(B))return Q("unknown_message_type",`Unknown worker message type: ${B}`);switch(B){case"register":return E(z);case"heartbeat":return V(z);case"taskResult":return i(z);default:return Q("unknown_message_type",`Unknown worker message type: ${B}`)}}function Zz(z){if(!F(z))return Q("invalid_message","Server protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return Q("invalid_message","Server protocol message.type must be a string");if(!_.has(B))return Q("unknown_message_type",`Unknown server message type: ${B}`);switch(B){case"registerAck":return u(z);case"registerError":return c(z);case"protocolError":return l(z);case"task":return t(z);case"cancel":return d(z);case"shutdown":return s();default:return Q("unknown_message_type",`Unknown server message type: ${B}`)}}export{Yz as parseWorkerToServerMessage,Zz as parseServerToWorkerMessage,x as isRemoteWorkerJsonValue,I as REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,Z as REMOTE_WORKER_PROTOCOL_VERSION,P as REMOTE_WORKER_PROTOCOL_JSON_SCHEMA,W as REMOTE_WORKER_MIN_PROTOCOL_VERSION,N as REMOTE_WORKER_MESSAGE_SCHEMAS,k as REMOTE_WORKER_MAX_PROTOCOL_VERSION};
2
+ var h=Object.defineProperty;var M=(z)=>z;function I(z,B){this[z]=M.bind(null,B)}var r=(z,B)=>{for(var D in B)h(z,D,{get:B[D],enumerable:!0,configurable:!0,set:I.bind(B,D)})};var o=(z,B)=>()=>(z&&(B=z(z=0)),B);function G(z){return z!==null&&typeof z==="object"&&!Array.isArray(z)}function U(z){if(z===null||typeof z==="string"||typeof z==="boolean"||typeof z==="number"&&Number.isFinite(z))return!0;if(Array.isArray(z))return z.every(U);if(!G(z))return!1;return Object.values(z).every(U)}function Y(z){return typeof z==="string"&&z.length>0}function j(z){return Array.isArray(z)&&z.every(Y)}function w(z){if(!G(z))return!1;return Object.values(z).every((B)=>typeof B==="string")}function q(z){if(!G(z))return!1;return Object.values(z).every(U)}function b(z){return typeof z==="number"&&Number.isFinite(z)}function Q(z,B,D){return{ok:!1,error:{code:z,message:B,...D!==void 0?{requestedProtocolVersion:D}:{}}}}function J(z,B,D){let X={};for(let[Z,x,H,C]of D){let L=B[Z];if(L===void 0){if(x)return{ok:!1,error:Q(z,C)};continue}if(!H(L))return{ok:!1,error:Q(z,C)};X[Z]=L}return{ok:!0,values:X}}var $=2,P=2,g=2,_=[2];var F={$ref:"#/$defs/jsonValue"},A={type:"object",additionalProperties:F},y={type:"object",additionalProperties:{type:"string"}},f={const:$},K={register:{type:"object",additionalProperties:!1,required:["type","protocolVersion","workerId","activities"],properties:{type:{const:"register"},protocolVersion:f,workerId:{type:"string",minLength:1},activities:{type:"array",items:{type:"string",minLength:1}},concurrency:{type:"number",minimum:1,maximum:1000},queue:{type:"string",minLength:1},deploymentName:{type:"string",minLength:1},buildId:{type:"string",minLength:1},runtimeVersion:{type:"string",minLength:1},gitSha:{type:"string",minLength:1},startedAt:{type:"number"},capabilities:A}},heartbeat:{type:"object",additionalProperties:!1,required:["type","workerId"],properties:{type:{const:"heartbeat"},workerId:{type:"string",minLength:1}}},taskResult:{oneOf:[{type:"object",additionalProperties:!1,required:["type","operationId","status","value"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"completed"},value:F,attemptToken:{type:"string",minLength:1}}},{type:"object",additionalProperties:!1,required:["type","operationId","status","error"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"failed"},error:{type:"string"},attemptToken:{type:"string",minLength:1}}},{type:"object",additionalProperties:!1,required:["type","operationId","status","error"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"cancelled"},error:{type:"string"},cancelled:{const:!0},attemptToken:{type:"string",minLength:1}}}]},task:{type:"object",additionalProperties:!1,required:["type","operationId","activityName","input"],properties:{type:{const:"task"},operationId:{type:"string",minLength:1},activityName:{type:"string",minLength:1},input:F,attempt:{type:"number",minimum:1},headers:y,attemptToken:{type:"string",minLength:1}}},cancel:{type:"object",additionalProperties:!1,required:["type","operationId"],properties:{type:{const:"cancel"},operationId:{type:"string",minLength:1}}},shutdown:{type:"object",additionalProperties:!1,required:["type"],properties:{type:{const:"shutdown"}}},registerAck:{type:"object",additionalProperties:!1,required:["type","protocolVersion","workerId","queue","activities","concurrency"],properties:{type:{const:"registerAck"},protocolVersion:f,workerId:{type:"string",minLength:1},queue:{type:"string",minLength:1},activities:{type:"array",items:{type:"string",minLength:1}},concurrency:{type:"number",minimum:1,maximum:1000}}},registerError:{type:"object",additionalProperties:!1,required:["type","code","message","supportedProtocolVersions"],properties:{type:{const:"registerError"},code:{enum:["invalid_registration","unsupported_protocol_version"]},message:{type:"string"},supportedProtocolVersions:{type:"array",items:f},requestedProtocolVersion:{type:"number"}}},protocolError:{type:"object",additionalProperties:!1,required:["type","code","message"],properties:{type:{const:"protocolError"},code:{enum:["invalid_json","invalid_message","unknown_message_type","registration_required"]},message:{type:"string"}}}},T={$schema:"https://json-schema.org/draft/2020-12/schema",$id:"https://weft.dev/schemas/remote-worker-protocol.v1.json",title:"Weft RemoteWorker Protocol v1",oneOf:Object.keys(K).map((z)=>({$ref:`#/$defs/messages/${z}`})),$defs:{jsonValue:{oneOf:[{type:"null"},{type:"boolean"},{type:"number"},{type:"string"},{type:"array",items:{$ref:"#/$defs/jsonValue"}},{type:"object",additionalProperties:{$ref:"#/$defs/jsonValue"}}]},jsonObject:A,messages:K}};function N(z){let B=z.attemptToken;if(B===void 0)return{ok:!0,message:{}};if(!Y(B))return Q("invalid_message","taskResult.attemptToken must be a non-empty string when present");return{ok:!0,message:{attemptToken:B}}}function O(z,B){let D=B.value;if(!U(D))return Q("invalid_message","completed taskResult.value must be valid JSON");let X=N(B);if(!X.ok)return X;return{ok:!0,message:{type:"taskResult",operationId:z,status:"completed",value:D,...X.message}}}function V(z,B){let D=B.error;if(typeof D!=="string")return Q("invalid_message","failed taskResult.error must be a string");let X=N(B);if(!X.ok)return X;return{ok:!0,message:{type:"taskResult",operationId:z,status:"failed",error:D,...X.message}}}function S(z,B){let D=B.error;if(typeof D!=="string")return Q("invalid_message","cancelled taskResult.error must be a string");let X=B.cancelled;if(X!==void 0&&X!==!0)return Q("invalid_message","taskResult.cancelled must be true when present");let Z=N(B);if(!Z.ok)return Z;return{ok:!0,message:{type:"taskResult",operationId:z,status:"cancelled",error:D,...X===!0?{cancelled:X}:{},...Z.message}}}var k={completed:O,failed:V,cancelled:S};function E(z){return z==="completed"||z==="failed"||z==="cancelled"}function W(z){let B=z.operationId;if(!Y(B))return Q("invalid_message","taskResult.operationId must be a non-empty string");let D=z.status;if(!E(D))return Q("invalid_message","taskResult.status must be completed, failed, or cancelled");return k[D](B,z)}var m=new Set(["register","heartbeat","taskResult"]),R=new Set(["registerAck","registerError","protocolError","task","cancel","shutdown"]);function p(z){if(z===$)return{ok:!0,message:z};let B=b(z)?z:void 0;return Q("unsupported_protocol_version",`Unsupported RemoteWorker protocol version: ${String(z)}`,B)}var n=[["workerId",!0,Y,"register.workerId must be a non-empty string"],["activities",!0,j,"register.activities must be an array of non-empty strings"],["concurrency",!1,b,"register.concurrency must be a finite number"],["queue",!1,Y,"register.queue must be a non-empty string"],["deploymentName",!1,Y,"register.deploymentName must be a non-empty string when present"],["buildId",!1,Y,"register.buildId must be a non-empty string when present"],["runtimeVersion",!1,Y,"register.runtimeVersion must be a non-empty string when present"],["gitSha",!1,Y,"register.gitSha must be a non-empty string when present"],["startedAt",!1,b,"register.startedAt must be a finite number when present"],["capabilities",!1,q,"register.capabilities must be a JSON object when present"]];function i(z){let B=p(z.protocolVersion);if(!B.ok)return B;let D=J("invalid_registration",z,n);if(!D.ok)return D.error;return{ok:!0,message:{type:"register",protocolVersion:B.message,...D.values}}}function v(z){let B=z.workerId;if(!Y(B))return Q("invalid_message","heartbeat.workerId must be a non-empty string");return{ok:!0,message:{type:"heartbeat",workerId:B}}}var d=[["operationId",!0,Y,"task.operationId must be a non-empty string"],["activityName",!0,Y,"task.activityName must be a non-empty string"],["input",!0,U,"task.input must be valid JSON"],["attempt",!1,b,"task.attempt must be a finite number"],["headers",!1,w,"task.headers must be a string map"],["attemptToken",!1,Y,"task.attemptToken must be a non-empty string"]];function u(z){let B=J("invalid_message",z,d);if(!B.ok)return B.error;return{ok:!0,message:{type:"task",...B.values}}}function s(z){let B=z.operationId;if(!Y(B))return Q("invalid_message","cancel.operationId must be a non-empty string");return{ok:!0,message:{type:"cancel",operationId:B}}}function c(){return{ok:!0,message:{type:"shutdown"}}}function t(z){let B=z.protocolVersion;if(B!==$)return Q("invalid_message",`registerAck.protocolVersion must be ${String($)}`);let{workerId:D,queue:X,activities:Z,concurrency:x}=z;if(!Y(D))return Q("invalid_message","registerAck.workerId must be a non-empty string");if(!Y(X))return Q("invalid_message","registerAck.queue must be a non-empty string");if(!j(Z))return Q("invalid_message","registerAck.activities must be a string array");if(typeof x!=="number"||!Number.isFinite(x))return Q("invalid_message","registerAck.concurrency must be a finite number");return{ok:!0,message:{type:"registerAck",protocolVersion:B,workerId:D,queue:X,activities:Z,concurrency:x}}}function l(z){let{code:B,message:D,supportedProtocolVersions:X,requestedProtocolVersion:Z}=z;if(B!=="invalid_registration"&&B!=="unsupported_protocol_version")return Q("invalid_message","registerError.code is not recognized");if(typeof D!=="string")return Q("invalid_message","registerError.message must be a string");if(!Array.isArray(X)||!X.every((H)=>H===$))return Q("invalid_message","registerError.supportedProtocolVersions is invalid");if(Z!==void 0&&!b(Z))return Q("invalid_message","registerError.requestedProtocolVersion must be a finite number");return{ok:!0,message:{type:"registerError",code:B,message:D,supportedProtocolVersions:X,...Z!==void 0?{requestedProtocolVersion:Z}:{}}}}function a(z){let{code:B,message:D}=z;if(B!=="invalid_json"&&B!=="invalid_message"&&B!=="unknown_message_type"&&B!=="registration_required")return Q("invalid_message","protocolError.code is not recognized");if(typeof D!=="string")return Q("invalid_message","protocolError.message must be a string");return{ok:!0,message:{type:"protocolError",code:B,message:D}}}function Gz(z){if(!G(z))return Q("invalid_message","Worker protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return Q("invalid_message","Worker protocol message.type must be a string");if(!m.has(B))return Q("unknown_message_type",`Unknown worker message type: ${B}`);switch(B){case"register":return i(z);case"heartbeat":return v(z);case"taskResult":return W(z);default:return Q("unknown_message_type",`Unknown worker message type: ${B}`)}}function bz(z){if(!G(z))return Q("invalid_message","Server protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return Q("invalid_message","Server protocol message.type must be a string");if(!R.has(B))return Q("unknown_message_type",`Unknown server message type: ${B}`);switch(B){case"registerAck":return t(z);case"registerError":return l(z);case"protocolError":return a(z);case"task":return u(z);case"cancel":return s(z);case"shutdown":return c();default:return Q("unknown_message_type",`Unknown server message type: ${B}`)}}export{Gz as parseWorkerToServerMessage,bz as parseServerToWorkerMessage,U as isRemoteWorkerJsonValue,_ as REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,$ as REMOTE_WORKER_PROTOCOL_VERSION,T as REMOTE_WORKER_PROTOCOL_JSON_SCHEMA,P as REMOTE_WORKER_MIN_PROTOCOL_VERSION,K as REMOTE_WORKER_MESSAGE_SCHEMAS,g as REMOTE_WORKER_MAX_PROTOCOL_VERSION};
@@ -57,3 +57,32 @@ export declare function scoreWorker(snapshot: WorkerScoreSnapshot): WorkerScore;
57
57
  * ```
58
58
  */
59
59
  export declare function compareScores(a: WorkerScore, b: WorkerScore): number;
60
+ /**
61
+ * Per-worker, per-key in-flight counters for fair-share routing. Keeping this
62
+ * state in one object — rather than an inline `Map<string, Map<string, number>>`
63
+ * in the registry — guarantees increments, releases, and per-worker purges all
64
+ * agree on the same idempotency rules, so the counts never drift from the
65
+ * in-flight task set that drives them.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { FairShareCounters } from './fair-share.ts';
70
+ * const counters = new FairShareCounters();
71
+ * counters.increment('w1', 'tenant-a');
72
+ * console.log(counters.load('w1', 'tenant-a')); // 1
73
+ * ```
74
+ */
75
+ export declare class FairShareCounters {
76
+ #private;
77
+ /** Current in-flight count for `workerId` on `key` (0 when untracked). */
78
+ load(workerId: string, key: string): number;
79
+ /** Increment the in-flight count for `workerId` on `key`. */
80
+ increment(workerId: string, key: string): void;
81
+ /**
82
+ * Decrement the in-flight count for `workerId` on `key`, pruning empty inner
83
+ * and outer maps so an idle worker leaves no residue. Floors at 0.
84
+ */
85
+ release(workerId: string, key: string): void;
86
+ /** Drop all counters for a worker that has disconnected. */
87
+ purge(workerId: string): void;
88
+ }
@@ -13,3 +13,33 @@ export function compareScores(a, b) {
13
13
  return a.inFlight - b.inFlight;
14
14
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
15
15
  }
16
+
17
+ export class FairShareCounters {
18
+ #counts = new Map;
19
+ load(workerId, key) {
20
+ return this.#counts.get(workerId)?.get(key) ?? 0;
21
+ }
22
+ increment(workerId, key) {
23
+ let workerCounts = this.#counts.get(workerId);
24
+ if (workerCounts === void 0) {
25
+ workerCounts = new Map;
26
+ this.#counts.set(workerId, workerCounts);
27
+ }
28
+ workerCounts.set(key, (workerCounts.get(key) ?? 0) + 1);
29
+ }
30
+ release(workerId, key) {
31
+ const workerCounts = this.#counts.get(workerId);
32
+ if (workerCounts === void 0)
33
+ return;
34
+ const next = Math.max(0, (workerCounts.get(key) ?? 0) - 1);
35
+ if (next === 0) {
36
+ workerCounts.delete(key);
37
+ if (workerCounts.size === 0)
38
+ this.#counts.delete(workerId);
39
+ } else
40
+ workerCounts.set(key, next);
41
+ }
42
+ purge(workerId) {
43
+ this.#counts.delete(workerId);
44
+ }
45
+ }
@@ -1,3 +1,4 @@
1
+ import { type FairShareCounters } from './fair-share.ts';
1
2
  import type { WorkerInfo } from './types.ts';
2
3
  /**
3
4
  * Return `true` when `worker` is eligible for the given `activityName` and
@@ -15,3 +16,20 @@ export declare function matchesWorkerCapabilities(worker: WorkerInfo, activityNa
15
16
  * `eligible` must be non-empty — the caller is responsible for this precondition.
16
17
  */
17
18
  export declare function pickLeastLoaded(eligible: WorkerInfo[]): WorkerInfo;
19
+ /**
20
+ * Round-robin selection with a per-(queue, activity) cursor so two activities
21
+ * sharing a queue advance independently. Mutates `cursor` in place, advancing
22
+ * the entry for this (queue, activity) pair.
23
+ *
24
+ * `eligible` must be non-empty — the caller is responsible for this precondition.
25
+ */
26
+ export declare function pickRoundRobin(eligible: WorkerInfo[], cursor: Map<string, number>, queue: string | undefined, activityName: string): WorkerInfo;
27
+ /**
28
+ * Fair-share selection: the worker carrying the fewest in-flight tasks for
29
+ * `fairShareKey` wins, ties broken by overall in-flight count then stable id
30
+ * order. The score snapshot is built synchronously so the ranking is consistent
31
+ * across the full candidate set.
32
+ *
33
+ * `eligible` must be non-empty — the caller is responsible for this precondition.
34
+ */
35
+ export declare function pickFairShare(eligible: WorkerInfo[], counters: FairShareCounters, fairShareKey: string): WorkerInfo;
@@ -1,3 +1,4 @@
1
+ import { compareScores, scoreWorker } from "./fair-share.js";
1
2
  export function matchesWorkerCapabilities(worker, activityName, queue) {
2
3
  if (queue !== void 0 && worker.queue !== queue)
3
4
  return !1;
@@ -14,3 +15,16 @@ export function pickLeastLoaded(eligible) {
14
15
  }
15
16
  return best;
16
17
  }
18
+ export function pickRoundRobin(eligible, cursor, queue, activityName) {
19
+ const key = `${queue ?? "__default__"}::${activityName}`, position = cursor.get(key) ?? 0, pick = eligible[position % eligible.length];
20
+ cursor.set(key, position + 1);
21
+ return pick;
22
+ }
23
+ export function pickFairShare(eligible, counters, fairShareKey) {
24
+ const winner = eligible.map((worker) => scoreWorker({
25
+ id: worker.id,
26
+ inFlight: worker.inFlight,
27
+ keyLoad: counters.load(worker.id, fairShareKey)
28
+ })).reduce((best, candidate) => compareScores(candidate, best) < 0 ? candidate : best);
29
+ return eligible.find((worker) => worker.id === winner.id);
30
+ }
@@ -116,6 +116,13 @@ export interface InFlightTask {
116
116
  visibilityTimeout: number;
117
117
  /** Optional fair-share partition key the task was assigned under. */
118
118
  fairShareKey?: string;
119
+ /**
120
+ * Unique, unguessable token for this dispatch attempt. The WebSocket completion
121
+ * handler compares the worker's echoed token against this in-memory value (after
122
+ * the workerId ownership guard) to reject a stale completion from an earlier
123
+ * attempt reassigned to the same worker.
124
+ */
125
+ attemptToken?: string;
119
126
  }
120
127
  export interface WorkerRegistryOptions {
121
128
  /** Routing policy used by {@link WorkerRegistry.findWorker}. Default: `'least-loaded'`. */
@@ -45,7 +45,7 @@ export declare class WorkerRegistry {
45
45
  */
46
46
  findWorker(activityName: string, options?: RoutingOptions): WorkerInfo | undefined;
47
47
  /** Track a task assignment with a visibility timeout deadline. */
48
- assignTask(workerId: string, operationId: string, visibilityTimeout: number, fairShareKey?: string): void;
48
+ assignTask(workerId: string, operationId: string, visibilityTimeout: number, fairShareKey?: string, attemptToken?: string): void;
49
49
  /** Return tasks whose deadline has passed and remove them from tracking. */
50
50
  checkExpiredTasks(now: number): InFlightTask[];
51
51
  /**
@@ -57,6 +57,21 @@ export declare class WorkerRegistry {
57
57
  getWorkerTasks(workerId: string): InFlightTask[];
58
58
  /** True when `operationId` is in flight on `workerId` — used at the trust boundary to reject stale completions after takeover. */
59
59
  isAssignedToWorker(operationId: string, workerId: string): boolean;
60
+ /**
61
+ * True when `operationId` is in flight on `workerId` for the specific dispatch
62
+ * attempt identified by `attemptToken`. Layered after {@link isAssignedToWorker}
63
+ * to reject a stale completion from an EARLIER attempt that was reassigned to the
64
+ * same worker — the only case the workerId guard alone cannot catch.
65
+ *
66
+ * The token check is purely additive, so a worker that predates the field is
67
+ * never refused (no protocol version bump): it fires only when the tracked
68
+ * task has a stored token AND the completion echoes one AND they differ. A
69
+ * token-less task matches any echo, and an absent echo falls back to the
70
+ * workerId-only guard that already passed. The defended case — a stale earlier
71
+ * attempt — always echoes the OLD token it was dispatched with (present, wrong),
72
+ * so it is still rejected.
73
+ */
74
+ isAssignedToAttempt(operationId: string, workerId: string, attemptToken: string | undefined): boolean;
60
75
  /** Check whether an operation is currently assigned to a worker. */
61
76
  isAssigned(operationId: string): boolean;
62
77
  /** Look up an in-flight task by operationId in O(1). */