@lostgradient/weft 0.18.0 → 0.19.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 +3 -1
- package/dist/cli/conformance.js +14 -18
- package/dist/cli/generated/operation-catalog.snapshot.json +258 -16
- package/dist/cli/generated/operation-client.generated.d.ts +30 -6
- package/dist/cli/generated/operation-client.generated.js +4 -0
- package/dist/core/context/child-workflow-pipe.d.ts +1 -1
- package/dist/core/context/child-workflow-pipe.js +1 -1
- package/dist/core/engine/construction.d.ts +2 -0
- package/dist/core/engine/construction.js +3 -0
- package/dist/core/engine/index.js +1 -0
- package/dist/core/engine/listing.d.ts +1 -0
- package/dist/core/engine/listing.js +6 -4
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/core/worker-execution-dispatcher.d.ts +8 -0
- package/dist/core/worker-execution-dispatcher.js +4 -0
- package/dist/core/worker-execution-strategy-options.d.ts +16 -0
- package/dist/core/worker-execution-strategy.js +71 -89
- package/dist/core/worker-fault-handling.d.ts +45 -0
- package/dist/core/worker-fault-handling.js +88 -0
- package/dist/core/worker-protocol.d.ts +18 -1
- package/dist/core/worker-protocol.js +1 -1
- package/dist/core/worker-realm-readiness.d.ts +78 -0
- package/dist/core/worker-realm-readiness.js +127 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +28 -1
- package/dist/json-schema.js +1 -1
- package/dist/runtime/portable.d.ts +14 -0
- package/dist/runtime/portable.js +10 -0
- package/dist/server/index.d.ts +71 -8
- package/dist/server/index.js +7 -3
- package/dist/server/operations/get-task-diagnostics.d.ts +43 -18
- package/dist/server/operations/get-task-diagnostics.js +87 -126
- package/dist/server/operations/get-worker-diagnostics.d.ts +177 -0
- package/dist/server/operations/get-worker-diagnostics.js +117 -0
- package/dist/server/operations/list-worker-registration-rejections.d.ts +79 -0
- package/dist/server/operations/list-worker-registration-rejections.js +60 -0
- package/dist/server/operations/list-workers.js +1 -3
- package/dist/server/operations/static-registrations.js +5 -20
- package/dist/server/rest-bindings.js +31 -2
- package/dist/server/runtime/authentication-bridge.js +6 -14
- package/dist/server/runtime/context.d.ts +23 -0
- package/dist/server/runtime/task-dispatch.d.ts +7 -1
- package/dist/server/runtime/task-dispatch.js +126 -55
- package/dist/server/runtime/task-ledger-completion.d.ts +68 -0
- package/dist/server/runtime/task-ledger-completion.js +72 -0
- package/dist/server/runtime/task-ledger-recovery.d.ts +55 -0
- package/dist/server/runtime/task-ledger-recovery.js +73 -0
- package/dist/server/runtime/task-ledger-runtime.d.ts +64 -0
- package/dist/server/runtime/task-ledger-runtime.js +34 -0
- package/dist/server/runtime/task-metrics.d.ts +6 -4
- package/dist/server/runtime/task-polling.d.ts +14 -12
- package/dist/server/runtime/task-polling.js +106 -82
- package/dist/server/runtime/task-reconciliation.d.ts +35 -8
- package/dist/server/runtime/task-reconciliation.js +75 -74
- package/dist/server/runtime/task-result-resolution.d.ts +2 -10
- package/dist/server/runtime/task-result-resolution.js +0 -67
- package/dist/server/runtime/task-result-view.d.ts +82 -0
- package/dist/server/runtime/task-result-view.js +48 -0
- package/dist/server/runtime/websocket-worker-messaging.d.ts +16 -0
- package/dist/server/runtime/websocket-worker-messaging.js +32 -0
- package/dist/server/runtime/websocket-worker-registration.d.ts +41 -0
- package/dist/server/runtime/websocket-worker-registration.js +134 -0
- package/dist/server/runtime/websocket-worker.d.ts +0 -1
- package/dist/server/runtime/websocket-worker.js +64 -116
- package/dist/server/serve-internals.d.ts +9 -6
- package/dist/server/serve-internals.js +14 -40
- package/dist/server/task-ledger-codec.d.ts +40 -0
- package/dist/server/task-ledger-codec.js +195 -0
- package/dist/server/task-ledger-keys.d.ts +13 -0
- package/dist/server/task-ledger-keys.js +5 -0
- package/dist/server/task-ledger-limits.d.ts +34 -0
- package/dist/server/task-ledger-limits.js +5 -0
- package/dist/server/task-ledger-transition-helpers.d.ts +38 -0
- package/dist/server/task-ledger-transition-helpers.js +42 -0
- package/dist/server/task-ledger-transitions-cancellation.d.ts +31 -0
- package/dist/server/task-ledger-transitions-cancellation.js +73 -0
- package/dist/server/task-ledger-transitions.d.ts +129 -0
- package/dist/server/task-ledger-transitions.js +162 -0
- package/dist/server/task-ledger-types.d.ts +220 -0
- package/dist/server/task-ledger-types.js +1 -0
- package/dist/server/task-ledger.d.ts +17 -0
- package/dist/server/task-ledger.js +24 -0
- package/dist/server/task-state.d.ts +25 -152
- package/dist/server/task-state.js +0 -188
- package/dist/server/worker-admission-policy.d.ts +70 -0
- package/dist/server/worker-admission-policy.js +0 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +0 -2
- package/dist/storage/interface.d.ts +0 -2
- package/dist/storage/interface.js +1 -1
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +1 -1
- package/dist/storage/postgres.js +1 -1
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +1 -1
- package/dist/storage/web-extension.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/index.d.ts +2 -0
- package/dist/worker/index.js +30 -4
- package/dist/worker/manifest/canonical-json.d.ts +20 -0
- package/dist/worker/manifest/canonical-json.js +13 -0
- package/dist/worker/manifest/capabilities.d.ts +24 -0
- package/dist/worker/manifest/capabilities.js +74 -0
- package/dist/worker/manifest/content-digest.d.ts +20 -0
- package/dist/worker/manifest/content-digest.js +13 -0
- package/dist/worker/manifest/declared-shape-digest.d.ts +14 -0
- package/dist/worker/manifest/declared-shape-digest.js +4 -0
- package/dist/worker/manifest/digest.d.ts +82 -0
- package/dist/worker/manifest/digest.js +9 -0
- package/dist/worker/manifest/execution-identity.d.ts +92 -0
- package/dist/worker/manifest/execution-identity.js +36 -0
- package/dist/worker/manifest/failure.d.ts +59 -0
- package/dist/worker/manifest/failure.js +4 -0
- package/dist/worker/manifest/index.d.ts +22 -0
- package/dist/worker/manifest/index.js +23 -0
- package/dist/worker/manifest/internal-realm.d.ts +50 -0
- package/dist/worker/manifest/internal-realm.js +39 -0
- package/dist/worker/manifest/is-record.d.ts +12 -0
- package/dist/worker/manifest/is-record.js +6 -0
- package/dist/worker/manifest/json-scan.d.ts +26 -0
- package/dist/worker/manifest/json-scan.js +88 -0
- package/dist/worker/manifest/limits.d.ts +138 -0
- package/dist/worker/manifest/limits.js +1 -0
- package/dist/worker/manifest/normalize.d.ts +63 -0
- package/dist/worker/manifest/normalize.js +73 -0
- package/dist/worker/manifest/parse-json.d.ts +24 -0
- package/dist/worker/manifest/parse-json.js +15 -0
- package/dist/worker/manifest/parse.d.ts +86 -0
- package/dist/worker/manifest/parse.js +188 -0
- package/dist/worker/manifest/registry-contract-builder.d.ts +120 -0
- package/dist/worker/manifest/registry-contract-builder.js +65 -0
- package/dist/worker/manifest/types.d.ts +263 -0
- package/dist/worker/manifest/types.js +1 -0
- package/dist/worker/manifest/utf8.d.ts +13 -0
- package/dist/worker/manifest/utf8.js +7 -0
- package/dist/worker/options.d.ts +55 -8
- package/dist/worker/options.js +65 -9
- package/dist/worker/protocol-internals.d.ts +1 -2
- package/dist/worker/protocol-internals.js +0 -5
- package/dist/worker/protocol-messages.d.ts +33 -17
- package/dist/worker/protocol-schemas.d.ts +36 -74
- package/dist/worker/protocol-schemas.js +25 -18
- package/dist/worker/protocol-version.d.ts +18 -14
- package/dist/worker/protocol-version.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/deployment-consistency.d.ts +78 -0
- package/dist/worker/registry/deployment-consistency.js +27 -0
- package/dist/worker/registry/drain.d.ts +14 -0
- package/dist/worker/registry/drain.js +24 -0
- package/dist/worker/registry/rejections.d.ts +31 -0
- package/dist/worker/registry/rejections.js +10 -0
- package/dist/worker/registry/summary.d.ts +0 -3
- package/dist/worker/registry/summary.js +2 -5
- package/dist/worker/registry/types.d.ts +10 -3
- package/dist/worker/registry.d.ts +47 -2
- package/dist/worker/registry.js +45 -32
- package/dist/workers/workflow-worker-entry.d.ts +12 -7
- package/dist/workers/workflow-worker-entry.js +9 -2
- package/package.json +3 -3
- package/dist/server/task-resolved-record.d.ts +0 -12
- package/dist/server/task-resolved-record.js +0 -35
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Current RemoteWorker wire protocol version.
|
|
12
12
|
*
|
|
13
|
-
* **Version
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* **Version 3** (Canonical Worker Manifest, WFT-27) replaces the register
|
|
14
|
+
* message's parallel top-level identity fields (`activities`, `queue`,
|
|
15
|
+
* `deploymentName`, `buildId`, `runtimeVersion`, `gitSha`, `capabilities`)
|
|
16
|
+
* with a single `manifest: WorkerManifest` field, and the ack echoes
|
|
17
|
+
* `acceptedManifestDigest` plus `serverCapabilities` instead of echoing
|
|
18
|
+
* `activities` back. Both directions of the handshake reshape — a strictly
|
|
19
|
+
* larger change than version 2's same-shape/different-semantics bump — so
|
|
20
|
+
* this is a clean break rather than a negotiated one: a v2 peer gets the
|
|
21
|
+
* canonical `WorkerProtocolIncompatibleError`, not a confusing
|
|
22
|
+
* `invalid_registration`.
|
|
19
23
|
*
|
|
20
24
|
* @example
|
|
21
25
|
* ```ts
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
* const registration = { type: 'register', protocolVersion: REMOTE_WORKER_PROTOCOL_VERSION };
|
|
25
29
|
* ```
|
|
26
30
|
*/
|
|
27
|
-
export declare const REMOTE_WORKER_PROTOCOL_VERSION =
|
|
31
|
+
export declare const REMOTE_WORKER_PROTOCOL_VERSION = 3;
|
|
28
32
|
/**
|
|
29
33
|
* Lowest RemoteWorker protocol version accepted by this package.
|
|
30
34
|
*
|
|
@@ -32,10 +36,10 @@ export declare const REMOTE_WORKER_PROTOCOL_VERSION = 2;
|
|
|
32
36
|
* ```ts
|
|
33
37
|
* import { REMOTE_WORKER_MIN_PROTOCOL_VERSION } from '@lostgradient/weft/worker-protocol';
|
|
34
38
|
*
|
|
35
|
-
* const supportsCurrentVersion = REMOTE_WORKER_MIN_PROTOCOL_VERSION ===
|
|
39
|
+
* const supportsCurrentVersion = REMOTE_WORKER_MIN_PROTOCOL_VERSION === 3;
|
|
36
40
|
* ```
|
|
37
41
|
*/
|
|
38
|
-
export declare const REMOTE_WORKER_MIN_PROTOCOL_VERSION =
|
|
42
|
+
export declare const REMOTE_WORKER_MIN_PROTOCOL_VERSION = 3;
|
|
39
43
|
/**
|
|
40
44
|
* Highest RemoteWorker protocol version accepted by this package.
|
|
41
45
|
*
|
|
@@ -43,10 +47,10 @@ export declare const REMOTE_WORKER_MIN_PROTOCOL_VERSION = 2;
|
|
|
43
47
|
* ```ts
|
|
44
48
|
* import { REMOTE_WORKER_MAX_PROTOCOL_VERSION } from '@lostgradient/weft/worker-protocol';
|
|
45
49
|
*
|
|
46
|
-
* const canUseRequestedVersion =
|
|
50
|
+
* const canUseRequestedVersion = 3 <= REMOTE_WORKER_MAX_PROTOCOL_VERSION;
|
|
47
51
|
* ```
|
|
48
52
|
*/
|
|
49
|
-
export declare const REMOTE_WORKER_MAX_PROTOCOL_VERSION =
|
|
53
|
+
export declare const REMOTE_WORKER_MAX_PROTOCOL_VERSION = 3;
|
|
50
54
|
/**
|
|
51
55
|
* Explicit supported RemoteWorker protocol versions.
|
|
52
56
|
*
|
|
@@ -54,10 +58,10 @@ export declare const REMOTE_WORKER_MAX_PROTOCOL_VERSION = 2;
|
|
|
54
58
|
* ```ts
|
|
55
59
|
* import { REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS } from '@lostgradient/weft/worker-protocol';
|
|
56
60
|
*
|
|
57
|
-
* const supported = REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS.includes(
|
|
61
|
+
* const supported = REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS.includes(3);
|
|
58
62
|
* ```
|
|
59
63
|
*/
|
|
60
|
-
export declare const REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS: readonly [
|
|
64
|
+
export declare const REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS: readonly [3];
|
|
61
65
|
/**
|
|
62
66
|
* RemoteWorker protocol version accepted by this package.
|
|
63
67
|
*
|
|
@@ -65,7 +69,7 @@ export declare const REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS: readonly [2];
|
|
|
65
69
|
* ```ts
|
|
66
70
|
* import type { RemoteWorkerProtocolVersion } from '@lostgradient/weft/worker-protocol';
|
|
67
71
|
*
|
|
68
|
-
* const version: RemoteWorkerProtocolVersion =
|
|
72
|
+
* const version: RemoteWorkerProtocolVersion = 3;
|
|
69
73
|
* ```
|
|
70
74
|
*/
|
|
71
75
|
export type RemoteWorkerProtocolVersion = (typeof REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS)[number];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const REMOTE_WORKER_PROTOCOL_VERSION =
|
|
1
|
+
export const REMOTE_WORKER_PROTOCOL_VERSION = 3, REMOTE_WORKER_MIN_PROTOCOL_VERSION = 3, REMOTE_WORKER_MAX_PROTOCOL_VERSION = 3, REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS = [REMOTE_WORKER_PROTOCOL_VERSION];
|
package/dist/worker/protocol.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var c=import.meta.require,l=(z,B,D)=>{if(B!=null){if(typeof B!=="object"&&typeof B!=="function")throw TypeError('Object expected to be assigned to "using" declaration');let X;if(D)X=B[Symbol.asyncDispose];if(X===void 0)X=B[Symbol.dispose];if(typeof X!=="function")throw TypeError("Object not disposable");z.push([D,X,B])}else if(D)z.push([D]);return B},a=(z,B,D)=>{let X=(U)=>B=D?new SuppressedError(U,B,"An error was suppressed during disposal"):(D=!0,U),Z=(U)=>{while(U=z.pop())try{var j=U[1]&&U[1].call(U[2]);if(U[0])return Promise.resolve(j).then(Z,(G)=>(X(G),Z()))}catch(G){X(G)}if(D)throw B};return Z()};function b(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(!b(z))return!1;return Object.values(z).every(x)}function Y(z){return typeof z==="string"&&z.length>0}function L(z){return Array.isArray(z)&&z.every(Y)}function q(z){if(!b(z))return!1;return Object.values(z).every((B)=>typeof B==="string")}function A(z){if(!b(z))return!1;return Object.values(z).every(x)}function H(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 f(z,B,D){let X={};for(let[Z,U,j,G]of D){let J=B[Z];if(J===void 0){if(U)return{ok:!1,error:Q(z,G)};continue}if(!j(J))return{ok:!1,error:Q(z,G)};X[Z]=J}return{ok:!0,values:X}}var $=2,W=2,M=2,I=[2];var w={$ref:"#/$defs/jsonValue"},C={type:"object",additionalProperties:w},g={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:C}},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","attemptToken"],properties:{type:{const:"taskResult"},operationId:{type:"string",minLength:1},status:{const:"completed"},value:w,attemptToken:{type:"string",minLength:1}}},{type:"object",additionalProperties:!1,required:["type","operationId","status","error","attemptToken"],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","attemptToken"],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","attemptToken"],properties:{type:{const:"task"},operationId:{type:"string",minLength:1},activityName:{type:"string",minLength:1},input:w,attempt:{type:"number",minimum:1},headers:g,workflowExecutionToken:{type:"string",minLength:1},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"}}}},P={$schema:"https://json-schema.org/draft/2020-12/schema",$id:`https://weft.dev/schemas/remote-worker-protocol.v${String($)}.json`,title:`Weft RemoteWorker Protocol v${String($)}`,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:C,messages:K}};function N(z){let B=z.attemptToken;if(!Y(B))return Q("invalid_message","taskResult.attemptToken must be a non-empty string");return{ok:!0,message:{attemptToken:B}}}function y(z,B){let D=B.value;if(!x(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 T(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 _(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 V={completed:y,failed:T,cancelled:_};function k(z){return z==="completed"||z==="failed"||z==="cancelled"}function h(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(!k(D))return Q("invalid_message","taskResult.status must be completed, failed, or cancelled");return V[D](B,z)}var S=new Set(["register","heartbeat","taskResult"]),O=new Set(["registerAck","registerError","protocolError","task","cancel","shutdown"]);function E(z){if(z===$)return{ok:!0,message:z};let B=H(z)?z:void 0;return Q("unsupported_protocol_version",`Unsupported RemoteWorker protocol version: ${String(z)}`,B)}var m=[["workerId",!0,Y,"register.workerId must be a non-empty string"],["activities",!0,L,"register.activities must be an array of non-empty strings"],["concurrency",!1,H,"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,H,"register.startedAt must be a finite number when present"],["capabilities",!1,A,"register.capabilities must be a JSON object when present"]];function R(z){let B=E(z.protocolVersion);if(!B.ok)return B;let D=f("invalid_registration",z,m);if(!D.ok)return D.error;return{ok:!0,message:{type:"register",protocolVersion:B.message,...D.values}}}function p(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 n=[["operationId",!0,Y,"task.operationId must be a non-empty string"],["activityName",!0,Y,"task.activityName must be a non-empty string"],["input",!0,x,"task.input must be valid JSON"],["attempt",!1,H,"task.attempt must be a finite number"],["headers",!1,q,"task.headers must be a string map"],["workflowExecutionToken",!1,Y,"task.workflowExecutionToken must be a non-empty string"],["attemptToken",!0,Y,"task.attemptToken must be a non-empty string"]];function i(z){let B=f("invalid_message",z,n);if(!B.ok)return B.error;return{ok:!0,message:{type:"task",...B.values}}}function v(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 d(){return{ok:!0,message:{type:"shutdown"}}}function u(z){let B=z.protocolVersion;if(B!==$)return Q("invalid_message",`registerAck.protocolVersion must be ${String($)}`);let{workerId:D,queue:X,activities:Z,concurrency:U}=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(!L(Z))return Q("invalid_message","registerAck.activities must be a string array");if(typeof U!=="number"||!Number.isFinite(U))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:U}}}function s(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((j)=>j===$))return Q("invalid_message","registerError.supportedProtocolVersions is invalid");if(Z!==void 0&&!H(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 t(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 $z(z){if(!b(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(!S.has(B))return Q("unknown_message_type",`Unknown worker message type: ${B}`);switch(B){case"register":return R(z);case"heartbeat":return p(z);case"taskResult":return h(z);default:return Q("unknown_message_type",`Unknown worker message type: ${B}`)}}function xz(z){if(!b(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(!O.has(B))return Q("unknown_message_type",`Unknown server message type: ${B}`);switch(B){case"registerAck":return u(z);case"registerError":return s(z);case"protocolError":return t(z);case"task":return i(z);case"cancel":return v(z);case"shutdown":return d();default:return Q("unknown_message_type",`Unknown server message type: ${B}`)}}export{$z as parseWorkerToServerMessage,xz as parseServerToWorkerMessage,x as isRemoteWorkerJsonValue,I as REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,$ as REMOTE_WORKER_PROTOCOL_VERSION,P as REMOTE_WORKER_PROTOCOL_JSON_SCHEMA,W as REMOTE_WORKER_MIN_PROTOCOL_VERSION,K as REMOTE_WORKER_MESSAGE_SCHEMAS,M as REMOTE_WORKER_MAX_PROTOCOL_VERSION};
|
|
2
|
+
var t=import.meta.require,l=(z,B,Q)=>{if(B!=null){if(typeof B!=="object"&&typeof B!=="function")throw TypeError('Object expected to be assigned to "using" declaration');let Y;if(Q)Y=B[Symbol.asyncDispose];if(Y===void 0)Y=B[Symbol.dispose];if(typeof Y!=="function")throw TypeError("Object not disposable");z.push([Q,Y,B])}else if(Q)z.push([Q]);return B},a=(z,B,Q)=>{let Y=($)=>B=Q?new SuppressedError($,B,"An error was suppressed during disposal"):(Q=!0,$),Z=($)=>{while($=z.pop())try{var x=$[1]&&$[1].call($[2]);if($[0])return Promise.resolve(x).then(Z,(b)=>(Y(b),Z()))}catch(b){Y(b)}if(Q)throw B};return Z()};function j(z){return z!==null&&typeof z==="object"&&!Array.isArray(z)}function H(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(H);if(!j(z))return!1;return Object.values(z).every(H)}function U(z){return typeof z==="string"&&z.length>0}function K(z){return Array.isArray(z)&&z.every(U)}function N(z){if(!j(z))return!1;return Object.values(z).every((B)=>typeof B==="string")}function G(z){return typeof z==="number"&&Number.isFinite(z)}function X(z,B,Q){return{ok:!1,error:{code:z,message:B,...Q!==void 0?{requestedProtocolVersion:Q}:{}}}}function L(z,B,Q){let Y={};for(let[Z,$,x,b]of Q){let J=B[Z];if(J===void 0){if($)return{ok:!1,error:X(z,b)};continue}if(!x(J))return{ok:!1,error:X(z,b)};Y[Z]=J}return{ok:!0,values:Y}}var D=3,h=3,C=3,W=[3];var F={$ref:"#/$defs/jsonValue"},I={type:"object",additionalProperties:F},M={type:"object",additionalProperties:{type:"string"}},q={const:D},f={register:{type:"object",additionalProperties:!1,required:["type","protocolVersion","workerId","manifest"],properties:{type:{const:"register"},protocolVersion:q,workerId:{type:"string",minLength:1},manifest:{type:"object"},concurrency:{type:"number",minimum:1,maximum:1000},startedAt:{type:"number"}}},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","attemptToken"],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","attemptToken"],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","attemptToken"],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","attemptToken"],properties:{type:{const:"task"},operationId:{type:"string",minLength:1},activityName:{type:"string",minLength:1},input:F,attempt:{type:"number",minimum:1},headers:M,workflowExecutionToken:{type:"string",minLength:1},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","concurrency","acceptedManifestDigest","serverCapabilities"],properties:{type:{const:"registerAck"},protocolVersion:q,workerId:{type:"string",minLength:1},queue:{type:"string",minLength:1},concurrency:{type:"number",minimum:1,maximum:1000},acceptedManifestDigest:{type:"string",minLength:1},serverCapabilities:{type:"array",items:{type:"string",minLength:1}}}},registerError:{type:"object",additionalProperties:!1,required:["type","code","message","supportedProtocolVersions"],properties:{type:{const:"registerError"},code:{enum:["invalid_registration","unsupported_protocol_version","deployment_conflict","registration_rejected"]},message:{type:"string"},supportedProtocolVersions:{type:"array",items:{type:"number"}},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.v${String(D)}.json`,title:`Weft RemoteWorker Protocol v${String(D)}`,oneOf:Object.keys(f).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:I,messages:f}};function w(z){let B=z.attemptToken;if(!U(B))return X("invalid_message","taskResult.attemptToken must be a non-empty string");return{ok:!0,message:{attemptToken:B}}}function g(z,B){let Q=B.value;if(!H(Q))return X("invalid_message","completed taskResult.value must be valid JSON");let Y=w(B);if(!Y.ok)return Y;return{ok:!0,message:{type:"taskResult",operationId:z,status:"completed",value:Q,...Y.message}}}function y(z,B){let Q=B.error;if(typeof Q!=="string")return X("invalid_message","failed taskResult.error must be a string");let Y=w(B);if(!Y.ok)return Y;return{ok:!0,message:{type:"taskResult",operationId:z,status:"failed",error:Q,...Y.message}}}function T(z,B){let Q=B.error;if(typeof Q!=="string")return X("invalid_message","cancelled taskResult.error must be a string");let Y=B.cancelled;if(Y!==void 0&&Y!==!0)return X("invalid_message","taskResult.cancelled must be true when present");let Z=w(B);if(!Z.ok)return Z;return{ok:!0,message:{type:"taskResult",operationId:z,status:"cancelled",error:Q,...Y===!0?{cancelled:Y}:{},...Z.message}}}var _={completed:g,failed:y,cancelled:T};function V(z){return z==="completed"||z==="failed"||z==="cancelled"}function A(z){let B=z.operationId;if(!U(B))return X("invalid_message","taskResult.operationId must be a non-empty string");let Q=z.status;if(!V(Q))return X("invalid_message","taskResult.status must be completed, failed, or cancelled");return _[Q](B,z)}var k=new Set(["register","heartbeat","taskResult"]),S=new Set(["registerAck","registerError","protocolError","task","cancel","shutdown"]);function m(z){if(z===D)return{ok:!0,message:z};let B=G(z)?z:void 0;return X("unsupported_protocol_version",`Unsupported RemoteWorker protocol version: ${String(z)}`,B)}var O=[["workerId",!0,U,"register.workerId must be a non-empty string"],["manifest",!0,j,"register.manifest must be a JSON object"],["concurrency",!1,G,"register.concurrency must be a finite number"],["startedAt",!1,G,"register.startedAt must be a finite number when present"]];function E(z){let B=m(z.protocolVersion);if(!B.ok)return B;let Q=L("invalid_registration",z,O);if(!Q.ok)return Q.error;return{ok:!0,message:{type:"register",protocolVersion:B.message,...Q.values}}}function R(z){let B=z.workerId;if(!U(B))return X("invalid_message","heartbeat.workerId must be a non-empty string");return{ok:!0,message:{type:"heartbeat",workerId:B}}}var p=[["operationId",!0,U,"task.operationId must be a non-empty string"],["activityName",!0,U,"task.activityName must be a non-empty string"],["input",!0,H,"task.input must be valid JSON"],["attempt",!1,G,"task.attempt must be a finite number"],["headers",!1,N,"task.headers must be a string map"],["workflowExecutionToken",!1,U,"task.workflowExecutionToken must be a non-empty string"],["attemptToken",!0,U,"task.attemptToken must be a non-empty string"]];function n(z){let B=L("invalid_message",z,p);if(!B.ok)return B.error;return{ok:!0,message:{type:"task",...B.values}}}function i(z){let B=z.operationId;if(!U(B))return X("invalid_message","cancel.operationId must be a non-empty string");return{ok:!0,message:{type:"cancel",operationId:B}}}function v(){return{ok:!0,message:{type:"shutdown"}}}function d(z){let B=z.protocolVersion;if(B!==D)return X("invalid_message",`registerAck.protocolVersion must be ${String(D)}`);let{workerId:Q,queue:Y,concurrency:Z,acceptedManifestDigest:$,serverCapabilities:x}=z;if(!U(Q))return X("invalid_message","registerAck.workerId must be a non-empty string");if(!U(Y))return X("invalid_message","registerAck.queue must be a non-empty string");if(typeof Z!=="number"||!Number.isFinite(Z))return X("invalid_message","registerAck.concurrency must be a finite number");if(!U($))return X("invalid_message","registerAck.acceptedManifestDigest must be a non-empty string");if(!K(x))return X("invalid_message","registerAck.serverCapabilities must be an array of non-empty strings");return{ok:!0,message:{type:"registerAck",protocolVersion:B,workerId:Q,queue:Y,concurrency:Z,acceptedManifestDigest:$,serverCapabilities:x}}}function u(z){return z==="invalid_registration"||z==="unsupported_protocol_version"||z==="deployment_conflict"||z==="registration_rejected"}function s(z){let{code:B,message:Q,supportedProtocolVersions:Y,requestedProtocolVersion:Z}=z;if(!u(B))return X("invalid_message","registerError.code is not recognized");if(typeof Q!=="string")return X("invalid_message","registerError.message must be a string");if(!Array.isArray(Y)||!Y.every((x)=>G(x)))return X("invalid_message","registerError.supportedProtocolVersions is invalid");if(Z!==void 0&&!G(Z))return X("invalid_message","registerError.requestedProtocolVersion must be a finite number");return{ok:!0,message:{type:"registerError",code:B,message:Q,supportedProtocolVersions:Y,...Z!==void 0?{requestedProtocolVersion:Z}:{}}}}function c(z){let{code:B,message:Q}=z;if(B!=="invalid_json"&&B!=="invalid_message"&&B!=="unknown_message_type"&&B!=="registration_required")return X("invalid_message","protocolError.code is not recognized");if(typeof Q!=="string")return X("invalid_message","protocolError.message must be a string");return{ok:!0,message:{type:"protocolError",code:B,message:Q}}}function xz(z){if(!j(z))return X("invalid_message","Worker protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return X("invalid_message","Worker protocol message.type must be a string");if(!k.has(B))return X("unknown_message_type",`Unknown worker message type: ${B}`);switch(B){case"register":return E(z);case"heartbeat":return R(z);case"taskResult":return A(z);default:return X("unknown_message_type",`Unknown worker message type: ${B}`)}}function Dz(z){if(!j(z))return X("invalid_message","Server protocol message must be a JSON object");let B=z.type;if(typeof B!=="string")return X("invalid_message","Server protocol message.type must be a string");if(!S.has(B))return X("unknown_message_type",`Unknown server message type: ${B}`);switch(B){case"registerAck":return d(z);case"registerError":return s(z);case"protocolError":return c(z);case"task":return n(z);case"cancel":return i(z);case"shutdown":return v();default:return X("unknown_message_type",`Unknown server message type: ${B}`)}}export{xz as parseWorkerToServerMessage,Dz as parseServerToWorkerMessage,H as isRemoteWorkerJsonValue,W as REMOTE_WORKER_SUPPORTED_PROTOCOL_VERSIONS,D as REMOTE_WORKER_PROTOCOL_VERSION,P as REMOTE_WORKER_PROTOCOL_JSON_SCHEMA,h as REMOTE_WORKER_MIN_PROTOCOL_VERSION,f as REMOTE_WORKER_MESSAGE_SCHEMAS,C as REMOTE_WORKER_MAX_PROTOCOL_VERSION};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local enforcement that one `(deploymentName, buildId)` pair never
|
|
3
|
+
* registers two different artifact digests.
|
|
4
|
+
*
|
|
5
|
+
* A build ID is meant to identify one immutable build output. If two workers
|
|
6
|
+
* present the same deployment name and build ID with different artifact
|
|
7
|
+
* digests, at least one of them is lying about what it is running — either a
|
|
8
|
+
* build ID was reused across a redeploy with different bytes, or a worker is
|
|
9
|
+
* misconfigured. Either way, silently accepting both would let routing send
|
|
10
|
+
* work to a worker running code the operator did not intend for that build
|
|
11
|
+
* identity.
|
|
12
|
+
*
|
|
13
|
+
* This is process-local and non-durable: restart-durable enforcement needs
|
|
14
|
+
* revision-aware routing, which is not yet built. The guard also does not
|
|
15
|
+
* evict entries when a worker disconnects — surviving disconnects is the
|
|
16
|
+
* point. A redeploy that reuses a build ID with different bytes must still
|
|
17
|
+
* conflict even if every worker from the first deploy has since dropped off,
|
|
18
|
+
* because the record of what that build ID means must outlive any single
|
|
19
|
+
* connection.
|
|
20
|
+
*
|
|
21
|
+
* @module worker/registry/deployment-consistency
|
|
22
|
+
*/
|
|
23
|
+
/** Outcome of {@link DeploymentConsistencyGuard.checkAndRecord}. */
|
|
24
|
+
export type DeploymentConsistencyResult = {
|
|
25
|
+
readonly ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly existingArtifactDigest: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Tracks the artifact digest first seen for each `(deploymentName, buildId)`
|
|
32
|
+
* pair for the lifetime of this process.
|
|
33
|
+
*
|
|
34
|
+
* Keyed by nested maps rather than a joined string: `deploymentName` and
|
|
35
|
+
* `buildId` are validated only as non-empty, byte-bounded strings, with no
|
|
36
|
+
* character-set restriction, so a separator-joined key could collide across
|
|
37
|
+
* two distinct pairs (e.g. `("ab", "c")` and `("a", "bc")` would
|
|
38
|
+
* both join to `"abc"`). Nested maps compare each component
|
|
39
|
+
* independently and cannot collide this way.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { DeploymentConsistencyGuard } from '@lostgradient/weft/server';
|
|
44
|
+
*
|
|
45
|
+
* const guard = new DeploymentConsistencyGuard();
|
|
46
|
+
* console.log(guard.checkAndRecord('billing', 'b3', 'sha256:aa').ok); // true
|
|
47
|
+
* console.log(guard.checkAndRecord('billing', 'b3', 'sha256:aa').ok); // true (same digest)
|
|
48
|
+
* console.log(guard.checkAndRecord('billing', 'b3', 'sha256:bb').ok); // false (conflict)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare class DeploymentConsistencyGuard {
|
|
52
|
+
#private;
|
|
53
|
+
constructor();
|
|
54
|
+
/**
|
|
55
|
+
* Read-only: does `artifactDigest` conflict with any digest already
|
|
56
|
+
* recorded for `(deploymentName, buildId)`? Never records. Callers with a
|
|
57
|
+
* rejection gate between checking and committing (an admission policy, a
|
|
58
|
+
* hijack guard) must call this before that gate and call {@link record}
|
|
59
|
+
* only once every later gate has also passed, so a worker this function's
|
|
60
|
+
* caller ultimately declines cannot permanently poison a deployment/build
|
|
61
|
+
* slot with an untrusted digest.
|
|
62
|
+
*/
|
|
63
|
+
check(deploymentName: string, buildId: string, artifactDigest: string): DeploymentConsistencyResult;
|
|
64
|
+
/**
|
|
65
|
+
* Record `artifactDigest` as the digest of record for `(deploymentName,
|
|
66
|
+
* buildId)` if this is the first sighting; a no-op otherwise. Callers must
|
|
67
|
+
* already have confirmed via {@link check} that this does not conflict.
|
|
68
|
+
*/
|
|
69
|
+
record(deploymentName: string, buildId: string, artifactDigest: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Check whether `artifactDigest` is consistent with any digest already
|
|
72
|
+
* recorded for `(deploymentName, buildId)`, recording it as the digest of
|
|
73
|
+
* record when this is the first sighting. Convenience combining
|
|
74
|
+
* {@link check} and {@link record} for callers with no rejection gate
|
|
75
|
+
* between checking and committing.
|
|
76
|
+
*/
|
|
77
|
+
checkAndRecord(deploymentName: string, buildId: string, artifactDigest: string): DeploymentConsistencyResult;
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class DeploymentConsistencyGuard {
|
|
2
|
+
#artifactDigestByBuildIdByDeployment;
|
|
3
|
+
constructor() {
|
|
4
|
+
this.#artifactDigestByBuildIdByDeployment = new Map;
|
|
5
|
+
}
|
|
6
|
+
check(deploymentName, buildId, artifactDigest) {
|
|
7
|
+
const existingArtifactDigest = this.#artifactDigestByBuildIdByDeployment.get(deploymentName)?.get(buildId);
|
|
8
|
+
if (existingArtifactDigest === void 0 || existingArtifactDigest === artifactDigest)
|
|
9
|
+
return { ok: !0 };
|
|
10
|
+
return { ok: !1, existingArtifactDigest };
|
|
11
|
+
}
|
|
12
|
+
record(deploymentName, buildId, artifactDigest) {
|
|
13
|
+
let byBuildId = this.#artifactDigestByBuildIdByDeployment.get(deploymentName);
|
|
14
|
+
if (byBuildId === void 0) {
|
|
15
|
+
byBuildId = new Map;
|
|
16
|
+
this.#artifactDigestByBuildIdByDeployment.set(deploymentName, byBuildId);
|
|
17
|
+
}
|
|
18
|
+
if (!byBuildId.has(buildId))
|
|
19
|
+
byBuildId.set(buildId, artifactDigest);
|
|
20
|
+
}
|
|
21
|
+
checkAndRecord(deploymentName, buildId, artifactDigest) {
|
|
22
|
+
const result = this.check(deploymentName, buildId, artifactDigest);
|
|
23
|
+
if (result.ok)
|
|
24
|
+
this.record(deploymentName, buildId, artifactDigest);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorkerDrainOptions, WorkerHealth, WorkerInfo } from './types.ts';
|
|
2
|
+
/** A drain marker: when it started and an optional operator-supplied reason. */
|
|
3
|
+
export type DrainRecord = {
|
|
4
|
+
reason?: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function createDrainRecord(options?: WorkerDrainOptions): DrainRecord;
|
|
8
|
+
/**
|
|
9
|
+
* The drain record covering `worker`: its own explicit marker if set,
|
|
10
|
+
* otherwise its deployment's marker (when the worker declares a deployment).
|
|
11
|
+
*/
|
|
12
|
+
export declare function drainRecordForWorker(worker: WorkerInfo, deploymentDrainStates: ReadonlyMap<string, DrainRecord>): DrainRecord | undefined;
|
|
13
|
+
export declare function isWorkerDraining(worker: WorkerInfo, deploymentDrainStates: ReadonlyMap<string, DrainRecord>): boolean;
|
|
14
|
+
export declare function workerHealth(worker: WorkerInfo, deploymentDrainStates: ReadonlyMap<string, DrainRecord>): WorkerHealth;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function createDrainRecord(options) {
|
|
2
|
+
return {
|
|
3
|
+
startedAt: options?.updatedAt ?? Date.now(),
|
|
4
|
+
...options?.reason !== void 0 ? { reason: options.reason } : {}
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function drainRecordForWorker(worker, deploymentDrainStates) {
|
|
8
|
+
if (worker.drainStartedAt !== void 0)
|
|
9
|
+
return {
|
|
10
|
+
startedAt: worker.drainStartedAt,
|
|
11
|
+
...worker.drainReason !== void 0 ? { reason: worker.drainReason } : {}
|
|
12
|
+
};
|
|
13
|
+
if (worker.deploymentName === void 0)
|
|
14
|
+
return;
|
|
15
|
+
return deploymentDrainStates.get(worker.deploymentName);
|
|
16
|
+
}
|
|
17
|
+
export function isWorkerDraining(worker, deploymentDrainStates) {
|
|
18
|
+
return drainRecordForWorker(worker, deploymentDrainStates) !== void 0;
|
|
19
|
+
}
|
|
20
|
+
export function workerHealth(worker, deploymentDrainStates) {
|
|
21
|
+
if (!isWorkerDraining(worker, deploymentDrainStates))
|
|
22
|
+
return "active";
|
|
23
|
+
return worker.inFlight > 0 ? "draining" : "drained";
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RegisterErrorMessage } from '../protocol.ts';
|
|
2
|
+
/** Number of most-recent rejection entries retained before the oldest is evicted. */
|
|
3
|
+
export declare const MAX_REGISTRATION_REJECTION_ENTRIES = 200;
|
|
4
|
+
/**
|
|
5
|
+
* One declined `register` attempt.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately bounded content: the rejection `code` (the same taxonomy
|
|
8
|
+
* `registerError` sends on the wire), the attempted `workerId` when the frame
|
|
9
|
+
* carried one, and whichever deployment/queue identity had already been
|
|
10
|
+
* parsed by the time the gate that rejected it ran. No free-text message and
|
|
11
|
+
* no manifest content — this is an auditable event log, not a diagnostic
|
|
12
|
+
* dump. `workerId` is optional because the earliest rejection gate — a
|
|
13
|
+
* malformed or wire-shape-invalid frame — can fail before any `workerId`
|
|
14
|
+
* is known to be valid.
|
|
15
|
+
*/
|
|
16
|
+
export type RegistrationRejectionEntry = Readonly<{
|
|
17
|
+
code: RegisterErrorMessage['code'];
|
|
18
|
+
workerId?: string | undefined;
|
|
19
|
+
rejectedAt: number;
|
|
20
|
+
queue?: string | undefined;
|
|
21
|
+
deploymentName?: string | undefined;
|
|
22
|
+
buildId?: string | undefined;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Append `entry` to `log`, evicting the oldest entry once the log exceeds
|
|
26
|
+
* {@link MAX_REGISTRATION_REJECTION_ENTRIES}. Mutates `log` in place and
|
|
27
|
+
* returns it, matching the registry's existing in-place array conventions.
|
|
28
|
+
*/
|
|
29
|
+
export declare function appendRegistrationRejection(log: RegistrationRejectionEntry[], entry: RegistrationRejectionEntry): RegistrationRejectionEntry[];
|
|
30
|
+
/** Return the `limit` most recent entries, newest first. */
|
|
31
|
+
export declare function recentRegistrationRejections(log: readonly RegistrationRejectionEntry[], limit: number): RegistrationRejectionEntry[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const MAX_REGISTRATION_REJECTION_ENTRIES = 200;
|
|
2
|
+
export function appendRegistrationRejection(log, entry) {
|
|
3
|
+
log.push(entry);
|
|
4
|
+
if (log.length > MAX_REGISTRATION_REJECTION_ENTRIES)
|
|
5
|
+
log.splice(0, log.length - MAX_REGISTRATION_REJECTION_ENTRIES);
|
|
6
|
+
return log;
|
|
7
|
+
}
|
|
8
|
+
export function recentRegistrationRejections(log, limit) {
|
|
9
|
+
return log.slice(Math.max(0, log.length - limit)).toReversed();
|
|
10
|
+
}
|
|
@@ -19,7 +19,6 @@ export type WorkerSnapshot = {
|
|
|
19
19
|
deploymentName?: string | undefined;
|
|
20
20
|
buildId?: string | undefined;
|
|
21
21
|
runtimeVersion?: string | undefined;
|
|
22
|
-
gitSha?: string | undefined;
|
|
23
22
|
};
|
|
24
23
|
/**
|
|
25
24
|
* Project a single worker snapshot into a {@link WorkerSummary}.
|
|
@@ -42,7 +41,6 @@ export type WorkerDeploymentSummary = {
|
|
|
42
41
|
deploymentName: string | null;
|
|
43
42
|
buildId: string | null;
|
|
44
43
|
runtimeVersion: string | null;
|
|
45
|
-
gitSha: string | null;
|
|
46
44
|
health: WorkerHealth;
|
|
47
45
|
workers: number;
|
|
48
46
|
activeWorkers: number;
|
|
@@ -55,7 +53,6 @@ type WorkerDeploymentIdentity = {
|
|
|
55
53
|
deploymentName?: string | null | undefined;
|
|
56
54
|
buildId?: string | null | undefined;
|
|
57
55
|
runtimeVersion?: string | null | undefined;
|
|
58
|
-
gitSha?: string | null | undefined;
|
|
59
56
|
};
|
|
60
57
|
/** Stable composite key for grouping workers by deployment identity. */
|
|
61
58
|
export declare function deploymentIdentityKey(identity: WorkerDeploymentIdentity): string;
|
|
@@ -15,8 +15,7 @@ export function projectWorkerSummary(snapshot, now) {
|
|
|
15
15
|
health: snapshot.health,
|
|
16
16
|
...snapshot.deploymentName !== void 0 ? { deploymentName: snapshot.deploymentName } : {},
|
|
17
17
|
...snapshot.buildId !== void 0 ? { buildId: snapshot.buildId } : {},
|
|
18
|
-
...snapshot.runtimeVersion !== void 0 ? { runtimeVersion: snapshot.runtimeVersion } : {}
|
|
19
|
-
...snapshot.gitSha !== void 0 ? { gitSha: snapshot.gitSha } : {}
|
|
18
|
+
...snapshot.runtimeVersion !== void 0 ? { runtimeVersion: snapshot.runtimeVersion } : {}
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
export function projectWorkerSummaries(snapshots, now) {
|
|
@@ -26,8 +25,7 @@ export function deploymentIdentityKey(identity) {
|
|
|
26
25
|
return [
|
|
27
26
|
identity.deploymentName ?? "",
|
|
28
27
|
identity.buildId ?? "",
|
|
29
|
-
identity.runtimeVersion ?? ""
|
|
30
|
-
identity.gitSha ?? ""
|
|
28
|
+
identity.runtimeVersion ?? ""
|
|
31
29
|
].join("\x1F");
|
|
32
30
|
}
|
|
33
31
|
export function countWorkerHealth(healthValues) {
|
|
@@ -55,7 +53,6 @@ export function projectDeploymentSummary(workers, drainActive) {
|
|
|
55
53
|
deploymentName: first?.deploymentName ?? null,
|
|
56
54
|
buildId: first?.buildId ?? null,
|
|
57
55
|
runtimeVersion: first?.runtimeVersion ?? null,
|
|
58
|
-
gitSha: first?.gitSha ?? null,
|
|
59
56
|
health: deploymentHealth(healthValues, drainActive),
|
|
60
57
|
workers: workers.length,
|
|
61
58
|
activeWorkers: healthCounts.active,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkerManifest } from '../manifest/types.ts';
|
|
1
2
|
import type { RemoteWorkerCapabilities } from '../protocol.ts';
|
|
2
3
|
/** Lifecycle health state of a connected worker. */
|
|
3
4
|
export type WorkerHealth = 'active' | 'draining' | 'drained';
|
|
@@ -5,6 +6,11 @@ export type WorkerHealth = 'active' | 'draining' | 'drained';
|
|
|
5
6
|
* Full internal state record for a connected worker, held by the registry.
|
|
6
7
|
* `drainReason` and `drainStartedAt` are present only when a worker-level
|
|
7
8
|
* drain marker has been set.
|
|
9
|
+
*
|
|
10
|
+
* `manifest` and `acceptedManifestDigest` are internal — not part of
|
|
11
|
+
* {@link WorkerSummary} or any REST/JSON-RPC surface. Exposing bounded
|
|
12
|
+
* worker/deployment detail from the accepted manifest is a diagnostics
|
|
13
|
+
* concern, not a registration concern.
|
|
8
14
|
*/
|
|
9
15
|
export interface WorkerInfo {
|
|
10
16
|
id: string;
|
|
@@ -19,7 +25,8 @@ export interface WorkerInfo {
|
|
|
19
25
|
deploymentName?: string;
|
|
20
26
|
buildId?: string;
|
|
21
27
|
runtimeVersion?: string;
|
|
22
|
-
|
|
28
|
+
manifest: WorkerManifest;
|
|
29
|
+
acceptedManifestDigest: string;
|
|
23
30
|
drainReason?: string;
|
|
24
31
|
drainStartedAt?: number;
|
|
25
32
|
}
|
|
@@ -32,7 +39,8 @@ export type WorkerRegistrationInfo = {
|
|
|
32
39
|
deploymentName?: string;
|
|
33
40
|
buildId?: string;
|
|
34
41
|
runtimeVersion?: string;
|
|
35
|
-
|
|
42
|
+
manifest: WorkerManifest;
|
|
43
|
+
acceptedManifestDigest: string;
|
|
36
44
|
startedAt?: number;
|
|
37
45
|
capabilities?: RemoteWorkerCapabilities;
|
|
38
46
|
};
|
|
@@ -60,7 +68,6 @@ export type WorkerSummary = {
|
|
|
60
68
|
deploymentName?: string | undefined;
|
|
61
69
|
buildId?: string | undefined;
|
|
62
70
|
runtimeVersion?: string | undefined;
|
|
63
|
-
gitSha?: string | undefined;
|
|
64
71
|
};
|
|
65
72
|
export type WorkerDrainMutationResult = {
|
|
66
73
|
target: 'worker';
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type DeploymentConsistencyResult } from './registry/deployment-consistency.ts';
|
|
2
|
+
import { type RegistrationRejectionEntry } from './registry/rejections.ts';
|
|
1
3
|
import { type WorkerDeploymentSummary } from './registry/summary.ts';
|
|
2
4
|
import type { InFlightTask, RoutingOptions, RoutingPolicy, WorkerDrainMutationResult, WorkerDrainOptions, WorkerInfo, WorkerRegistrationInfo, WorkerRegistryOptions, WorkerSummary } from './registry/types.ts';
|
|
5
|
+
export type { RegistrationRejectionEntry } from './registry/rejections.ts';
|
|
3
6
|
export type { WorkerDeploymentSummary } from './registry/summary.ts';
|
|
4
7
|
export type { InFlightTask, RoutingOptions, RoutingPolicy, WorkerDrainMutationResult, WorkerDrainOptions, WorkerHealth, WorkerInfo, WorkerRegistrationInfo, WorkerRegistryOptions, WorkerSummary, } from './registry/types.ts';
|
|
5
8
|
/**
|
|
@@ -12,9 +15,27 @@ export type { InFlightTask, RoutingOptions, RoutingPolicy, WorkerDrainMutationRe
|
|
|
12
15
|
*
|
|
13
16
|
* @example
|
|
14
17
|
* ```ts
|
|
15
|
-
* import { WorkerRegistry } from '@lostgradient/weft';
|
|
18
|
+
* import { WorkerRegistry, type WorkerManifest } from '@lostgradient/weft';
|
|
19
|
+
*
|
|
20
|
+
* const manifest: WorkerManifest = {
|
|
21
|
+
* manifestVersion: 1,
|
|
22
|
+
* protocolVersion: 3,
|
|
23
|
+
* sdkVersion: '0.18.0',
|
|
24
|
+
* runtime: { name: 'bun', version: '1.3.14' },
|
|
25
|
+
* deployment: { name: 'billing', buildId: 'b3', artifactDigest: 'sha256:41d0' },
|
|
26
|
+
* workflows: {},
|
|
27
|
+
* capabilities: {},
|
|
28
|
+
* };
|
|
29
|
+
*
|
|
16
30
|
* const registry = new WorkerRegistry({ policy: 'least-loaded' });
|
|
17
|
-
* registry.register({
|
|
31
|
+
* registry.register({
|
|
32
|
+
* id: 'worker-1',
|
|
33
|
+
* queue: 'default',
|
|
34
|
+
* activities: ['sendEmail'],
|
|
35
|
+
* concurrency: 10,
|
|
36
|
+
* manifest,
|
|
37
|
+
* acceptedManifestDigest: 'sha256:deadbeef',
|
|
38
|
+
* });
|
|
18
39
|
* const best = registry.findWorker('sendEmail', { queue: 'default' });
|
|
19
40
|
* ```
|
|
20
41
|
*/
|
|
@@ -23,6 +44,16 @@ export declare class WorkerRegistry {
|
|
|
23
44
|
constructor(options?: WorkerRegistryOptions);
|
|
24
45
|
/** The routing policy this registry was configured with. */
|
|
25
46
|
get policy(): RoutingPolicy;
|
|
47
|
+
/**
|
|
48
|
+
* Read-only check of a manifest's `(deploymentName, buildId,
|
|
49
|
+
* artifactDigest)` against digests previously seen for that deployment and
|
|
50
|
+
* build. Callers with a later rejection gate must call
|
|
51
|
+
* {@link recordDeploymentConsistency} only once every such gate has
|
|
52
|
+
* passed, so a declined worker cannot poison a deployment/build slot.
|
|
53
|
+
*/
|
|
54
|
+
checkDeploymentConsistency(deploymentName: string, buildId: string, artifactDigest: string): DeploymentConsistencyResult;
|
|
55
|
+
/** Record `artifactDigest` for `(deploymentName, buildId)` once every rejection gate has passed. */
|
|
56
|
+
recordDeploymentConsistency(deploymentName: string, buildId: string, artifactDigest: string): void;
|
|
26
57
|
/** Register a worker. */
|
|
27
58
|
register(info: WorkerRegistrationInfo): void;
|
|
28
59
|
/** Unregister a worker. Purges fair-share counters and in-flight task entries for this worker. */
|
|
@@ -46,6 +77,16 @@ export declare class WorkerRegistry {
|
|
|
46
77
|
findWorker(activityName: string, options?: RoutingOptions): WorkerInfo | undefined;
|
|
47
78
|
/** Track a task assignment with a visibility timeout deadline. */
|
|
48
79
|
assignTask(workerId: string, operationId: string, visibilityTimeout: number, fairShareKey: string | undefined, attemptToken: string): void;
|
|
80
|
+
/**
|
|
81
|
+
* Undo exactly `assignTask`'s effects — capacity increment, fair-share
|
|
82
|
+
* count, and the in-flight entry — when the durable claim that was
|
|
83
|
+
* supposed to follow the reservation never committed. Distinct from
|
|
84
|
+
* {@link completeTask} even though the body is identical: this path means
|
|
85
|
+
* "the reservation was never realized," not "the task finished," and
|
|
86
|
+
* keeping the names apart stops a reader from mistaking an aborted claim
|
|
87
|
+
* for a normal completion at the call site.
|
|
88
|
+
*/
|
|
89
|
+
releaseReservation(operationId: string): InFlightTask | undefined;
|
|
49
90
|
/** Return tasks whose deadline has passed and remove them from tracking. */
|
|
50
91
|
checkExpiredTasks(now: number): InFlightTask[];
|
|
51
92
|
/**
|
|
@@ -76,6 +117,10 @@ export declare class WorkerRegistry {
|
|
|
76
117
|
getWorker(workerId: string): WorkerInfo | undefined;
|
|
77
118
|
/** Get all registered workers. */
|
|
78
119
|
getAll(): WorkerInfo[];
|
|
120
|
+
/** Record one declined `register` attempt, evicting the oldest once the bounded log is full. */
|
|
121
|
+
recordRejection(entry: RegistrationRejectionEntry): void;
|
|
122
|
+
/** Return the `limit` most recently recorded rejections, newest first. */
|
|
123
|
+
getRecentRejections(limit: number): RegistrationRejectionEntry[];
|
|
79
124
|
/** Mark one connected worker as draining so routing excludes it from new tasks. */
|
|
80
125
|
markWorkerDraining(workerId: string, options?: WorkerDrainOptions): WorkerDrainMutationResult | undefined;
|
|
81
126
|
/** Clear an explicit worker drain marker. Deployment-level drains can still apply. */
|