@worker-protocol/hono 0.1.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Rowing Tech, S.A.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ worker-protocol
2
+ Copyright 2026 Rowing Tech, S.A.
3
+
4
+ This product includes software developed at Rowing Tech, S.A. (https://rowing.tech).
5
+
6
+ Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
7
+
8
+ "worker-protocol" and any conformance claim made in its name are not licensed under Apache-2.0;
9
+ see section 6 of the License.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * The `actions` surface: the call, and everything the protocol fixes about one.
3
+ *
4
+ * A Worker knows what an Action DOES. Everything else on the way in is a rule — the name that is
5
+ * not declared, the body that will not parse, the input that does not match the schema the Worker
6
+ * published, the required idempotency key that is absent, the repeat under a key already recorded,
7
+ * and the key reused with another body.
8
+ *
9
+ * The input is declared as a Zod object and used twice: `mount()` generates the JSON Schema the
10
+ * Descriptor carries from it, and this validates against the same object. ACT-2 requires that a
11
+ * console can render a form from the declaration without being told anything else, and a Worker
12
+ * that wrote the schema by hand and the validation by hand had two places to disagree.
13
+ *
14
+ * **Nothing here knows whether an Action is answering a Task.** It used to: a holder named its
15
+ * Claim in a header and this refused a stale one before performing anything. The lease is
16
+ * withdrawn, so a Response and any other performance are the same request, which `spec/tasks.md`
17
+ * states rather than hides.
18
+ */
19
+ import * as z from "zod";
20
+ import type { Answer, Refusal } from "./worker.ts";
21
+ /**
22
+ * What a Worker declares about one Action, and what it does.
23
+ *
24
+ * ACT-2's `input` and ACT-3's `result` are Zod objects here and JSON Schema in the Descriptor.
25
+ * `mount()` converts them, so nothing is declared twice and nothing can drift.
26
+ */
27
+ export type Action = {
28
+ /** ACT-2. The whole of what a caller sends, and what a console renders a form from. */
29
+ input: z.ZodType;
30
+ /** ACT-3. What a performance answers, absent where it answers nothing (ACT-10's `204`). */
31
+ result?: z.ZodType;
32
+ /** ACT-4. Declared rather than discovered, because a caller decides whether it can wait. */
33
+ completesWithinCall?: boolean;
34
+ /** ACT-12, ENDP-15. Absent where the Action takes no key. */
35
+ idempotency?: {
36
+ required: boolean;
37
+ from: "header";
38
+ windowSeconds: number;
39
+ } | {
40
+ required: boolean;
41
+ from: "input";
42
+ member: string;
43
+ windowSeconds: number;
44
+ };
45
+ /**
46
+ * What this Action does. The input has already been validated against `input` above.
47
+ *
48
+ * A `Refusal` of `unprocessable_content` is ACT-9: schema-valid, and refused on the Worker's own
49
+ * rules. Anything returned that is not a refusal is the result, and `undefined` is ACT-10's
50
+ * `204` — the status comes from what the Action declared, never from what a handler chose.
51
+ */
52
+ run: (input: never, call: ActionCall) => unknown | Promise<unknown>;
53
+ };
54
+ /** What a performance knows about the call it arrived on, beyond its input. */
55
+ export type ActionCall = {
56
+ /** The Action's own name, for a handler shared between several. */
57
+ name: string;
58
+ /** REG-3. The credential presented, for a Worker that splits its own facts by it. */
59
+ token: string | undefined;
60
+ };
61
+ export type ActionDeclarations = Record<string, Action>;
62
+ /**
63
+ * Write an Action, and have `run` typed by the schema above it.
64
+ *
65
+ * `Action.input` is a Zod object and `Action.run` takes what it parses — but a record of Actions
66
+ * cannot carry one type parameter per entry, so annotating the record makes `run`'s argument
67
+ * `unknown` and the author writes the shape a second time, by hand, beside the schema that already
68
+ * states it. Two declarations of one thing, and the day they disagree the compiler says nothing.
69
+ *
70
+ * ```ts
71
+ * accepts: {
72
+ * "record-check": action({
73
+ * input: z.object({ vehicle: z.string(), reachable: z.boolean() }),
74
+ * run: ({ vehicle }) => …, // vehicle is a string, from the line above
75
+ * }),
76
+ * }
77
+ * ```
78
+ *
79
+ * Like `defineWorker`, it returns its argument and no rule id can be cited for it. It is here
80
+ * because the alternative is every Worker author writing every input type twice.
81
+ */
82
+ export declare const action: <I extends z.ZodType>(declaration: Omit<Action, "input" | "run"> & {
83
+ input: I;
84
+ run: (input: z.infer<I>, call: ActionCall) => unknown | Promise<unknown>;
85
+ }) => Action;
86
+ export type ActionFacts = {
87
+ /** ACT-16. Every Action this Worker accepts, keyed by name. */
88
+ accepts: ActionDeclarations;
89
+ /** ACT-15. The document `configure` would accept, where the Worker accepts settings. */
90
+ settings?: () => unknown | Promise<unknown>;
91
+ /**
92
+ * ENDP-16. Where the recorded outcomes live. **Required as soon as any Action declares a key.**
93
+ *
94
+ * There is no default, and the absence is the design. The obvious one is a Map, which is correct
95
+ * in exactly one place — a single long-lived process — and silently wrong everywhere that scales
96
+ * horizontally: a Worker across isolates has one Map per isolate, so a repeat under the same key
97
+ * reaches a process that recorded nothing, the Action is performed a second time *while the
98
+ * caller believes it is protected*, and both calls answer `200` so nobody sees two.
99
+ *
100
+ * A default would have made that the thing you get by not thinking about it, on the platform
101
+ * this protocol's architecture names first. So it is written, in one line, by whoever knows where
102
+ * their Worker runs: `memoryOutcomes()` in a process, a store over a durable object, a KV
103
+ * namespace or a table anywhere else.
104
+ */
105
+ outcomes?: OutcomeStore;
106
+ };
107
+ /**
108
+ * What `begin` answers: the outcome already recorded, somebody else performing it now, or the
109
+ * reservation — which is the only one of the three that performs anything.
110
+ */
111
+ export type Reservation =
112
+ /** ENDP-16. Within the window, this key already has an outcome. It is answered, not performed. */
113
+ {
114
+ held: Recorded;
115
+ }
116
+ /** Another request holds this key and has not finished. Nothing is performed; ENDP-29's `409`. */
117
+ | "in-flight"
118
+ /** Nobody held it. This request performs the Action and calls `complete` or `release`. */
119
+ | "reserved";
120
+ /**
121
+ * Where a Worker keeps what ENDP-16 promised, when a Map will not do.
122
+ *
123
+ * **It reserves rather than reads, and that is the whole shape of it.** A `get` then a `put` with
124
+ * the Action running in between is a check-then-act: two requests arriving at once under one key
125
+ * both find nothing recorded, both perform, and both record. A durable object does not fix it,
126
+ * because the Action runs outside the durable object — between the two calls, which is exactly
127
+ * where the window is. So the first call *takes* the key, and only whoever took it performs.
128
+ *
129
+ * Every store has this in one operation already. A durable object is single-threaded, so reading
130
+ * and writing in one method is atomic by construction. SQL is `insert … on conflict do nothing`,
131
+ * and the rows affected say which of the three happened. A Map is `has` then `set` with nothing
132
+ * between them, because JavaScript does not interleave.
133
+ *
134
+ * `until` travels on `begin` so that a store with its own time-to-live can set it when it reserves
135
+ * — a KV namespace, a durable object alarm — and one without may ignore it: `begin` is asked to
136
+ * treat a record whose window has passed as absent, and `mount()` never sees the difference.
137
+ */
138
+ export type OutcomeStore = {
139
+ /**
140
+ * Take the key, or say what is already there. Nothing is performed unless this answers
141
+ * `"reserved"`, and whoever gets that calls `complete` or `release`.
142
+ *
143
+ * **A reservation expires at `until`, and that is a requirement and not a hint.** A request that
144
+ * dies between `begin` and `complete` — the process evicted, the isolate killed, the machine
145
+ * gone — calls neither, so nothing gives the key back. Without an expiry every later request
146
+ * under it would meet `in-flight` forever, and one crash would lock an Action out permanently
147
+ * over work that never finished. With one, it costs a window. A store treats a reservation whose
148
+ * `until` has passed exactly as it treats one that was never taken.
149
+ */
150
+ begin: (key: string, until: number) => Reservation | Promise<Reservation>;
151
+ /** The Action ran and this is what it answered. The reservation becomes the record. */
152
+ complete: (key: string, held: Recorded) => void | Promise<void>;
153
+ /**
154
+ * The Action did not run, or refused. The reservation is given up so the next caller may take
155
+ * it — a refusal is not an outcome, and a key held by a request that failed would lock the
156
+ * Action out for the whole window over something that never happened.
157
+ */
158
+ release: (key: string) => void | Promise<void>;
159
+ /** Set by `memoryOutcomes` alone, so `mount()` can tell one built per request from a durable one. */
160
+ readonly [IN_MEMORY]?: true;
161
+ };
162
+ /**
163
+ * What marks a store as living in this process and nowhere else.
164
+ *
165
+ * `mount()` uses it for one check it could not otherwise make: a store built INSIDE the function
166
+ * that answers the Worker is a fresh one on every request, which forgets everything between two
167
+ * calls and breaks ENDP-16 exactly as having no store does. Comparing identity would catch that and
168
+ * would also fail a perfectly correct Worker that builds a thin adapter per request over a durable
169
+ * backend — so only a memory store is compared, where a second object is unambiguously the mistake.
170
+ */
171
+ export declare const IN_MEMORY: unique symbol;
172
+ /**
173
+ * ENDP-16. One recorded outcome, for as long as the Action declared.
174
+ *
175
+ * `until` is epoch milliseconds on the Worker's own clock. Nothing compares it against a caller's:
176
+ * ENDP-16 is a promise the Worker makes about its own memory, and the caller only ever learns
177
+ * whether it was kept by sending the request again.
178
+ */
179
+ export type Recorded = {
180
+ body: string;
181
+ answer: Answer;
182
+ until: number;
183
+ };
184
+ /**
185
+ * A store in memory: right in one long-lived process, and wrong everywhere else.
186
+ *
187
+ * It is exported rather than defaulted so that choosing it is a line somebody wrote. A Worker in a
188
+ * single Node or Bun process is the case it is right for, and that case is common enough to
189
+ * deserve the helper and not common enough to deserve the default.
190
+ */
191
+ export declare const memoryOutcomes: () => OutcomeStore;
192
+ export declare function actions(facts: ActionFacts): (name: string, raw: string, key: string | undefined, token: string | undefined) => Promise<Answer | Refusal>;
193
+ /**
194
+ * ACT-2, ACT-3 — the JSON Schema the Descriptor carries, generated from the Zod object beside it.
195
+ *
196
+ * `io: "input"` is what makes a declaration with a default describe what a CALLER sends rather
197
+ * than what the Worker ends up holding, which is the document ACT-2 says a console renders a form
198
+ * from. The two differ exactly where a schema is most likely to have one.
199
+ */
200
+ export declare const jsonSchema: (schema: z.ZodType) => Record<string, unknown>;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The `actions` surface: the call, and everything the protocol fixes about one.
3
+ *
4
+ * A Worker knows what an Action DOES. Everything else on the way in is a rule — the name that is
5
+ * not declared, the body that will not parse, the input that does not match the schema the Worker
6
+ * published, the required idempotency key that is absent, the repeat under a key already recorded,
7
+ * and the key reused with another body.
8
+ *
9
+ * The input is declared as a Zod object and used twice: `mount()` generates the JSON Schema the
10
+ * Descriptor carries from it, and this validates against the same object. ACT-2 requires that a
11
+ * console can render a form from the declaration without being told anything else, and a Worker
12
+ * that wrote the schema by hand and the validation by hand had two places to disagree.
13
+ *
14
+ * **Nothing here knows whether an Action is answering a Task.** It used to: a holder named its
15
+ * Claim in a header and this refused a stale one before performing anything. The lease is
16
+ * withdrawn, so a Response and any other performance are the same request, which `spec/tasks.md`
17
+ * states rather than hides.
18
+ */
19
+ import * as z from "zod";
20
+ /**
21
+ * Write an Action, and have `run` typed by the schema above it.
22
+ *
23
+ * `Action.input` is a Zod object and `Action.run` takes what it parses — but a record of Actions
24
+ * cannot carry one type parameter per entry, so annotating the record makes `run`'s argument
25
+ * `unknown` and the author writes the shape a second time, by hand, beside the schema that already
26
+ * states it. Two declarations of one thing, and the day they disagree the compiler says nothing.
27
+ *
28
+ * ```ts
29
+ * accepts: {
30
+ * "record-check": action({
31
+ * input: z.object({ vehicle: z.string(), reachable: z.boolean() }),
32
+ * run: ({ vehicle }) => …, // vehicle is a string, from the line above
33
+ * }),
34
+ * }
35
+ * ```
36
+ *
37
+ * Like `defineWorker`, it returns its argument and no rule id can be cited for it. It is here
38
+ * because the alternative is every Worker author writing every input type twice.
39
+ */
40
+ export const action = (declaration) => declaration;
41
+ /**
42
+ * What marks a store as living in this process and nowhere else.
43
+ *
44
+ * `mount()` uses it for one check it could not otherwise make: a store built INSIDE the function
45
+ * that answers the Worker is a fresh one on every request, which forgets everything between two
46
+ * calls and breaks ENDP-16 exactly as having no store does. Comparing identity would catch that and
47
+ * would also fail a perfectly correct Worker that builds a thin adapter per request over a durable
48
+ * backend — so only a memory store is compared, where a second object is unambiguously the mistake.
49
+ */
50
+ export const IN_MEMORY = Symbol.for("worker-protocol.outcomes.in-memory");
51
+ const refuse = (code, message) => ({ code, message });
52
+ /**
53
+ * A store in memory: right in one long-lived process, and wrong everywhere else.
54
+ *
55
+ * It is exported rather than defaulted so that choosing it is a line somebody wrote. A Worker in a
56
+ * single Node or Bun process is the case it is right for, and that case is common enough to
57
+ * deserve the helper and not common enough to deserve the default.
58
+ */
59
+ export const memoryOutcomes = () => {
60
+ /**
61
+ * A reservation and a record are one row, told apart by whether an answer arrived.
62
+ *
63
+ * Both carry `until`, and a row past it is treated as absent — which is what gives a reservation
64
+ * its expiry. In one process a crash takes the Map with it, so the expiry earns nothing here; it
65
+ * is written anyway because this is the shape every other store is being asked to implement, and
66
+ * one that quietly did less would be the wrong thing to copy.
67
+ */
68
+ const held = new Map();
69
+ return {
70
+ [IN_MEMORY]: true,
71
+ begin: (key, until) => {
72
+ const row = held.get(key);
73
+ const live = row !== undefined && row.until > Date.now();
74
+ if (live && row.answer !== undefined)
75
+ return { held: row.answer };
76
+ if (live)
77
+ return "in-flight";
78
+ // Nothing runs between the read and the write: a store on a real backend needs one operation
79
+ // for this — a durable object method, an `insert … on conflict do nothing` — and this one
80
+ // needs none, because JavaScript does not interleave here.
81
+ held.set(key, { until });
82
+ return "reserved";
83
+ },
84
+ complete: (key, record) => void held.set(key, { until: record.until, answer: record }),
85
+ release: (key) => void held.delete(key),
86
+ };
87
+ };
88
+ export function actions(facts) {
89
+ // ENDP-16 needs somewhere to record, and `mount()` has refused to build a Worker that declares a
90
+ // key without naming one — so by here it is either given or never asked for.
91
+ const recorded = facts.outcomes;
92
+ return async function perform(name, raw, key, token) {
93
+ // ACT-6: an Action the entry does not declare is a resource that does not exist.
94
+ const declaration = facts.accepts[name];
95
+ if (declaration === undefined) {
96
+ return refuse("not_found", `No Action named ${name} is declared.`);
97
+ }
98
+ // ENDP-4: the body is JSON. ENDP-18: a required key absent is `400`, and it is asked before
99
+ // the body, because an Action that needs protecting must not be performed by the request that
100
+ // forgot to ask for the protection.
101
+ const idempotency = declaration.idempotency;
102
+ if (idempotency?.required === true && idempotency.from === "header" && key === undefined) {
103
+ return refuse("idempotency_key_required", "This Action requires an Idempotency-Key.");
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(raw);
108
+ }
109
+ catch {
110
+ return refuse("malformed_request", "The body did not parse.");
111
+ }
112
+ // ACT-8: an input that does not match the schema this Worker published. `400` and not `422`,
113
+ // because ENDP-12 divides them at whether the Worker could READ it — and a caller fixes this
114
+ // one by re-reading the Descriptor, which ENDP-5's headers already told it had moved.
115
+ const input = declaration.input.safeParse(parsed);
116
+ if (!input.success) {
117
+ const issue = input.error.issues[0];
118
+ const where = issue?.path.join(".");
119
+ return refuse("schema_mismatch", `${where ? `\`${where}\`: ` : ""}${issue?.message ?? "The input does not match the schema."}`);
120
+ }
121
+ // ACT-12: a key read from a named member of the input, where the Action declares one. A
122
+ // payload that already carries its own identity needs no second key beside it.
123
+ const recordedKey = idempotency === undefined
124
+ ? undefined
125
+ : idempotency.from === "header"
126
+ ? key
127
+ : readMember(input.data, idempotency.member);
128
+ if (idempotency?.required === true && recordedKey === undefined) {
129
+ return refuse("idempotency_key_required", "This Action requires an idempotency key.");
130
+ }
131
+ const now = Date.now();
132
+ const keyed = recordedKey === undefined || recorded === undefined || idempotency === undefined
133
+ ? undefined
134
+ : { store: recorded, key: `${name}:${recordedKey}`, idempotency };
135
+ // ENDP-16: the key is TAKEN before the Action runs, not read. Reading and then writing with
136
+ // the Action in between is a check-then-act, and two callers under one key would both perform.
137
+ if (keyed !== undefined) {
138
+ const reservation = await keyed.store.begin(keyed.key, now + keyed.idempotency.windowSeconds * 1000);
139
+ if (reservation === "in-flight") {
140
+ // ENDP-32: `retry` and not `reject`, which is the whole point. The caller backs off and
141
+ // repeats under ENDP-30, and by then the first performance has recorded an outcome, so
142
+ // ENDP-16 answers it. A `reject` would have told the caller to stop — and ENDP-28 would
143
+ // have been right to make it, over a condition that resolves itself in a second.
144
+ return refuse("unavailable", "This idempotency key is being performed right now.");
145
+ }
146
+ if (reservation !== "reserved") {
147
+ // ENDP-17: a key reused with a different body is `409`. Only the caller can tell a retry
148
+ // from a genuine repeat, and this is the Worker declining to guess.
149
+ if (reservation.held.body !== raw) {
150
+ return refuse("idempotency_key_reused", "That key was used with another body.");
151
+ }
152
+ // ENDP-16: within the window, a repeat is not a second performance.
153
+ return reservation.held.answer;
154
+ }
155
+ }
156
+ let produced;
157
+ try {
158
+ produced = await declaration.run(input.data, { name, token });
159
+ }
160
+ catch (thrown) {
161
+ // The key is given back before the failure travels: a reservation held by a request that
162
+ // threw would lock the Action out for the whole window over something that never happened.
163
+ if (keyed !== undefined)
164
+ await keyed.store.release(keyed.key);
165
+ throw thrown;
166
+ }
167
+ if (isRefusal(produced)) {
168
+ if (keyed !== undefined)
169
+ await keyed.store.release(keyed.key);
170
+ return produced;
171
+ }
172
+ // ACT-10, ACT-11: the status comes from what the Action DECLARED, so a caller knows which to
173
+ // expect before it sends and a handler never picks one.
174
+ const answer = declaration.completesWithinCall === false
175
+ ? { status: 202, body: null }
176
+ : declaration.result === undefined
177
+ ? { status: 204, body: null }
178
+ : { status: 200, body: produced };
179
+ if (keyed !== undefined) {
180
+ await keyed.store.complete(keyed.key, {
181
+ body: raw,
182
+ answer,
183
+ until: now + keyed.idempotency.windowSeconds * 1000,
184
+ });
185
+ }
186
+ return answer;
187
+ };
188
+ }
189
+ const isRefusal = (value) => typeof value === "object" && value !== null && "code" in value && "message" in value;
190
+ const readMember = (input, member) => {
191
+ if (typeof input !== "object" || input === null)
192
+ return undefined;
193
+ const value = input[member];
194
+ return value === undefined ? undefined : String(value);
195
+ };
196
+ /**
197
+ * ACT-2, ACT-3 — the JSON Schema the Descriptor carries, generated from the Zod object beside it.
198
+ *
199
+ * `io: "input"` is what makes a declaration with a default describe what a CALLER sends rather
200
+ * than what the Worker ends up holding, which is the document ACT-2 says a console renders a form
201
+ * from. The two differ exactly where a schema is most likely to have one.
202
+ */
203
+ export const jsonSchema = (schema) => z.toJSONSchema(schema, { io: "input", target: "draft-2020-12" });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Bucket boundaries, cut in the time zone a Worker declares.
3
+ *
4
+ * MET-20 cuts every boundary in the zone the `metrics` entry declares, MET-7 makes a week the ISO
5
+ * 8601 one beginning Monday, and MET-13 has each bucket carry an end rather than a duration —
6
+ * because a day across a daylight-saving transition is 23 or 25 hours and a reader comparing
7
+ * against its own clock would otherwise need a calendar.
8
+ *
9
+ * This is the clearest case in the whole SDK for the package carrying what the specification says.
10
+ * A Worker author who wrote this themselves would write it in UTC, it would work, and it would be
11
+ * silently wrong for every consumer in a zone that observes daylight saving — a class of bug that
12
+ * appears twice a year and is attributed to anything but the metric.
13
+ */
14
+ import type { metricGranularity } from "@worker-protocol/schemas";
15
+ import type * as z from "zod";
16
+ export type Granularity = z.infer<typeof metricGranularity>;
17
+ /** MET-20, MET-7 — the start of the bucket an instant falls in. */
18
+ export declare function startOf(at: number, granularity: Granularity, zone: string): number;
19
+ /** MET-13 — the end of a bucket, which is the start of the next one and is carried, not derived. */
20
+ export declare function endOf(start: number, granularity: Granularity, zone: string): number;
21
+ /**
22
+ * MET-11, MET-12 — the whole buckets a half-open interval covers.
23
+ *
24
+ * Whole or not at all: a read starting mid-bucket gets the buckets whose START falls in the
25
+ * interval, so two adjacent reads share a boundary instant and no bucket is answered twice.
26
+ */
27
+ export declare function bucketsIn(from: number, to: number, granularity: Granularity, zone: string): {
28
+ start: number;
29
+ end: number;
30
+ }[];
31
+ /**
32
+ * An RFC 3339 instant carrying an offset, which is what MET-13, TASK-28, ALRT-3 and ACTV-3 travel
33
+ * as. It takes whichever of the two a caller already holds, so that reaching it never costs a
34
+ * `new Date` or a `.getTime()` at the call site.
35
+ */
36
+ export declare const rfc3339: (at: number | Date) => string;