@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.
- package/README.md +47 -22
- package/dist/cli/generated/operation-client.generated.d.ts +28 -1
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli-main.js +79 -79
- package/dist/client/handle-delegation.d.ts +4 -0
- package/dist/client/handle-delegation.js +6 -0
- package/dist/client/http-client-requests.d.ts +2 -0
- package/dist/client/http-client-requests.js +3 -0
- package/dist/client/http-client.d.ts +4 -1
- package/dist/client/http-client.js +9 -1
- package/dist/client/interface.d.ts +57 -2
- package/dist/client/local.d.ts +4 -1
- package/dist/client/local.js +7 -0
- package/dist/client/start-body.d.ts +7 -1
- package/dist/client/start-body.js +13 -4
- package/dist/core/codec/extension-codec.js +4 -2
- package/dist/core/codec/index.d.ts +1 -0
- package/dist/core/codec/index.js +1 -0
- package/dist/core/codec/serializer-registry.d.ts +122 -0
- package/dist/core/codec/serializer-registry.js +51 -0
- package/dist/core/context/index.d.ts +9 -0
- package/dist/core/context/internals.d.ts +9 -0
- package/dist/core/context/internals.js +3 -0
- package/dist/core/context/run-operation.d.ts +16 -3
- package/dist/core/context/run-operation.js +16 -7
- package/dist/core/engine/bulk-operations.js +1 -1
- package/dist/core/engine/construction.d.ts +0 -1
- package/dist/core/engine/construction.js +10 -1
- package/dist/core/engine/disposal.js +12 -0
- package/dist/core/engine/engine-create-types.d.ts +0 -14
- package/dist/core/engine/engine-internal-types.d.ts +12 -0
- package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
- package/dist/core/engine/engine-leak-warnings.js +4 -0
- package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
- package/dist/core/engine/engine-runtime-helpers.js +26 -5
- package/dist/core/engine/errors.d.ts +74 -0
- package/dist/core/engine/errors.js +25 -1
- package/dist/core/engine/handle-result.js +1 -1
- package/dist/core/engine/handles.d.ts +89 -40
- package/dist/core/engine/handles.js +25 -27
- package/dist/core/engine/index.d.ts +96 -4
- package/dist/core/engine/index.js +75 -4
- package/dist/core/engine/inline-launch-queue.d.ts +14 -0
- package/dist/core/engine/inline-launch-queue.js +32 -7
- package/dist/core/engine/internals.d.ts +18 -10
- package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
- package/dist/core/engine/lifecycle/persist.js +5 -20
- package/dist/core/engine/lifecycle/resume.js +25 -4
- package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
- package/dist/core/engine/lifecycle/start-commit.js +27 -0
- package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
- package/dist/core/engine/lifecycle/start-exec.js +38 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
- package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
- package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
- package/dist/core/engine/lifecycle/start.d.ts +3 -3
- package/dist/core/engine/lifecycle/start.js +31 -37
- package/dist/core/engine/lifecycle.d.ts +3 -2
- package/dist/core/engine/lifecycle.js +9 -2
- package/dist/core/engine/listing.js +1 -1
- package/dist/core/engine/persisted-data-version.d.ts +5 -9
- package/dist/core/engine/persisted-data-version.js +4 -5
- package/dist/core/engine/schedule-handle.d.ts +45 -0
- package/dist/core/engine/schedule-handle.js +26 -0
- package/dist/core/engine/schedules.d.ts +1 -1
- package/dist/core/engine/schedules.js +7 -3
- package/dist/core/engine/second-instance-detector.d.ts +96 -0
- package/dist/core/engine/second-instance-detector.js +108 -0
- package/dist/core/engine/signals.d.ts +22 -0
- package/dist/core/engine/signals.js +15 -0
- package/dist/core/engine/termination/cleanup.d.ts +25 -0
- package/dist/core/engine/termination/cleanup.js +19 -1
- package/dist/core/engine/termination/complete.js +4 -3
- package/dist/core/engine/termination/suspend.d.ts +68 -0
- package/dist/core/engine/termination/suspend.js +41 -0
- package/dist/core/engine/termination.d.ts +4 -2
- package/dist/core/engine/termination.js +2 -0
- package/dist/core/engine/validation.js +25 -1
- package/dist/core/engine/workflow-feed.d.ts +5 -3
- package/dist/core/events/event-map.d.ts +2 -1
- package/dist/core/events/workflow-events.d.ts +23 -0
- package/dist/core/events/workflow-events.js +9 -0
- package/dist/core/list-filter-validation.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +22 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/step-context.d.ts +10 -6
- package/dist/core/step-context.js +7 -15
- package/dist/core/types/activity.d.ts +6 -3
- package/dist/core/types/identity.d.ts +8 -1
- package/dist/core/types/launch-metadata.d.ts +33 -0
- package/dist/core/types/launch-metadata.js +0 -0
- package/dist/core/types/message-handles.d.ts +25 -0
- package/dist/core/types/options.d.ts +48 -54
- package/dist/core/types/reviews.d.ts +2 -1
- package/dist/core/types/services-resolution.d.ts +47 -0
- package/dist/core/types/services-resolution.js +0 -0
- package/dist/core/types/state.d.ts +11 -11
- package/dist/core/types/workflow-builder.d.ts +5 -4
- package/dist/core/types/workflow-function.d.ts +17 -0
- package/dist/core/types/workflow-snapshot.d.ts +29 -0
- package/dist/core/types/workflow-snapshot.js +0 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.js +3 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +3 -1
- package/dist/diagnostics/doctor.js +6 -3
- package/dist/diagnostics/format.js +2 -2
- package/dist/diagnostics/types.d.ts +1 -0
- package/dist/diagnostics/version-check.js +6 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -1
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +35 -35
- package/dist/mcp/list-filter.js +2 -1
- package/dist/mcp/session.js +1 -0
- package/dist/observability/index.js +2 -2
- package/dist/server/handler.js +30 -30
- package/dist/server/index.js +33 -33
- package/dist/server/interactive-operations.js +1 -0
- package/dist/server/operations/resume-workflow.js +2 -2
- package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
- package/dist/server/operations/start-or-signal-workflow.js +140 -0
- package/dist/server/operations/start-workflow-options.d.ts +32 -0
- package/dist/server/operations/start-workflow-options.js +63 -0
- package/dist/server/operations/start-workflow.js +7 -69
- package/dist/server/operations/suspend-workflow.d.ts +13 -0
- package/dist/server/operations/suspend-workflow.js +36 -0
- package/dist/server/rest-binding.d.ts +18 -7
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/task-dispatch.js +5 -3
- package/dist/server/runtime/task-polling.d.ts +16 -2
- package/dist/server/runtime/task-polling.js +20 -5
- package/dist/server/runtime/websocket-worker.js +8 -0
- package/dist/server/serve-internals.d.ts +8 -0
- package/dist/server/serve-internals.js +4 -2
- package/dist/server/task-state.d.ts +8 -0
- package/dist/service-worker/index.js +28 -28
- package/dist/storage/capabilities.d.ts +10 -2
- package/dist/storage/capabilities.js +2 -2
- package/dist/storage/http.js +2 -2
- package/dist/storage/index.d.ts +6 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/interface.d.ts +26 -0
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +2 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon-value-mapping.d.ts +47 -0
- package/dist/storage/neon-value-mapping.js +11 -0
- package/dist/storage/neon.d.ts +108 -0
- package/dist/storage/neon.js +10 -0
- package/dist/storage/node-sqlite-loader.d.ts +71 -0
- package/dist/storage/node-sqlite-loader.js +41 -0
- package/dist/storage/node-sqlite.d.ts +1 -19
- package/dist/storage/node-sqlite.js +38 -32
- package/dist/storage/postgres-key-value-queries.d.ts +79 -0
- package/dist/storage/postgres-key-value-queries.js +63 -0
- package/dist/storage/resolve.d.ts +2 -165
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/storage-configuration.d.ts +209 -0
- package/dist/storage/storage-configuration.js +0 -0
- package/dist/storage/text-value-store.d.ts +9 -9
- package/dist/storage/turso.js +2 -2
- package/dist/storage/typed-storage.js +1 -1
- package/dist/storage/web-extension.js +1 -1
- package/dist/testing/index.js +33 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/index.js +9 -5
- package/dist/worker/long-poll.js +4 -0
- package/dist/worker/protocol-messages.d.ts +20 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +8 -4
- package/dist/worker/protocol-task-result.d.ts +28 -0
- package/dist/worker/protocol-task-result.js +76 -0
- package/dist/worker/protocol.d.ts +4 -15
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/fair-share.d.ts +29 -0
- package/dist/worker/registry/fair-share.js +30 -0
- package/dist/worker/registry/routing.d.ts +18 -0
- package/dist/worker/registry/routing.js +14 -0
- package/dist/worker/registry/types.d.ts +7 -0
- package/dist/worker/registry.d.ts +16 -1
- package/dist/worker/registry.js +24 -36
- package/package.json +17 -4
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.3.0";
|
package/dist/worker/index.js
CHANGED
|
@@ -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);
|
package/dist/worker/long-poll.js
CHANGED
|
@@ -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`,
|
|
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
|
*
|
package/dist/worker/protocol.js
CHANGED
|
@@ -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). */
|