@ontrails/cloudflare 1.0.0-beta.39 → 1.0.0-beta.41

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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @ontrails/cloudflare
2
2
 
3
+ ## 1.0.0-beta.41
4
+
5
+ ## 1.0.0-beta.40
6
+
7
+ ### Minor Changes
8
+
9
+ - [`9874e0b`](https://github.com/outfitter-dev/trails/commit/9874e0bb034c0f98edeb19833d9d3519c2a07a4c): Add `@ontrails/cloudflare/d1`, an env-bound Cloudflare D1 store resource for `@ontrails/store` definitions. The new subpath exports `cloudflareD1` and `connectD1`, supports the backend-agnostic store accessor contract (`get`, `list`, `upsert`, `remove`), versioned-table optimistic concurrency, fixture/mock seeding, store-derived write signals, Miniflare-backed conformance tests, and Worker env-bridge integration.
10
+
11
+ `@ontrails/core` and `@ontrails/store` no longer require the Bun global for signal fire ids or late-bound store signal tokens, so store definitions and store-derived signal emission work inside Worker modules. `@ontrails/warden` now treats `cloudflareD1` as a required Cloudflare public export with `@example` coverage.
12
+
13
+ - [`1e64ee7`](https://github.com/outfitter-dev/trails/commit/1e64ee7bc270901486c5bb51ac38bf045c924adc): Add first-class queue activation sources with `queue()` in `@ontrails/core`.
14
+ Queue sources validate their runtime queue name and parse contract, project the
15
+ queue name into durable topo facts, participate in activation input
16
+ compatibility, and block established outputs when malformed.
17
+
18
+ Add `@ontrails/cloudflare/queues` with `cloudflareQueue`, `createMemoryQueue`,
19
+ and `createQueueHandler`. Cloudflare Workers now expose both `fetch` and
20
+ `queue` entrypoints from `createWorkersHandler`, resolve env-bound resources for
21
+ queue-activated trails, acknowledge successful/skipped/cancelled messages, and
22
+ acknowledge traced non-retryable Trails errors so permanently invalid messages
23
+ do not churn through the queue. Failures explicitly marked retryable enter
24
+ Cloudflare's retry and DLQ flow, with rate-limit delays preserved.
25
+
26
+ `@ontrails/warden` now treats queue activation sources as materialized and
27
+ requires `cloudflareQueue` public export example coverage.
28
+
29
+ - [`4086b5b`](https://github.com/outfitter-dev/trails/commit/4086b5b2f01b24660924fd8b667523f38caaed29): Add `@ontrails/cloudflare/r2`, an env-bound Cloudflare R2 bucket resource with
30
+ `cloudflareR2`, `createMemoryR2`, and `r2ObjectToBlobRef`. The resource
31
+ materializes Worker `r2_buckets` bindings through the shared env bridge, records
32
+ Cloudflare lock overlay facts, carries an in-memory object mock for
33
+ configuration-free tests, and documents the supported object operations plus
34
+ streaming/metadata boundaries.
35
+
36
+ `@ontrails/warden` now treats `cloudflareR2` as a required Cloudflare public
37
+ export with `@example` coverage.
38
+
39
+ - [`5adb995`](https://github.com/outfitter-dev/trails/commit/5adb99551c2dda6190d46cce7f60bb08d63c99aa): Complete the v1 hard cutover from the authored `blaze` field to
40
+ `implementation` across trail contracts, surface projections, tests, examples,
41
+ and public source-analysis helpers. Existing applications must rename authored
42
+ trail behavior fields and direct trail-object access before upgrading.
43
+
3
44
  ## 1.0.0-beta.39
4
45
 
5
46
  ### Minor Changes
package/README.md CHANGED
@@ -6,15 +6,15 @@ The Cloudflare adapter collection for Trails. One package, one subpath per Cloud
6
6
  | --- | --- | --- |
7
7
  | `@ontrails/cloudflare/workers` | HTTP surface materializer (fetch handler) | ✅ |
8
8
  | `@ontrails/cloudflare/kv` | Key-value resource | ✅ |
9
- | `/d1` | Store driver | Planned |
10
- | `/queues` | Activation source + outbound delivery | Planned |
11
- | `/r2` | Blob/object resource | Planned |
9
+ | `@ontrails/cloudflare/d1` | D1-backed store resource | |
10
+ | `@ontrails/cloudflare/queues` | Queue producer resource + consumer materializer | |
11
+ | `@ontrails/cloudflare/r2` | R2 blob/object resource | |
12
12
 
13
13
  Adapter composition doctrine applies throughout: subpaths take primitive-authored declarations (a resource definition, a surface config) and never shadow authoring verbs. Bindings arrive ambiently on the Worker `env`, so runtime dependencies are near-zero.
14
14
 
15
- ## `/workers` — the fetch-handler materializer
15
+ ## `/workers` — the Worker materializer
16
16
 
17
- `createWorkersHandler(graph, options)` produces the `{ fetch(request, env, ctx) }` Worker export by delegating to the shared HTTP fetch kernel from `@ontrails/http` — the same kernel behind the Bun and Hono surfaces, so routes, validation, error projection, and webhook handling behave identically.
17
+ `createWorkersHandler(graph, options)` produces the Worker export for Cloudflare runtime entrypoints. Its `fetch(request, env, ctx)` member delegates to the shared HTTP fetch kernel from `@ontrails/http` — the same kernel behind the Bun and Hono surfaces, so routes, validation, error projection, and webhook handling behave identically. Its `queue(batch, env, ctx)` member dispatches first-class core `queue()` activation sources through the `/queues` materializer.
18
18
 
19
19
  ```ts
20
20
  // src/worker.ts
@@ -62,7 +62,7 @@ import { z } from 'zod';
62
62
  const flags = cloudflareKv('flags', { binding: 'FLAGS' });
63
63
 
64
64
  const showFlag = trail('flag.show', {
65
- blaze: async (input, ctx) => {
65
+ implementation: async (input, ctx) => {
66
66
  const value = await flags.from(ctx).get(input.key);
67
67
  return Result.ok({ value });
68
68
  },
@@ -96,9 +96,161 @@ testAll(graph);
96
96
 
97
97
  `createMemoryKv()` is also exported directly for hand-rolled tests, with an injectable clock for TTL assertions. Two documented divergences from the real binding: the mock does not enforce KV's 60-second minimum TTL, and when both `expiration` and `expirationTtl` are passed the mock prefers `expirationTtl` where the real binding rejects the combination.
98
98
 
99
+ ## `/d1` — the store resource
100
+
101
+ `cloudflareD1(definition, { binding, id })` binds an `@ontrails/store` definition to a Cloudflare D1 database. Trails declare the returned resource and use the standard store accessors (`get`, `list`, `upsert`, `remove`) through `db.from(ctx)`.
102
+
103
+ ```ts
104
+ import { cloudflareD1 } from '@ontrails/cloudflare/d1';
105
+ import { trail, Result } from '@ontrails/core';
106
+ import { store } from '@ontrails/store';
107
+ import { z } from 'zod';
108
+
109
+ const definition = store({
110
+ notes: {
111
+ identity: 'id',
112
+ schema: z.object({ id: z.string(), body: z.string() }),
113
+ },
114
+ });
115
+
116
+ const db = cloudflareD1(definition, { binding: 'DB', id: 'notes.store' });
117
+
118
+ const saveNote = trail('note.save', {
119
+ implementation: async (input, ctx) =>
120
+ Result.ok(await db.from(ctx).notes.upsert(input)),
121
+ input: z.object({ id: z.string(), body: z.string() }),
122
+ intent: 'write',
123
+ output: z.object({ id: z.string(), body: z.string() }),
124
+ resources: [db],
125
+ });
126
+ ```
127
+
128
+ Declare the binding in wrangler config:
129
+
130
+ ```toml
131
+ d1_databases = [
132
+ { binding = "DB", database_name = "my-worker", database_id = "<database-id>" }
133
+ ]
134
+ ```
135
+
136
+ The first implementation stores one JSON entity per D1 row (`id TEXT PRIMARY KEY`, `entity TEXT NOT NULL`, nullable `version INTEGER`) in adapter-owned tables prefixed by the resource id. It preserves the backend-agnostic store contract, including generated identity fields, `createdAt`/`updatedAt` generation, versioned-table optimistic concurrency (`ConflictError` on stale `version`), fixture/mock seeding, and store-derived `created`/`updated`/`removed` signals.
137
+
138
+ Capability boundaries are explicit:
139
+
140
+ - `indexed`/`indexes`, `references`, and `search` stay store metadata for this driver; the D1 adapter does not create secondary indexes, foreign keys, or FTS tables yet.
141
+ - `list(filters)` performs simple equality filtering in the adapter after reading table rows, then applies `offset`/`limit`.
142
+ - `connectD1(definition, database, options)` is exported for tests and advanced runtimes that already hold a D1 binding. Schema creation and optional runtime `seed` run lazily before the first accessor call so the Workers env bridge can resolve resources synchronously. Runtime seed rows require explicit stable identities and are insert-only, so rematerializing a Worker cannot overwrite user edits.
143
+
144
+ Every `cloudflareD1` resource carries an in-memory mock factory seeded from table fixtures or `mockSeed`, so `testAll(app)` remains configuration-free. Miniflare can provide a real D1 binding for local integration tests without a Cloudflare account.
145
+
146
+ ## `/r2` — blob/object resource
147
+
148
+ `cloudflareR2(id, { binding })` authors a resource wrapping a Cloudflare R2 bucket binding. Trails declare it with `resources: [...]` and use `bucket.from(ctx)` to call the Worker binding's object operations (`put`, `get`, `head`, `delete`, and `list`).
149
+
150
+ ```ts
151
+ import {
152
+ NotFoundError,
153
+ Result,
154
+ blobRefSchema,
155
+ trail,
156
+ } from '@ontrails/core';
157
+ import { cloudflareR2, r2ObjectToBlobRef } from '@ontrails/cloudflare/r2';
158
+ import { z } from 'zod';
159
+
160
+ const assets = cloudflareR2('assets', { binding: 'ASSETS' });
161
+
162
+ const readAsset = trail('asset.read', {
163
+ implementation: async (input, ctx) => {
164
+ const object = await assets.from(ctx).get(input.key);
165
+ if (object === null || !('body' in object)) {
166
+ return Result.err(new NotFoundError(`Asset "${input.key}" not found`));
167
+ }
168
+ return Result.ok(r2ObjectToBlobRef(object));
169
+ },
170
+ input: z.object({ key: z.string() }),
171
+ intent: 'read',
172
+ output: blobRefSchema,
173
+ resources: [assets],
174
+ });
175
+ ```
176
+
177
+ Declare the binding in wrangler config:
178
+
179
+ ```toml
180
+ [[r2_buckets]]
181
+ binding = "ASSETS"
182
+ bucket_name = "my-assets"
183
+ ```
184
+
185
+ The resource surface follows the R2 Worker binding structurally. A real R2 bucket binding passes through unchanged, including Cloudflare's conditional operation behavior where `get()` can return metadata without a body and `put()` can return `null` when a precondition fails. `r2ObjectToBlobRef(object)` is the small bridge from a fetched R2 object body to core's `BlobRef` binary-output contract; the HTTP and MCP surfaces already know how to project `blobRefSchema`.
186
+
187
+ Capability boundaries are explicit:
188
+
189
+ - The adapter does not expose public bucket URLs, signed URLs, S3 clients, multipart upload helpers, or object-event subscriptions.
190
+ - The in-memory mock implements object bytes, metadata, `put`, `get`, `head`, `delete`, and lexicographic `list` pagination with prefix/cursor/delimiter grouping. It accepts SSE-C option shapes for binding compatibility but does not encrypt objects, validate keys, or emit `ssecKeyMd5`; it also does not model R2 preconditions, range reads, checksums, storage-tier billing behavior, or multipart uploads.
191
+ - For raw HTTP upload/download routes, keep authorization in your own trails or surface layer; the R2 resource only materializes the bucket binding.
192
+
193
+ Every `cloudflareR2` resource carries an in-memory mock (`createMemoryR2`) so object trails work in `testAll(app)` and focused unit tests without a Cloudflare account or wrangler.
194
+
195
+ ## `/queues` — Queue producer and consumer support
196
+
197
+ `cloudflareQueue(id, { binding })` authors a resource wrapping a Cloudflare Queue producer binding. Trails declare it with `resources: [...]` and send messages with `jobs.from(ctx).send(...)` or `sendBatch(...)`.
198
+
199
+ ```ts
200
+ import { cloudflareQueue } from '@ontrails/cloudflare/queues';
201
+ import { Result, queue, trail } from '@ontrails/core';
202
+ import { z } from 'zod';
203
+
204
+ const jobs = cloudflareQueue<{ id: string }>('jobs', { binding: 'JOBS' });
205
+
206
+ const enqueueJob = trail('job.enqueue', {
207
+ implementation: async (input, ctx) => {
208
+ await jobs.from(ctx).send({ id: input.id });
209
+ return Result.ok({ queued: true });
210
+ },
211
+ input: z.object({ id: z.string() }),
212
+ output: z.object({ queued: z.boolean() }),
213
+ resources: [jobs],
214
+ });
215
+ ```
216
+
217
+ Queue consumers are authored with the core `queue()` activation source and materialized by `createWorkersHandler`:
218
+
219
+ ```ts
220
+ const consumeJob = trail('job.consume', {
221
+ implementation: async (input) => Result.ok({ processed: input.id }),
222
+ input: z.object({ id: z.string() }),
223
+ on: [
224
+ queue('queue.jobs', {
225
+ queue: 'jobs',
226
+ parse: z.object({ id: z.string() }),
227
+ }),
228
+ ],
229
+ output: z.object({ processed: z.string() }),
230
+ });
231
+ ```
232
+
233
+ Declare the producer binding and consumer in wrangler config:
234
+
235
+ ```toml
236
+ [[queues.producers]]
237
+ binding = "JOBS"
238
+ queue = "jobs"
239
+
240
+ [[queues.consumers]]
241
+ queue = "jobs"
242
+ max_batch_size = 10
243
+ max_retries = 3
244
+ dead_letter_queue = "jobs-dlq"
245
+ ```
246
+
247
+ The consumer materializer acknowledges each message after all matching Trails queue consumers succeed, skip by `where`, or return `CancelledError`. It also traces and acknowledges non-retryable Trails errors, including validation failures, so permanently invalid messages do not churn through the queue. Errors explicitly marked retryable call `message.retry(...)`; Cloudflare's retry and dead-letter configuration decides when a retried message moves to a DLQ. `RateLimitError.retryAfter` is passed through as `delaySeconds`.
248
+
249
+ Every `cloudflareQueue` resource carries an in-memory mock (`createMemoryQueue`) so producer trails work in `testAll(app)` and unit tests can inspect sent messages. The local Worker-env regression test exercises a queue-activated trail using KV through the env bridge; the Miniflare lane remains focused on HTTP/webhook/KV/D1 until its queue harness is wired in this repo.
250
+
99
251
  ## Local integration testing
100
252
 
101
- Integration runs are local-first via [miniflare](https://miniflare.dev) (workerd in-process): the test lane bundles a demo Worker with `Bun.build`, boots it with a real KV namespace, and exercises HTTP, webhook, and KV routes. See `src/__tests__/miniflare.test.ts`. Real-account deploys are manual and never CI-required.
253
+ Integration runs are local-first via [miniflare](https://miniflare.dev) (workerd in-process): the test lane bundles a demo Worker with `Bun.build`, boots it with real KV and D1 bindings, and exercises HTTP, webhook, KV, and D1 store routes. Queue producer/consumer behavior and R2 object behavior are covered by focused structural tests under `src/queues/__tests__/queues.test.ts` and `src/r2/__tests__/r2.test.ts`. Real-account deploys are manual and never CI-required.
102
254
 
103
255
  ## Lock facts
104
256
 
@@ -114,4 +266,4 @@ export const app = topo('my-worker', { readFlag });
114
266
  export const trailsOverlays = [cloudflareOverlay];
115
267
  ```
116
268
 
117
- `trails compile` validates the derived facts against the schema and embeds them as `overlays.cloudflare`; `trails wayfind --facts cloudflare` reads them back. Toolchains that predate a overlay's namespace preserve it byte-for-byte — adding a new fact family never edits the lock schema or graph type.
269
+ `trails compile` validates the derived facts against the schema and embeds them as `overlays.cloudflare`; `trails wayfind --overlay cloudflare` reads them back. Toolchains that predate an overlay's namespace preserve it byte-for-byte — adding a new fact family never edits the lock schema or graph type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/cloudflare",
3
- "version": "1.0.0-beta.39",
3
+ "version": "1.0.0-beta.41",
4
4
  "files": [
5
5
  "src/**/*.ts",
6
6
  "!src/**/__tests__/**",
@@ -14,30 +14,39 @@
14
14
  ".": "./src/index.ts",
15
15
  "./workers": "./src/workers/index.ts",
16
16
  "./kv": "./src/kv/index.ts",
17
+ "./d1": "./src/d1/index.ts",
18
+ "./r2": "./src/r2/index.ts",
19
+ "./queues": "./src/queues/index.ts",
17
20
  "./package.json": "./package.json"
18
21
  },
19
22
  "scripts": {
20
23
  "build": "tsc -b",
21
- "test": "bun test",
24
+ "test": "bun test --max-concurrency=1",
22
25
  "typecheck": "tsc --noEmit",
23
26
  "lint": "oxlint ./src",
24
27
  "clean": "rm -rf dist *.tsbuildinfo"
25
28
  },
26
29
  "dependencies": {
27
- "@ontrails/core": "^1.0.0-beta.39"
30
+ "@ontrails/core": "^1.0.0-beta.41"
28
31
  },
29
32
  "devDependencies": {
30
- "@ontrails/adapter-kit": "^1.0.0-beta.39",
31
- "@ontrails/testing": "^1.0.0-beta.39",
33
+ "@ontrails/adapter-kit": "^1.0.0-beta.41",
34
+ "@ontrails/testing": "^1.0.0-beta.41",
32
35
  "miniflare": "^4.20250617.4"
33
36
  },
34
37
  "peerDependencies": {
35
- "@ontrails/http": "^1.0.0-beta.39",
38
+ "@ontrails/http": "^1.0.0-beta.41",
39
+ "@ontrails/store": "^1.0.0-beta.41",
36
40
  "zod": "^4.3.5"
37
41
  },
38
42
  "trails": {
39
43
  "adapter": {
40
44
  "target": "http"
45
+ },
46
+ "adapters": {
47
+ "./d1": {
48
+ "target": "store"
49
+ }
41
50
  }
42
51
  }
43
52
  }