@worker-protocol/hono 0.1.0 → 0.1.1
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 +155 -0
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @worker-protocol/hono
|
|
2
|
+
|
|
3
|
+
The protocol's surface as Hono routes, and `mount()`: implement an interface, and get every address,
|
|
4
|
+
header, envelope and refusal worker-protocol fixes.
|
|
5
|
+
|
|
6
|
+
**worker-protocol is an open specification for Workers that can be seen, operated and given work by
|
|
7
|
+
people who did not build them.** HTTP and JSON Schema, no runtime. A *Worker* describes itself in a
|
|
8
|
+
Descriptor served at `/.well-known/worker-protocol` — the one address this protocol fixes — and
|
|
9
|
+
declares there which of its Capabilities it implements: `health`, `metrics`, `actions`, `alerts`,
|
|
10
|
+
`activity`, `nudges`, `tasks`, `events`. Any combination is allowed, including none.
|
|
11
|
+
|
|
12
|
+
This package is what makes complying cheap. You write what your Worker *is* — what it knows how to
|
|
13
|
+
do, what it counts, which conditions hold, what it is working on — and `mount()` writes the rest,
|
|
14
|
+
once, the same way in every Worker.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm i @worker-protocol/hono hono zod
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`hono` (`^4.13.7`) and `zod` (`^4.5.4`) are peer dependencies, so your app and this package share
|
|
23
|
+
one instance of each.
|
|
24
|
+
|
|
25
|
+
## A Worker
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { action, defineWorker, memoryOutcomes, mount, type OpenTask } from "@worker-protocol/hono";
|
|
29
|
+
import * as z from "zod";
|
|
30
|
+
|
|
31
|
+
const SILENT_VEHICLE = "tech.rowing.fleet.check-silent-vehicle";
|
|
32
|
+
|
|
33
|
+
const silent = new Map<string, Date>();
|
|
34
|
+
const checked = new Set<string>();
|
|
35
|
+
|
|
36
|
+
// Where a repeat under one idempotency key finds the outcome the first call recorded. Built out
|
|
37
|
+
// here, not inside the Worker below: that is answered per request, and a store built there would
|
|
38
|
+
// forget what the last one recorded while the caller believed the repeat was protected.
|
|
39
|
+
const outcomes = memoryOutcomes();
|
|
40
|
+
|
|
41
|
+
export const fleetWorker = defineWorker<{ CREDENTIAL?: string }>((env) => ({
|
|
42
|
+
// Not the URL, and not derived from it: moving this Worker to another host must not make it
|
|
43
|
+
// another Worker.
|
|
44
|
+
id: "tech.rowing.fleet.watcher",
|
|
45
|
+
|
|
46
|
+
authenticate: (token) => (token === env.CREDENTIAL ? "accepted" : "unauthenticated"),
|
|
47
|
+
|
|
48
|
+
health: () => ({
|
|
49
|
+
status: silent.size > 100 ? "degraded" : "healthy",
|
|
50
|
+
checks: { store: { status: "healthy", detail: `${silent.size} vehicles tracked` } },
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
actions: {
|
|
54
|
+
outcomes,
|
|
55
|
+
accepts: {
|
|
56
|
+
"answer-check": action({
|
|
57
|
+
input: z.object({ vehicle: z.string().min(1), reachable: z.boolean() }),
|
|
58
|
+
result: z.object({ recordedAt: z.string() }),
|
|
59
|
+
// A repeat under the caller's key replays the first answer instead of doing the work
|
|
60
|
+
// again, against the store named above.
|
|
61
|
+
idempotency: { required: true, from: "header", windowSeconds: 3600 },
|
|
62
|
+
run: ({ vehicle }) => {
|
|
63
|
+
checked.add(vehicle);
|
|
64
|
+
return { recordedAt: new Date().toISOString() };
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
tasks: {
|
|
71
|
+
// What this Worker sends with a Task of this type, and the one operation of its own that
|
|
72
|
+
// answers it. Whoever does the work never has to be told where to send the answer.
|
|
73
|
+
raises: {
|
|
74
|
+
[SILENT_VEHICLE]: {
|
|
75
|
+
payload: z.object({ vehicle: z.string() }),
|
|
76
|
+
answeredBy: "answer-check",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
// The condition, and the whole of what this Worker owes. A Task exists while its vehicle is
|
|
80
|
+
// quiet and unchecked, and it stops existing when that stops being true: nobody closes one,
|
|
81
|
+
// so nothing here can be left open by a consumer that crashed.
|
|
82
|
+
current: (): OpenTask[] =>
|
|
83
|
+
[...silent.entries()]
|
|
84
|
+
.filter(([vehicle]) => !checked.has(vehicle))
|
|
85
|
+
.map(([vehicle, since]) => ({ id: `silent:${vehicle}`, type: SILENT_VEHICLE, payload: { vehicle }, since })),
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
export const app = mount(fleetWorker);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`app` is an `OpenAPIHono`, so `app.fetch` is what every platform wants: `export default { fetch:
|
|
93
|
+
app.fetch }` on Cloudflare, Vercel edge and Deno Deploy; `serve({ fetch: app.fetch })` on Node, Bun
|
|
94
|
+
and Deno. Writing the Worker as a function of its environment is what makes the same file run in all
|
|
95
|
+
of them — bindings and secrets arrive per request on an edge platform and are ambient on a server,
|
|
96
|
+
and a Worker that reads them at module scope works in one place only.
|
|
97
|
+
|
|
98
|
+
## What `mount()` carries, so your Worker does not
|
|
99
|
+
|
|
100
|
+
- Every address, derived from the Capabilities you declared, and the Descriptor that publishes them.
|
|
101
|
+
- The Descriptor itself, including the JSON Schema of each Action's input and each Task type's
|
|
102
|
+
payload, written from the Zod objects you handed in — so what a console renders a form from and
|
|
103
|
+
what your Worker accepts cannot drift apart.
|
|
104
|
+
- The error envelope and which code answers which refusal, the difference between a `reject` a
|
|
105
|
+
caller must not repeat and a `retry` it should.
|
|
106
|
+
- The page envelope, its cursor and the order a page is returned in.
|
|
107
|
+
- Metric bucket boundaries, cut in the time zone your Worker declared.
|
|
108
|
+
- The idempotency window: a repeat under a key replays the recorded outcome instead of performing
|
|
109
|
+
the work twice.
|
|
110
|
+
- Authentication, as `Authorization: Bearer <token>` on every address, answered by your
|
|
111
|
+
`authenticate`; and the refusal of a Nudge for a Task type you declare no Skill for.
|
|
112
|
+
|
|
113
|
+
What it never does is anything the specification leaves to you: what a Task's condition is, what an
|
|
114
|
+
Action does, what a number means.
|
|
115
|
+
|
|
116
|
+
## Also exported
|
|
117
|
+
|
|
118
|
+
`action()` and `jsonSchema()` for declaring an Action; `memoryOutcomes()` as an outcome store for a
|
|
119
|
+
single long-lived process (an edge deployment wants a durable one — a `Map` per isolate has the same
|
|
120
|
+
problem one step further out); `CODES` and `ErrorCode`; `bucketsIn`, `startOf`, `endOf`, `rfc3339`
|
|
121
|
+
for metric boundaries; and the route objects themselves — `readDescriptor`, `pollHealth`,
|
|
122
|
+
`readMetric`, `performAction`, `readTasks`, `readAlerts`, `readActivity` — which are the declaration
|
|
123
|
+
the protocol's OpenAPI documents are generated from.
|
|
124
|
+
|
|
125
|
+
Types: `Worker`, `Activity`, `Alert`, `Answer`, `Refusal`, `SkillDeclaration`, `OpenTask`,
|
|
126
|
+
`TaskFacts`, `TaskTypes`, `MetricFacts`, `MetricQuery`, `MetricSample`, `Bucket`, `ActionFacts`,
|
|
127
|
+
`ActionDeclarations`, `OutcomeStore`, `Reservation`, `Recorded`, `WorkerSource`, `WorkerBuilder`,
|
|
128
|
+
`ExecutionCtx`.
|
|
129
|
+
|
|
130
|
+
## What standing this package has
|
|
131
|
+
|
|
132
|
+
**Nothing in it carries behavior of its *own*.** What is forbidden is a package doing something the
|
|
133
|
+
specification does not say — that is how a package becomes the standard and the text starts to rot.
|
|
134
|
+
What is wanted is a package carrying everything the specification *does* say, because the
|
|
135
|
+
alternative is every Worker author deriving the same rules again and the ones who get a detail wrong
|
|
136
|
+
being non-conformant in a way only a verifier ever finds. Every line here cites the rule id it
|
|
137
|
+
carries. If `mount()` does something no rule requires, that is the bug.
|
|
138
|
+
|
|
139
|
+
What vouches for it is not this README: it is `@worker-protocol/conformance` run against a Worker
|
|
140
|
+
built on it.
|
|
141
|
+
|
|
142
|
+
## Related packages
|
|
143
|
+
|
|
144
|
+
- `@worker-protocol/schemas` — the Zod objects that generate the normative JSON Schemas.
|
|
145
|
+
- `@worker-protocol/client` — `consume()`: read a Worker, and take work from it. A consumer is not a
|
|
146
|
+
server and installs no web framework.
|
|
147
|
+
- `@worker-protocol/conformance` — point it at a Worker's base URL, get a report of what it complies
|
|
148
|
+
with.
|
|
149
|
+
|
|
150
|
+
## License and name
|
|
151
|
+
|
|
152
|
+
Apache-2.0, patent grant included — implement the protocol in any product, commercial or not,
|
|
153
|
+
without asking anyone. The name is not part of that grant (Apache-2.0 §6): a claim that something
|
|
154
|
+
*speaks worker-protocol* is one this project vouches for, and the conformance tool is how it is
|
|
155
|
+
earned.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worker-protocol/hono",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"workerProtocolEdition": "0.1",
|
|
5
5
|
"description": "The protocol's surface as Hono routes, and mount(): a Worker author implements an interface and gets every address, header and refusal this protocol fixes",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/rowing-tech/worker-protocol.git",
|
|
10
10
|
"directory": "packages/hono"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.com/rowing-tech/worker-protocol#readme",
|
|
12
|
+
"homepage": "https://github.com/rowing-tech/worker-protocol/tree/main/packages/hono#readme",
|
|
13
13
|
"bugs": "https://github.com/rowing-tech/worker-protocol/issues",
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@hono/zod-openapi": "1.6.3",
|
|
33
|
-
"@worker-protocol/schemas": "0.1.
|
|
33
|
+
"@worker-protocol/schemas": "0.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"hono": "^4.13.7",
|