@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 +201 -0
- package/NOTICE +9 -0
- package/dist/actions.d.ts +200 -0
- package/dist/actions.js +203 -0
- package/dist/buckets.d.ts +36 -0
- package/dist/buckets.js +133 -0
- package/dist/codes.d.ts +201 -0
- package/dist/codes.js +129 -0
- package/dist/collection.d.ts +44 -0
- package/dist/collection.js +62 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +22 -0
- package/dist/metrics.d.ts +56 -0
- package/dist/metrics.js +122 -0
- package/dist/mount.d.ts +92 -0
- package/dist/mount.js +432 -0
- package/dist/surfaces.d.ts +483 -0
- package/dist/surfaces.js +424 -0
- package/dist/tasks.d.ts +78 -0
- package/dist/tasks.js +45 -0
- package/dist/worker.d.ts +169 -0
- package/dist/worker.js +1 -0
- package/package.json +51 -0
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>;
|
package/dist/actions.js
ADDED
|
@@ -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;
|