@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.
@@ -0,0 +1,133 @@
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
+ const formatters = new Map();
15
+ const formatter = (zone) => {
16
+ let held = formatters.get(zone);
17
+ if (held === undefined) {
18
+ held = new Intl.DateTimeFormat("en-US", {
19
+ timeZone: zone,
20
+ year: "numeric",
21
+ month: "2-digit",
22
+ day: "2-digit",
23
+ hour: "2-digit",
24
+ minute: "2-digit",
25
+ second: "2-digit",
26
+ hour12: false,
27
+ });
28
+ formatters.set(zone, held);
29
+ }
30
+ return held;
31
+ };
32
+ /** The wall clock an instant reads as, in a zone. */
33
+ function wallClock(at, zone) {
34
+ const read = {};
35
+ for (const part of formatter(zone).formatToParts(new Date(at))) {
36
+ if (part.type !== "literal")
37
+ read[part.type] = part.value;
38
+ }
39
+ return {
40
+ year: Number(read.year),
41
+ // `hour` reads `24` at midnight under some ICU versions, which is the same instant as `0`.
42
+ month: Number(read.month),
43
+ day: Number(read.day),
44
+ hour: Number(read.hour) % 24,
45
+ minute: Number(read.minute),
46
+ };
47
+ }
48
+ /** How far the zone is from UTC at an instant, in milliseconds. */
49
+ const offset = (at, zone) => {
50
+ const local = wallClock(at, zone);
51
+ return Date.UTC(local.year, local.month - 1, local.day, local.hour, local.minute) - at;
52
+ };
53
+ /**
54
+ * The instant a wall clock reads at, in a zone.
55
+ *
56
+ * Two passes, because the offset to subtract is the one in force at the answer rather than at the
57
+ * guess, and the two differ exactly across a transition. An hour that a zone skips has no instant
58
+ * and this lands on the one after it; an hour a zone repeats has two and this takes the first,
59
+ * which is the convention every calendar makes and is worth knowing about rather than discovering.
60
+ */
61
+ function instantOf(parts, zone) {
62
+ const local = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute);
63
+ const first = local - offset(local, zone);
64
+ return local - offset(first, zone);
65
+ }
66
+ /** MET-20, MET-7 — the start of the bucket an instant falls in. */
67
+ export function startOf(at, granularity, zone) {
68
+ const local = wallClock(at, zone);
69
+ const midnight = { ...local, hour: 0, minute: 0 };
70
+ if (granularity === "hour")
71
+ return instantOf({ ...local, minute: 0 }, zone);
72
+ if (granularity === "day")
73
+ return instantOf(midnight, zone);
74
+ if (granularity === "month")
75
+ return instantOf({ ...midnight, day: 1 }, zone);
76
+ if (granularity === "year")
77
+ return instantOf({ ...midnight, month: 1, day: 1 }, zone);
78
+ // MET-7: the ISO 8601 week, beginning Monday. The weekday is read off the local date rather than
79
+ // off the instant, because the two disagree either side of midnight in most of the world.
80
+ const weekday = new Date(Date.UTC(local.year, local.month - 1, local.day)).getUTCDay();
81
+ const back = (weekday + 6) % 7;
82
+ const monday = new Date(Date.UTC(local.year, local.month - 1, local.day - back));
83
+ return instantOf({
84
+ year: monday.getUTCFullYear(),
85
+ month: monday.getUTCMonth() + 1,
86
+ day: monday.getUTCDate(),
87
+ hour: 0,
88
+ minute: 0,
89
+ }, zone);
90
+ }
91
+ /** MET-13 — the end of a bucket, which is the start of the next one and is carried, not derived. */
92
+ export function endOf(start, granularity, zone) {
93
+ const local = wallClock(start, zone);
94
+ const next = granularity === "hour"
95
+ ? { ...local, hour: local.hour + 1 }
96
+ : granularity === "day"
97
+ ? { ...local, day: local.day + 1 }
98
+ : granularity === "week"
99
+ ? { ...local, day: local.day + 7 }
100
+ : granularity === "month"
101
+ ? { ...local, month: local.month + 1 }
102
+ : { ...local, year: local.year + 1 };
103
+ // `Date.UTC` normalises a 32nd of January or a 13th month, so nothing here needs a calendar.
104
+ return instantOf(next, zone);
105
+ }
106
+ /**
107
+ * MET-11, MET-12 — the whole buckets a half-open interval covers.
108
+ *
109
+ * Whole or not at all: a read starting mid-bucket gets the buckets whose START falls in the
110
+ * interval, so two adjacent reads share a boundary instant and no bucket is answered twice.
111
+ */
112
+ export function bucketsIn(from, to, granularity, zone) {
113
+ const buckets = [];
114
+ let start = startOf(from, granularity, zone);
115
+ if (start < from)
116
+ start = endOf(start, granularity, zone);
117
+ // A bound rather than a `while (true)`: a zone or a granularity that failed to advance would
118
+ // otherwise hang a Worker on a request, which is a worse failure than a short answer.
119
+ for (let n = 0; n < 10_000 && start < to; n++) {
120
+ const end = endOf(start, granularity, zone);
121
+ if (end <= start)
122
+ break;
123
+ buckets.push({ start, end });
124
+ start = end;
125
+ }
126
+ return buckets;
127
+ }
128
+ /**
129
+ * An RFC 3339 instant carrying an offset, which is what MET-13, TASK-28, ALRT-3 and ACTV-3 travel
130
+ * as. It takes whichever of the two a caller already holds, so that reaching it never costs a
131
+ * `new Date` or a `.getTime()` at the call site.
132
+ */
133
+ export const rfc3339 = (at) => (at instanceof Date ? at : new Date(at)).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -0,0 +1,201 @@
1
+ /**
2
+ * ENDP-25, ENDP-26 — the closed code vocabulary, with the status each code is answered with.
3
+ *
4
+ * `schemas/error.json` carries the code with its class, which is the half a schema can assert. The
5
+ * status is not in the body, so it lived in a table in `spec/endpoints.md` that
6
+ * `packages/conformance` parsed with a regular expression. It lives here now, and that table is a
7
+ * reading aid — which is what makes the surface declaration normative rather than a convenience.
8
+ *
9
+ * This file carries data and imports nothing, deliberately: `generate-rules.ts` reads it from
10
+ * source, and CI runs `rules:check` before any package is built, so an import of
11
+ * `@worker-protocol/schemas` here would fail on a clean checkout. The class column therefore
12
+ * restates what `rejectCodes` and `retryCodes` in that package already partition; `error.json`
13
+ * and the verifier are what keep the two in step.
14
+ *
15
+ * `as const` is what makes `ErrorCode` a real union rather than `string`: a typo in a code is then
16
+ * a compile error, which is the whole return on a closed vocabulary.
17
+ */
18
+ export declare const CODES: readonly [{
19
+ readonly code: "malformed_request";
20
+ readonly status: 400;
21
+ readonly class: "reject";
22
+ readonly condition: "A body that will not parse, or a content type that is not `application/json`.";
23
+ }, {
24
+ readonly code: "schema_mismatch";
25
+ readonly status: 400;
26
+ readonly class: "reject";
27
+ readonly condition: "A body that parses and does not match the schema the surface declared.";
28
+ }, {
29
+ readonly code: "invalid_parameter";
30
+ readonly status: 400;
31
+ readonly class: "reject";
32
+ readonly condition: "A parameter missing or malformed.";
33
+ }, {
34
+ readonly code: "unknown_filter";
35
+ readonly status: 400;
36
+ readonly class: "reject";
37
+ readonly condition: "A filter parameter the Worker does not recognize.";
38
+ }, {
39
+ readonly code: "unsupported_version";
40
+ readonly status: 400;
41
+ readonly class: "reject";
42
+ readonly condition: "A requested Capability version the Worker cannot answer.";
43
+ }, {
44
+ readonly code: "idempotency_key_required";
45
+ readonly status: 400;
46
+ readonly class: "reject";
47
+ readonly condition: "An Action requires a key and none was sent.";
48
+ }, {
49
+ readonly code: "unauthenticated";
50
+ readonly status: 401;
51
+ readonly class: "reject";
52
+ readonly condition: "No credential, or one the Worker cannot read.";
53
+ }, {
54
+ readonly code: "forbidden";
55
+ readonly status: 403;
56
+ readonly class: "reject";
57
+ readonly condition: "The credential is understood and does not carry the right.";
58
+ }, {
59
+ readonly code: "not_found";
60
+ readonly status: 404;
61
+ readonly class: "reject";
62
+ readonly condition: "No such address, or no such resource.";
63
+ }, {
64
+ readonly code: "request_timeout";
65
+ readonly status: 408;
66
+ readonly class: "retry";
67
+ readonly condition: "The request did not arrive in time to be answered.";
68
+ }, {
69
+ readonly code: "conflict";
70
+ readonly status: 409;
71
+ readonly class: "reject";
72
+ readonly condition: "The request conflicts with the current state.";
73
+ }, {
74
+ readonly code: "idempotency_key_reused";
75
+ readonly status: 409;
76
+ readonly class: "reject";
77
+ readonly condition: "A key reused with a different body.";
78
+ }, {
79
+ readonly code: "unprocessable_content";
80
+ readonly status: 422;
81
+ readonly class: "reject";
82
+ readonly condition: "Well-formed, schema-valid, and refused on the Worker's own rules.";
83
+ }, {
84
+ readonly code: "rate_limited";
85
+ readonly status: 429;
86
+ readonly class: "retry";
87
+ readonly condition: "Too many requests. Carries `Retry-After`.";
88
+ }, {
89
+ readonly code: "internal_error";
90
+ readonly status: 500;
91
+ readonly class: "retry";
92
+ readonly condition: "The Worker failed for its own reasons.";
93
+ }, {
94
+ readonly code: "upstream_error";
95
+ readonly status: 502;
96
+ readonly class: "retry";
97
+ readonly condition: "Something the Worker depends on answered badly.";
98
+ }, {
99
+ readonly code: "unavailable";
100
+ readonly status: 503;
101
+ readonly class: "retry";
102
+ readonly condition: "Starting, `unhealthy`, or a dependency down.";
103
+ }, {
104
+ readonly code: "upstream_timeout";
105
+ readonly status: 504;
106
+ readonly class: "retry";
107
+ readonly condition: "Something the Worker depends on did not answer in time.";
108
+ }];
109
+ export type ErrorCode = (typeof CODES)[number]["code"];
110
+ /** One row by its code: the status it fixes and the class it carries (ENDP-26). */
111
+ export declare const byCode: Map<"conflict" | "forbidden" | "idempotency_key_required" | "idempotency_key_reused" | "internal_error" | "invalid_parameter" | "malformed_request" | "not_found" | "rate_limited" | "request_timeout" | "schema_mismatch" | "unauthenticated" | "unavailable" | "unknown_filter" | "unprocessable_content" | "unsupported_version" | "upstream_error" | "upstream_timeout", {
112
+ readonly code: "malformed_request";
113
+ readonly status: 400;
114
+ readonly class: "reject";
115
+ readonly condition: "A body that will not parse, or a content type that is not `application/json`.";
116
+ } | {
117
+ readonly code: "schema_mismatch";
118
+ readonly status: 400;
119
+ readonly class: "reject";
120
+ readonly condition: "A body that parses and does not match the schema the surface declared.";
121
+ } | {
122
+ readonly code: "invalid_parameter";
123
+ readonly status: 400;
124
+ readonly class: "reject";
125
+ readonly condition: "A parameter missing or malformed.";
126
+ } | {
127
+ readonly code: "unknown_filter";
128
+ readonly status: 400;
129
+ readonly class: "reject";
130
+ readonly condition: "A filter parameter the Worker does not recognize.";
131
+ } | {
132
+ readonly code: "unsupported_version";
133
+ readonly status: 400;
134
+ readonly class: "reject";
135
+ readonly condition: "A requested Capability version the Worker cannot answer.";
136
+ } | {
137
+ readonly code: "idempotency_key_required";
138
+ readonly status: 400;
139
+ readonly class: "reject";
140
+ readonly condition: "An Action requires a key and none was sent.";
141
+ } | {
142
+ readonly code: "unauthenticated";
143
+ readonly status: 401;
144
+ readonly class: "reject";
145
+ readonly condition: "No credential, or one the Worker cannot read.";
146
+ } | {
147
+ readonly code: "forbidden";
148
+ readonly status: 403;
149
+ readonly class: "reject";
150
+ readonly condition: "The credential is understood and does not carry the right.";
151
+ } | {
152
+ readonly code: "not_found";
153
+ readonly status: 404;
154
+ readonly class: "reject";
155
+ readonly condition: "No such address, or no such resource.";
156
+ } | {
157
+ readonly code: "request_timeout";
158
+ readonly status: 408;
159
+ readonly class: "retry";
160
+ readonly condition: "The request did not arrive in time to be answered.";
161
+ } | {
162
+ readonly code: "conflict";
163
+ readonly status: 409;
164
+ readonly class: "reject";
165
+ readonly condition: "The request conflicts with the current state.";
166
+ } | {
167
+ readonly code: "idempotency_key_reused";
168
+ readonly status: 409;
169
+ readonly class: "reject";
170
+ readonly condition: "A key reused with a different body.";
171
+ } | {
172
+ readonly code: "unprocessable_content";
173
+ readonly status: 422;
174
+ readonly class: "reject";
175
+ readonly condition: "Well-formed, schema-valid, and refused on the Worker's own rules.";
176
+ } | {
177
+ readonly code: "rate_limited";
178
+ readonly status: 429;
179
+ readonly class: "retry";
180
+ readonly condition: "Too many requests. Carries `Retry-After`.";
181
+ } | {
182
+ readonly code: "internal_error";
183
+ readonly status: 500;
184
+ readonly class: "retry";
185
+ readonly condition: "The Worker failed for its own reasons.";
186
+ } | {
187
+ readonly code: "upstream_error";
188
+ readonly status: 502;
189
+ readonly class: "retry";
190
+ readonly condition: "Something the Worker depends on answered badly.";
191
+ } | {
192
+ readonly code: "unavailable";
193
+ readonly status: 503;
194
+ readonly class: "retry";
195
+ readonly condition: "Starting, `unhealthy`, or a dependency down.";
196
+ } | {
197
+ readonly code: "upstream_timeout";
198
+ readonly status: 504;
199
+ readonly class: "retry";
200
+ readonly condition: "Something the Worker depends on did not answer in time.";
201
+ }>;
package/dist/codes.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * ENDP-25, ENDP-26 — the closed code vocabulary, with the status each code is answered with.
3
+ *
4
+ * `schemas/error.json` carries the code with its class, which is the half a schema can assert. The
5
+ * status is not in the body, so it lived in a table in `spec/endpoints.md` that
6
+ * `packages/conformance` parsed with a regular expression. It lives here now, and that table is a
7
+ * reading aid — which is what makes the surface declaration normative rather than a convenience.
8
+ *
9
+ * This file carries data and imports nothing, deliberately: `generate-rules.ts` reads it from
10
+ * source, and CI runs `rules:check` before any package is built, so an import of
11
+ * `@worker-protocol/schemas` here would fail on a clean checkout. The class column therefore
12
+ * restates what `rejectCodes` and `retryCodes` in that package already partition; `error.json`
13
+ * and the verifier are what keep the two in step.
14
+ *
15
+ * `as const` is what makes `ErrorCode` a real union rather than `string`: a typo in a code is then
16
+ * a compile error, which is the whole return on a closed vocabulary.
17
+ */
18
+ export const CODES = [
19
+ {
20
+ code: "malformed_request",
21
+ status: 400,
22
+ class: "reject",
23
+ condition: "A body that will not parse, or a content type that is not `application/json`.",
24
+ },
25
+ {
26
+ code: "schema_mismatch",
27
+ status: 400,
28
+ class: "reject",
29
+ condition: "A body that parses and does not match the schema the surface declared.",
30
+ },
31
+ {
32
+ code: "invalid_parameter",
33
+ status: 400,
34
+ class: "reject",
35
+ condition: "A parameter missing or malformed.",
36
+ },
37
+ {
38
+ code: "unknown_filter",
39
+ status: 400,
40
+ class: "reject",
41
+ condition: "A filter parameter the Worker does not recognize.",
42
+ },
43
+ {
44
+ code: "unsupported_version",
45
+ status: 400,
46
+ class: "reject",
47
+ condition: "A requested Capability version the Worker cannot answer.",
48
+ },
49
+ {
50
+ code: "idempotency_key_required",
51
+ status: 400,
52
+ class: "reject",
53
+ condition: "An Action requires a key and none was sent.",
54
+ },
55
+ {
56
+ code: "unauthenticated",
57
+ status: 401,
58
+ class: "reject",
59
+ condition: "No credential, or one the Worker cannot read.",
60
+ },
61
+ {
62
+ code: "forbidden",
63
+ status: 403,
64
+ class: "reject",
65
+ condition: "The credential is understood and does not carry the right.",
66
+ },
67
+ {
68
+ code: "not_found",
69
+ status: 404,
70
+ class: "reject",
71
+ condition: "No such address, or no such resource.",
72
+ },
73
+ {
74
+ code: "request_timeout",
75
+ status: 408,
76
+ class: "retry",
77
+ condition: "The request did not arrive in time to be answered.",
78
+ },
79
+ {
80
+ code: "conflict",
81
+ status: 409,
82
+ class: "reject",
83
+ condition: "The request conflicts with the current state.",
84
+ },
85
+ {
86
+ code: "idempotency_key_reused",
87
+ status: 409,
88
+ class: "reject",
89
+ condition: "A key reused with a different body.",
90
+ },
91
+ {
92
+ code: "unprocessable_content",
93
+ status: 422,
94
+ class: "reject",
95
+ condition: "Well-formed, schema-valid, and refused on the Worker's own rules.",
96
+ },
97
+ {
98
+ code: "rate_limited",
99
+ status: 429,
100
+ class: "retry",
101
+ condition: "Too many requests. Carries `Retry-After`.",
102
+ },
103
+ {
104
+ code: "internal_error",
105
+ status: 500,
106
+ class: "retry",
107
+ condition: "The Worker failed for its own reasons.",
108
+ },
109
+ {
110
+ code: "upstream_error",
111
+ status: 502,
112
+ class: "retry",
113
+ condition: "Something the Worker depends on answered badly.",
114
+ },
115
+ {
116
+ code: "unavailable",
117
+ status: 503,
118
+ class: "retry",
119
+ condition: "Starting, `unhealthy`, or a dependency down.",
120
+ },
121
+ {
122
+ code: "upstream_timeout",
123
+ status: 504,
124
+ class: "retry",
125
+ condition: "Something the Worker depends on did not answer in time.",
126
+ },
127
+ ];
128
+ /** One row by its code: the status it fixes and the class it carries (ENDP-26). */
129
+ export const byCode = new Map(CODES.map((c) => [c.code, c]));
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The page envelope every collection surface answers in, in one place.
3
+ *
4
+ * ENDP-19's cap, ENDP-20's envelope and its absent cursor, ENDP-21's opaque cursor and ENDP-23's
5
+ * order are the protocol's, not a Worker's, and they were written out three times: once inside
6
+ * `tasks.ts`, and once inline per handler for `alerts` and `activity` — which carried neither the
7
+ * cursor nor the cap, while `openapi/` published a `cursor` parameter both of them refused with
8
+ * `unknown_filter`. A document that describes a call the Worker will not take is the fault this
9
+ * repository names everywhere else, and one implementation is what stops it recurring.
10
+ *
11
+ * What a surface still owns is what it filters on and what its items look like. This carries the
12
+ * rest.
13
+ */
14
+ import type { Refusal } from "./worker.ts";
15
+ /** One page: the items as they travel, and ENDP-20's cursor where there is more. */
16
+ export type Page = {
17
+ items: Record<string, unknown>[];
18
+ nextCursor?: string;
19
+ };
20
+ /** ENDP-19 (recommended). A Worker caps rather than negotiating; this is the cap when none is set. */
21
+ export declare const DEFAULT_PAGE_SIZE = 50;
22
+ /**
23
+ * One page of a collection, ordered, cut and serialized.
24
+ *
25
+ * The cursor is an offset spelled as a string, which satisfies ENDP-21 without pretending to more:
26
+ * it is produced only here, a caller sends back what it was given, and anything else is refused as
27
+ * a parameter this Worker did not mint. ENDP-23's order is by `id` — minted by the Worker and the
28
+ * one field every collection here has that nothing else reorders — so paging terminates.
29
+ */
30
+ export declare function collection<T extends {
31
+ id: string;
32
+ since: Date;
33
+ }>(items: T[], query: URLSearchParams, serialize: (one: T) => Record<string, unknown>, pageSize?: number,
34
+ /** What this surface defines beyond the cursor — TASK-8's `type` is the only one today. */
35
+ filters?: readonly string[]): Refusal | Page;
36
+ /**
37
+ * The instant every item in this protocol carries, serialized once.
38
+ *
39
+ * TASK-28, ALRT-3 and ACTV-3 all fix an RFC 3339 instant with an offset, and a Worker hands each
40
+ * of them a `Date` — so no Worker writes the format and no surface here writes it twice.
41
+ */
42
+ export declare const serializeSince: <T extends {
43
+ since: Date;
44
+ }>(one: T) => Record<string, unknown>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The page envelope every collection surface answers in, in one place.
3
+ *
4
+ * ENDP-19's cap, ENDP-20's envelope and its absent cursor, ENDP-21's opaque cursor and ENDP-23's
5
+ * order are the protocol's, not a Worker's, and they were written out three times: once inside
6
+ * `tasks.ts`, and once inline per handler for `alerts` and `activity` — which carried neither the
7
+ * cursor nor the cap, while `openapi/` published a `cursor` parameter both of them refused with
8
+ * `unknown_filter`. A document that describes a call the Worker will not take is the fault this
9
+ * repository names everywhere else, and one implementation is what stops it recurring.
10
+ *
11
+ * What a surface still owns is what it filters on and what its items look like. This carries the
12
+ * rest.
13
+ */
14
+ import { rfc3339 } from "./buckets.js";
15
+ const refuse = (code, message) => ({ code, message });
16
+ /** ENDP-19 (recommended). A Worker caps rather than negotiating; this is the cap when none is set. */
17
+ export const DEFAULT_PAGE_SIZE = 50;
18
+ /**
19
+ * One page of a collection, ordered, cut and serialized.
20
+ *
21
+ * The cursor is an offset spelled as a string, which satisfies ENDP-21 without pretending to more:
22
+ * it is produced only here, a caller sends back what it was given, and anything else is refused as
23
+ * a parameter this Worker did not mint. ENDP-23's order is by `id` — minted by the Worker and the
24
+ * one field every collection here has that nothing else reorders — so paging terminates.
25
+ */
26
+ export function collection(items, query, serialize, pageSize = DEFAULT_PAGE_SIZE,
27
+ /** What this surface defines beyond the cursor — TASK-8's `type` is the only one today. */
28
+ filters = []) {
29
+ // ENDP-24: an unrecognized filter is `400` and is never ignored. A filter dropped in silence
30
+ // answers with MORE than the caller asked for, in a shape it will happily parse. It is checked
31
+ // here rather than by a middleware because a surface that reads a cursor has to opt out of the
32
+ // one that refuses every parameter, and opting out must not mean opting out of this.
33
+ const defined = new Set(["cursor", ...filters]);
34
+ for (const key of query.keys()) {
35
+ if (!defined.has(key)) {
36
+ return refuse("unknown_filter", `This address takes no parameter named ${key}.`);
37
+ }
38
+ }
39
+ const from = Number(query.get("cursor") ?? "0");
40
+ if (!Number.isInteger(from) || from < 0) {
41
+ return refuse("invalid_parameter", "That cursor was not produced by this Worker.");
42
+ }
43
+ // Mapped before sorted, so the copy the ordering needs is the one the serialization already made
44
+ // — `sort` mutates, and the array handed in is the Worker's own.
45
+ const ordered = items
46
+ .map((one) => ({ id: one.id, row: serialize(one) }))
47
+ .sort((a, b) => a.id.localeCompare(b.id));
48
+ const next = from + pageSize;
49
+ const page = ordered.slice(from, next).map((one) => one.row);
50
+ // ENDP-20: the cursor is absent at the end of the collection — absent, not null.
51
+ return next < ordered.length ? { items: page, nextCursor: String(next) } : { items: page };
52
+ }
53
+ /**
54
+ * The instant every item in this protocol carries, serialized once.
55
+ *
56
+ * TASK-28, ALRT-3 and ACTV-3 all fix an RFC 3339 instant with an offset, and a Worker hands each
57
+ * of them a `Date` — so no Worker writes the format and no surface here writes it twice.
58
+ */
59
+ export const serializeSince = (one) => ({
60
+ ...one,
61
+ since: rfc3339(one.since),
62
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@worker-protocol/hono` — the protocol's surface as Hono routes, and `mount()`.
3
+ *
4
+ * Two things, and the second exists because of the first. The routes in `surfaces.ts` are the
5
+ * declaration `openapi/` is generated from, and they are `createRoute` objects rather than data of
6
+ * this repository's own because most Workers built on this protocol run on Hono: the declaration
7
+ * that runs is the one that generates, and nothing is declared twice. `mount()` is what makes them
8
+ * run — a Worker author implements `Worker` and gets every address, header, envelope, refusal,
9
+ * page and bucket boundary this protocol fixes, once, the same way in every Worker.
10
+ *
11
+ * This package carries behaviour, and `packages/README.md` says what standing that has: what is
12
+ * forbidden is behaviour of its OWN — something `spec/` does not say — and what is wanted is all of
13
+ * what `spec/` does say, because the alternative is every Worker deriving the same rules again.
14
+ * Every line here cites the rule it carries. `conformance/reference-worker` is `mount()` over an
15
+ * implementation arranged to be checked, and `@worker-protocol/conformance` passing against it is
16
+ * what vouches for this package; `examples/minimal-worker` is what says it is cheap.
17
+ */
18
+ export type { Action, ActionCall, ActionDeclarations, ActionFacts, OutcomeStore, Recorded, Reservation, } from "./actions.ts";
19
+ export { action, jsonSchema, memoryOutcomes } from "./actions.ts";
20
+ export { bucketsIn, endOf, type Granularity, rfc3339, startOf } from "./buckets.ts";
21
+ export { CODES, type ErrorCode } from "./codes.ts";
22
+ export type { Bucket, MetricFacts, MetricQuery, MetricSample } from "./metrics.ts";
23
+ export { defineWorker, type ExecutionCtx, mount, type WorkerBuilder, type WorkerSource, } from "./mount.ts";
24
+ export { performAction, pollHealth, readActivity, readAlerts, readDescriptor, readMetric, readTasks, } from "./surfaces.ts";
25
+ export type { OpenTask, TaskFacts, TaskTypes } from "./tasks.ts";
26
+ export type { Activity, Alert, Answer, Refusal, SkillDeclaration, Worker } from "./worker.ts";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@worker-protocol/hono` — the protocol's surface as Hono routes, and `mount()`.
3
+ *
4
+ * Two things, and the second exists because of the first. The routes in `surfaces.ts` are the
5
+ * declaration `openapi/` is generated from, and they are `createRoute` objects rather than data of
6
+ * this repository's own because most Workers built on this protocol run on Hono: the declaration
7
+ * that runs is the one that generates, and nothing is declared twice. `mount()` is what makes them
8
+ * run — a Worker author implements `Worker` and gets every address, header, envelope, refusal,
9
+ * page and bucket boundary this protocol fixes, once, the same way in every Worker.
10
+ *
11
+ * This package carries behaviour, and `packages/README.md` says what standing that has: what is
12
+ * forbidden is behaviour of its OWN — something `spec/` does not say — and what is wanted is all of
13
+ * what `spec/` does say, because the alternative is every Worker deriving the same rules again.
14
+ * Every line here cites the rule it carries. `conformance/reference-worker` is `mount()` over an
15
+ * implementation arranged to be checked, and `@worker-protocol/conformance` passing against it is
16
+ * what vouches for this package; `examples/minimal-worker` is what says it is cheap.
17
+ */
18
+ export { action, jsonSchema, memoryOutcomes } from "./actions.js";
19
+ export { bucketsIn, endOf, rfc3339, startOf } from "./buckets.js";
20
+ export { CODES } from "./codes.js";
21
+ export { defineWorker, mount, } from "./mount.js";
22
+ export { performAction, pollHealth, readActivity, readAlerts, readDescriptor, readMetric, readTasks, } from "./surfaces.js";