@worker-protocol/cloudflare 0.0.0-stage → 0.6.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/README.md +191 -2
- package/dist/durable.d.ts +38 -0
- package/dist/durable.js +6 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/logs.d.ts +64 -0
- package/dist/logs.js +120 -0
- package/dist/outbox.d.ts +52 -0
- package/dist/outbox.js +173 -0
- package/dist/outcomes.d.ts +13 -0
- package/dist/outcomes.js +62 -0
- package/dist/queues.d.ts +68 -0
- package/dist/queues.js +148 -0
- package/dist/schema.d.ts +44 -0
- package/dist/schema.js +64 -0
- package/dist/subscriptions.d.ts +23 -0
- package/dist/subscriptions.js +121 -0
- package/package.json +59 -3
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,192 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @worker-protocol/cloudflare
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The protocol's stores in Durable Objects, and the two Queues between an outbox and a sink: one
|
|
4
|
+
mixin per piece, for Workers on Cloudflare.
|
|
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.** `@worker-protocol/hono` carries everything the protocol fixes and
|
|
8
|
+
leaves to the platform what depends on it: where ENDP-16's outcomes, SUB-7's subscriptions and
|
|
9
|
+
LOG-2's records live, and what carries a delivery. On Cloudflare that is a Durable Object and a
|
|
10
|
+
Queue, and this package is the one way of writing both.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
npm i @worker-protocol/cloudflare @worker-protocol/hono hono zod
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`hono` (`^4.13.7`) and `zod` (`^4.5.4`) are peer dependencies, shared with `@worker-protocol/hono`.
|
|
19
|
+
The declarations name the Workers runtime's own types — `DurableObjectState`, `Queue`,
|
|
20
|
+
`MessageBatch` — so a Worker type-checks against them with what `wrangler types` writes.
|
|
21
|
+
|
|
22
|
+
**What has run, and what has not yet.** Every piece runs on workerd in this package's suite, and
|
|
23
|
+
`examples/fleet-worker`, built on it, delivers to a subscriber's sink across Miniflare's Queues in
|
|
24
|
+
the conformance suite. What no local run shows is a real Cloudflare account: what reaches the
|
|
25
|
+
dead-letter queue after the platform's last retry, the platform's limits on batches and alarms,
|
|
26
|
+
and how much writing a delivery's outcome costs the one object at scale. Those are the first things
|
|
27
|
+
to watch in a first deployment.
|
|
28
|
+
|
|
29
|
+
## One mixin per piece
|
|
30
|
+
|
|
31
|
+
| Mixin | What it adds | Where it goes |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `withOutcomes` | ENDP-16's reservations and recorded outcomes | the object `actions.outcomes` reads |
|
|
34
|
+
| `withSubscriptions` | SUB-7's subscriptions, found and ensured in one step | **one** object, never one per shard |
|
|
35
|
+
| `withOutbox` | an outbox, drained when a call ends and retried by the alarm | every object whose changes raise events |
|
|
36
|
+
| `withLogs` | LOG-2's window, filtered and paged in SQL | the object `/logs` reads, and a Tail Worker writes |
|
|
37
|
+
|
|
38
|
+
Mixins rather than one base class, because a Worker in production keeps one object per vehicle and
|
|
39
|
+
one for the fleet: the subscriptions belong in the one, an outbox in every other, and a class that
|
|
40
|
+
carried everything would put subscription tables in thousands of objects. Plain functions over
|
|
41
|
+
`SqlStorage` would compose with any base class as well, at the price of a dozen one-line RPC
|
|
42
|
+
wrappers per Worker per piece — the boilerplate this package exists to remove. A Worker with a
|
|
43
|
+
single object composes all four in it:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { DurableObject } from "cloudflare:workers";
|
|
47
|
+
import { withLogs, withOutbox, withOutcomes, withSubscriptions } from "@worker-protocol/cloudflare";
|
|
48
|
+
|
|
49
|
+
export class Fleet extends withLogs(
|
|
50
|
+
withOutbox(withSubscriptions(withOutcomes(DurableObject<Env>)), {
|
|
51
|
+
events: (env) => env.EVENTS,
|
|
52
|
+
}),
|
|
53
|
+
{ keep: 500 },
|
|
54
|
+
) {
|
|
55
|
+
async ingest(readings: Reading[], now: number) {
|
|
56
|
+
// ...the domain's own writes...
|
|
57
|
+
this.enqueue(now, [taskRaised(task)]); // same transaction as the writes above
|
|
58
|
+
await this.flush(); // sends what was raised; what does not go, the alarm retries
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
And one that shards puts each where it belongs:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export class Fleet extends withSubscriptions(DurableObject<Env>) {}
|
|
67
|
+
export class Asset extends withOutbox(DurableObject<Env>, { events: (env) => env.EVENTS }) {
|
|
68
|
+
override async wake() {
|
|
69
|
+
// the domain's own alarm, asked for with `this.wakeAt(at)`
|
|
70
|
+
await this.ctx.storage.deleteAll();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each piece keeps its tables under a `wp_` prefix, so none meets a table of the domain's.
|
|
76
|
+
|
|
77
|
+
## Migrations, object by object
|
|
78
|
+
|
|
79
|
+
A Worker with an object per vehicle has thousands of copies of each piece's tables, and a column
|
|
80
|
+
or an index added in a later release has to reach every one of them — each on its own, the first
|
|
81
|
+
time it is reached after a deploy. `CREATE TABLE IF NOT EXISTS` reaches only the objects created
|
|
82
|
+
after the change, so each piece instead declares a `Schema`: its name, and every step its tables
|
|
83
|
+
have ever taken, oldest first. `migrate(storage, schema)` runs at the start of every method,
|
|
84
|
+
applies the steps an object still lacks in one transaction with the version it records in
|
|
85
|
+
`wp_schema`, and costs one read of that table when there is nothing to do.
|
|
86
|
+
|
|
87
|
+
A published step is never edited, and a change is a new step at the end. An object written by a
|
|
88
|
+
later release than the one running is refused rather than read, because older code over a newer
|
|
89
|
+
shape is a rollback and not a migration.
|
|
90
|
+
|
|
91
|
+
`migrate` is exported for a domain's own tables too, under a piece name of its own, so one object
|
|
92
|
+
keeps one record of what shape it is in:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { migrate, type Schema } from "@worker-protocol/cloudflare";
|
|
96
|
+
|
|
97
|
+
const VEHICLES: Schema = {
|
|
98
|
+
piece: "fleet.vehicles",
|
|
99
|
+
steps: [
|
|
100
|
+
["CREATE TABLE IF NOT EXISTS vehicle (plate TEXT PRIMARY KEY)"],
|
|
101
|
+
["ALTER TABLE vehicle ADD COLUMN kind TEXT NOT NULL DEFAULT 'unknown'"],
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const sql = migrate(this.ctx.storage, VEHICLES);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**There is no ORM here, on purpose.** The tables are few, the queries plain, and most of what they
|
|
109
|
+
hold is a JSON record in one column. A query builder carried by a library would be a version every
|
|
110
|
+
Worker installing it has to agree with — and Drizzle's migrations keep one journal per database,
|
|
111
|
+
which a Worker using Drizzle for its own tables in the same object would share with this package's.
|
|
112
|
+
A Worker that wants Drizzle or Kysely for its domain uses it, beside these tables.
|
|
113
|
+
|
|
114
|
+
## The outbox, and the one alarm
|
|
115
|
+
|
|
116
|
+
`enqueue(at, events)` writes each event with its id in the same transaction as the calling method's
|
|
117
|
+
own writes, so there is no moment at which a Fact changed and its event was not yet owed. The row
|
|
118
|
+
holds the whole event, so the domain's retention need not wait for what is pending — an outbox of
|
|
119
|
+
ids, rendered into events only when they are sent, would duplicate nothing and make every Worker
|
|
120
|
+
coordinate its retention with it by hand. An id still
|
|
121
|
+
waiting is not enqueued twice; once sent, the same id enqueued again is a republication under the
|
|
122
|
+
same `source` and `id`, which a consumer remembering them discards (EVT-8).
|
|
123
|
+
|
|
124
|
+
`flush()` sends what is waiting to the events Queue, in order, a hundred at a time, and never
|
|
125
|
+
rejects: what could not be sent stays, and the alarm tries again from five seconds, doubling to
|
|
126
|
+
five minutes. **A Durable Object has one alarm**, so the domain does not set it: it asks with
|
|
127
|
+
`wakeAt(at)` and overrides `wake()`, and the alarm fires at the earlier of the domain's instant and
|
|
128
|
+
the outbox's retry. A base class that sets the alarm itself does not compose with this one.
|
|
129
|
+
|
|
130
|
+
## The two Queues
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { consumeQueues, deliveryQueue, durableSubscriptions } from "@worker-protocol/cloudflare";
|
|
134
|
+
|
|
135
|
+
const hubOf = (env: Env) =>
|
|
136
|
+
eventHub({
|
|
137
|
+
id: ID,
|
|
138
|
+
events: EVENTS,
|
|
139
|
+
subscriptions: {
|
|
140
|
+
abandonAfterSeconds: 86_400,
|
|
141
|
+
store: durableSubscriptions(fleetOf(env)),
|
|
142
|
+
queue: deliveryQueue(env.DELIVERIES),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
fetch: app.fetch,
|
|
148
|
+
queue: consumeQueues<Env>({
|
|
149
|
+
queues: { events: "fleet-events", deliveries: "fleet-deliveries" },
|
|
150
|
+
hub: hubOf,
|
|
151
|
+
deadLetter: (env) => env.DEAD,
|
|
152
|
+
broker: ({ event }) => publishToKafka(event), // where the Worker declares a broker
|
|
153
|
+
record: ({ rows, env }) => fleetOf(env).record(rows), // where it keeps logs
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The consumer of the events Queue publishes each batch through the hub — the subscriptions are read
|
|
159
|
+
once per type for the whole batch — and leaves one delivery per matching subscription on the
|
|
160
|
+
deliveries Queue, then hands each event to the broker. An event of a type the Worker does not
|
|
161
|
+
declare is set aside rather than retried, since no retry would mend it, and a failure to reach the
|
|
162
|
+
store retries the batch whole, once. The consumer of the deliveries Queue runs `deliver()` and hands
|
|
163
|
+
its decision back: `retry({ delaySeconds })`, or `ack()`.
|
|
164
|
+
|
|
165
|
+
What is given up goes to the dead-letter queue as a `GivenUp`, tagged by its `kind` — a delivery
|
|
166
|
+
refused for good, outside EVT-8's window or abandoned with its subscription, with the reason; or an
|
|
167
|
+
event of an undeclared type — to be inspected and never redriven, and to a record an operator reads
|
|
168
|
+
through `/logs`. Name the same queue as each consumer's `dead_letter_queue` in
|
|
169
|
+
`wrangler.jsonc`, so it also receives what the platform drops after `max_retries`, and set
|
|
170
|
+
`max_retries` well above what the hub asks for: its default of 3 cuts a delivery short.
|
|
171
|
+
|
|
172
|
+
## Also exported
|
|
173
|
+
|
|
174
|
+
`durableOutcomes`, `durableSubscriptions` and `durableLogs` turn a stub into the store `mount()`
|
|
175
|
+
takes. A subscription crosses the RPC boundary as JSON — its filters nest, and a stub's types do not
|
|
176
|
+
survive a recursive one — and these do the parsing, so a Worker never sees the strings.
|
|
177
|
+
`tailRecords(events)` is what a Tail Worker records of the invocations it is handed: an exception
|
|
178
|
+
nobody caught and an invocation that ended badly, and never their `console` output.
|
|
179
|
+
|
|
180
|
+
## Related packages
|
|
181
|
+
|
|
182
|
+
- `@worker-protocol/hono` — `mount()` and `eventHub()`, which these stores are for.
|
|
183
|
+
- `@worker-protocol/client` — `consume()` and `sink()`, the other end of a subscription.
|
|
184
|
+
- `@worker-protocol/conformance` — point it at a Worker's base URL, get a report of what it complies
|
|
185
|
+
with.
|
|
186
|
+
|
|
187
|
+
## License and name
|
|
188
|
+
|
|
189
|
+
Apache-2.0, patent grant included — implement the protocol in any product, commercial or not,
|
|
190
|
+
without asking anyone. The name is not part of that grant (Apache-2.0 §6): a claim that something
|
|
191
|
+
*speaks worker-protocol* is one this project vouches for, and the conformance tool is how it is
|
|
192
|
+
earned.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DurableObject } from "cloudflare:workers";
|
|
2
|
+
/**
|
|
3
|
+
* Any Durable Object class, as a mixin here takes it.
|
|
4
|
+
*
|
|
5
|
+
* The rest parameter is `any[]` because TypeScript requires exactly that of a mixin's base
|
|
6
|
+
* constructor (TS2545), and it is the one `any` in this package: the constructor is the platform's,
|
|
7
|
+
* called by the platform with `(ctx, env)`, and nothing here calls it.
|
|
8
|
+
*/
|
|
9
|
+
export type DurableObjectClass = abstract new (...args: any[]) => DurableObject<unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* What a mixin answers: the class it was given, with the methods it adds.
|
|
12
|
+
*
|
|
13
|
+
* Spelled out as the return type of every mixin, rather than inferred from the class inside it,
|
|
14
|
+
* because the inferred type carries `ctx` and `env` — protected on `DurableObject` — and TypeScript
|
|
15
|
+
* cannot write a protected member of an anonymous class into a declaration file (TS4094). The
|
|
16
|
+
* intersection keeps both: a subclass still reaches `this.ctx`, through the class it was given.
|
|
17
|
+
*/
|
|
18
|
+
export type Mixed<B extends DurableObjectClass, M> = B & (abstract new (...args: any[]) => M);
|
|
19
|
+
/** The environment a Durable Object class was declared with, read back off the class. */
|
|
20
|
+
export type EnvOf<B extends DurableObjectClass> = InstanceType<B> extends DurableObject<infer E> ? E : never;
|
|
21
|
+
/** The one row a query answers, or `undefined`. */
|
|
22
|
+
export declare const first: <T extends Record<string, SqlStorageValue>>(cursor: SqlStorageCursor<T>) => T | undefined;
|
|
23
|
+
/** What one `sendBatch` carries: the platform's own cap per call. */
|
|
24
|
+
export declare const BATCH = 100;
|
|
25
|
+
/** Bodies as Queue messages, in JSON rather than structured clone so a dead-letter queue reads. */
|
|
26
|
+
export declare const asJson: <T>(bodies: T[]) => {
|
|
27
|
+
body: T;
|
|
28
|
+
contentType: "json";
|
|
29
|
+
}[];
|
|
30
|
+
/**
|
|
31
|
+
* A stub's view of what a mixin adds: every method, over RPC, answering a promise.
|
|
32
|
+
*
|
|
33
|
+
* Derived rather than written out, so that a mixin's methods and the adapter that reaches them over
|
|
34
|
+
* a stub are one list and cannot drift.
|
|
35
|
+
*/
|
|
36
|
+
export type Rpc<M> = {
|
|
37
|
+
[K in keyof M]: M[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
|
|
38
|
+
};
|
package/dist/durable.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The one row a query answers, or `undefined`. */
|
|
2
|
+
export const first = (cursor) => cursor.toArray()[0];
|
|
3
|
+
/** What one `sendBatch` carries: the platform's own cap per call. */
|
|
4
|
+
export const BATCH = 100;
|
|
5
|
+
/** Bodies as Queue messages, in JSON rather than structured clone so a dead-letter queue reads. */
|
|
6
|
+
export const asJson = (bodies) => bodies.map((body) => ({ body, contentType: "json" }));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@worker-protocol/cloudflare` — the protocol's stores in Durable Objects, and the two Queues
|
|
3
|
+
* between an outbox and a sink.
|
|
4
|
+
*
|
|
5
|
+
* `@worker-protocol/hono` leaves to the platform what depends on it: where ENDP-16's outcomes,
|
|
6
|
+
* SUB-7's subscriptions and LOG-2's records live, and what carries a delivery. On Cloudflare that is
|
|
7
|
+
* a Durable Object and a Queue, and this package is the one way of writing both, so that each Worker
|
|
8
|
+
* on the platform does not write it again with its own mistakes.
|
|
9
|
+
*
|
|
10
|
+
* **One mixin per piece**, because a Worker in production keeps one object per vehicle and one for
|
|
11
|
+
* the fleet, and each object should carry only what it holds: the subscriptions in the one, an
|
|
12
|
+
* outbox in every other. A Worker with a single object composes all four in it.
|
|
13
|
+
*/
|
|
14
|
+
export type { DurableObjectClass, EnvOf, Mixed } from "./durable.ts";
|
|
15
|
+
export { durableLogs, type LogMethods, type LogRow, type LogRows, type LogsQuery, type LogsRpc, tailRecords, withLogs, } from "./logs.ts";
|
|
16
|
+
export { type Flushed, type OutboxEvent, type OutboxMethods, withOutbox } from "./outbox.ts";
|
|
17
|
+
export { durableOutcomes, type OutcomeMethods, type OutcomesRpc, withOutcomes, } from "./outcomes.ts";
|
|
18
|
+
export { consumeQueues, deliveryQueue, type GivenUp, type QueuesConfig, } from "./queues.ts";
|
|
19
|
+
export { migrate, type Schema } from "./schema.ts";
|
|
20
|
+
export { durableSubscriptions, type SubscriptionMethods, type SubscriptionsRpc, withSubscriptions, } from "./subscriptions.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@worker-protocol/cloudflare` — the protocol's stores in Durable Objects, and the two Queues
|
|
3
|
+
* between an outbox and a sink.
|
|
4
|
+
*
|
|
5
|
+
* `@worker-protocol/hono` leaves to the platform what depends on it: where ENDP-16's outcomes,
|
|
6
|
+
* SUB-7's subscriptions and LOG-2's records live, and what carries a delivery. On Cloudflare that is
|
|
7
|
+
* a Durable Object and a Queue, and this package is the one way of writing both, so that each Worker
|
|
8
|
+
* on the platform does not write it again with its own mistakes.
|
|
9
|
+
*
|
|
10
|
+
* **One mixin per piece**, because a Worker in production keeps one object per vehicle and one for
|
|
11
|
+
* the fleet, and each object should carry only what it holds: the subscriptions in the one, an
|
|
12
|
+
* outbox in every other. A Worker with a single object composes all four in it.
|
|
13
|
+
*/
|
|
14
|
+
export { durableLogs, tailRecords, withLogs, } from "./logs.js";
|
|
15
|
+
export { withOutbox } from "./outbox.js";
|
|
16
|
+
export { durableOutcomes, withOutcomes, } from "./outcomes.js";
|
|
17
|
+
export { consumeQueues, deliveryQueue, } from "./queues.js";
|
|
18
|
+
export { migrate } from "./schema.js";
|
|
19
|
+
export { durableSubscriptions, withSubscriptions, } from "./subscriptions.js";
|
package/dist/logs.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type LogFacts, type LogLevel } from "@worker-protocol/hono";
|
|
2
|
+
import type { DurableObjectClass, Mixed, Rpc } from "./durable.ts";
|
|
3
|
+
/**
|
|
4
|
+
* LOG-2's window, in the Durable Object a Worker writes its records to.
|
|
5
|
+
*
|
|
6
|
+
* **Written on purpose, never captured.** There is no API in the Workers runtime for reading a
|
|
7
|
+
* Worker's own `console` back, and this does not go looking for one: a Worker records what is worth
|
|
8
|
+
* a line, in one call, the way it raises an event. A record written on purpose belongs to the work
|
|
9
|
+
* that produced it; a `console` line scraped out of the runtime belongs to whichever isolate was
|
|
10
|
+
* running.
|
|
11
|
+
*
|
|
12
|
+
* SQL, because a read filters by a level floor and a half-open interval (LOG-7, LOG-8), and a store
|
|
13
|
+
* that cannot filter would hand the Worker every row so it could throw most of them away. `seq` is
|
|
14
|
+
* what makes ENDP-33 free: it only grows, a cursor names one, and a page asks for what is below it,
|
|
15
|
+
* so a record written since the last page cannot appear in the next.
|
|
16
|
+
*/
|
|
17
|
+
/** One record as it crosses the RPC boundary: an instant as a number, and `fields` flat (LOG-9). */
|
|
18
|
+
export type LogRow = {
|
|
19
|
+
at: number;
|
|
20
|
+
level: LogLevel;
|
|
21
|
+
message: string;
|
|
22
|
+
fields?: Record<string, string | number | boolean>;
|
|
23
|
+
};
|
|
24
|
+
/** One page as the object answers it, before `durableLogs` turns instants into `Date`s. */
|
|
25
|
+
export type LogRows = {
|
|
26
|
+
rows: LogRow[];
|
|
27
|
+
nextCursor?: string;
|
|
28
|
+
};
|
|
29
|
+
/** A read as the object takes it: `mount()`'s query, with the floor and the cursor as numbers. */
|
|
30
|
+
export type LogsQuery = {
|
|
31
|
+
/** LOG-7. The floor: this level and every level above it. */
|
|
32
|
+
minRank: number;
|
|
33
|
+
/** ENDP-21. The position the caller's cursor named, or `null` for the first page. */
|
|
34
|
+
before: number | null;
|
|
35
|
+
from: number | null;
|
|
36
|
+
to: number | null;
|
|
37
|
+
limit: number;
|
|
38
|
+
};
|
|
39
|
+
/** What `withLogs` adds to a Durable Object. */
|
|
40
|
+
export interface LogMethods {
|
|
41
|
+
record(rows: LogRow[]): void;
|
|
42
|
+
logs(query: LogsQuery): LogRows;
|
|
43
|
+
}
|
|
44
|
+
export declare function withLogs<B extends DurableObjectClass>(Base: B, options: {
|
|
45
|
+
/** LOG-2: how many records the window holds. The oldest go first. */
|
|
46
|
+
keep: number;
|
|
47
|
+
}): Mixed<B, LogMethods>;
|
|
48
|
+
/** What `durableLogs` needs of a stub: the read `withLogs` adds, over RPC. */
|
|
49
|
+
export type LogsRpc = Pick<Rpc<LogMethods>, "logs">;
|
|
50
|
+
/** `logs` for `mount()`, over the object with `withLogs`. `mount()` decodes the query. */
|
|
51
|
+
export declare const durableLogs: (stub: LogsRpc, options: {
|
|
52
|
+
pageSize: number;
|
|
53
|
+
}) => LogFacts;
|
|
54
|
+
/**
|
|
55
|
+
* What a Tail Worker records of the invocations it is handed: an exception nobody caught, and an
|
|
56
|
+
* invocation that ended badly — the two things a Worker cannot record about itself, because by
|
|
57
|
+
* then it has stopped running.
|
|
58
|
+
*
|
|
59
|
+
* **It throws `event.logs` away**, which is every `console` call the producer made: forwarding it
|
|
60
|
+
* would be the capture `spec/logs.md` argues against. That filter is also what stops a tail from
|
|
61
|
+
* feeding itself — its own write to the object is traced, comes back as `ok` with no exceptions,
|
|
62
|
+
* and records nothing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function tailRecords(events: TraceItem[]): LogRow[];
|