@greatwait/contracts 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authority.d.ts +217 -0
- package/dist/authority.js +261 -0
- package/dist/callback-ownership.d.ts +58 -0
- package/dist/callback-ownership.js +71 -0
- package/dist/callback-safety.d.ts +38 -0
- package/dist/callback-safety.js +403 -0
- package/dist/compatibility.d.ts +82 -0
- package/dist/compatibility.js +115 -0
- package/dist/delivery.d.ts +401 -0
- package/dist/delivery.js +311 -0
- package/dist/endpoints.d.ts +305 -0
- package/dist/endpoints.js +179 -0
- package/dist/events.d.ts +62 -0
- package/dist/events.js +38 -0
- package/dist/first-value-journey.d.ts +305 -0
- package/dist/first-value-journey.js +466 -0
- package/dist/forecast.d.ts +443 -0
- package/dist/forecast.js +729 -0
- package/dist/identifiers.d.ts +109 -0
- package/dist/identifiers.js +131 -0
- package/dist/identity.d.ts +196 -0
- package/dist/identity.js +182 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mcp.d.ts +51 -0
- package/dist/mcp.js +56 -0
- package/dist/oauth.d.ts +796 -0
- package/dist/oauth.js +708 -0
- package/dist/operations.d.ts +3985 -0
- package/dist/operations.js +1837 -0
- package/dist/partition.d.ts +771 -0
- package/dist/partition.js +206 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +180 -0
- package/dist/problems.d.ts +158 -0
- package/dist/problems.js +104 -0
- package/dist/protocol.d.ts +308 -0
- package/dist/protocol.js +515 -0
- package/dist/scheduling-records.d.ts +395 -0
- package/dist/scheduling-records.js +284 -0
- package/dist/scheduling.d.ts +1098 -0
- package/dist/scheduling.js +1116 -0
- package/dist/secrets.d.ts +383 -0
- package/dist/secrets.js +272 -0
- package/dist/signatures.d.ts +704 -0
- package/dist/signatures.js +867 -0
- package/dist/tenancy.d.ts +178 -0
- package/dist/tenancy.js +145 -0
- package/dist/test-sink.d.ts +986 -0
- package/dist/test-sink.js +755 -0
- package/dist/time.d.ts +45 -0
- package/dist/time.js +128 -0
- package/dist/usage.d.ts +121 -0
- package/dist/usage.js +200 -0
- package/dist/webhooks.d.ts +116 -0
- package/dist/webhooks.js +161 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.js +59 -0
- package/generated/v1/callback-delivery-signature.json +467 -0
- package/generated/v1/callback-endpoint-verification.json +59 -0
- package/generated/v1/callback-receiver.json +241 -0
- package/generated/v1/examples/attempts.list.response.json +24 -0
- package/generated/v1/examples/attempts.read.response.json +20 -0
- package/generated/v1/examples/credentials.create.request.json +5 -0
- package/generated/v1/examples/credentials.create.response.json +10 -0
- package/generated/v1/examples/credentials.list.response.json +17 -0
- package/generated/v1/examples/credentials.revoke.response.json +11 -0
- package/generated/v1/examples/credentials.rotate.request.json +3 -0
- package/generated/v1/examples/credentials.rotate.response.json +22 -0
- package/generated/v1/examples/endpoints.create.request.json +3 -0
- package/generated/v1/examples/endpoints.create.response.json +16 -0
- package/generated/v1/examples/endpoints.disable.response.json +15 -0
- package/generated/v1/examples/endpoints.list.response.json +21 -0
- package/generated/v1/examples/endpoints.read.response.json +14 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
- package/generated/v1/examples/endpoints.update.request.json +3 -0
- package/generated/v1/examples/endpoints.update.response.json +13 -0
- package/generated/v1/examples/endpoints.verify.response.json +15 -0
- package/generated/v1/examples/forecast.read.response.json +28 -0
- package/generated/v1/examples/forecast.windows.response.json +21 -0
- package/generated/v1/examples/grants.list.response.json +17 -0
- package/generated/v1/examples/grants.revoke.response.json +16 -0
- package/generated/v1/examples/meta.scaffold.response.json +5 -0
- package/generated/v1/examples/meta.version.response.json +5 -0
- package/generated/v1/examples/occurrences.list.response.json +17 -0
- package/generated/v1/examples/occurrences.read.response.json +15 -0
- package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
- package/generated/v1/examples/projects.create.request.json +3 -0
- package/generated/v1/examples/projects.create.response.json +5 -0
- package/generated/v1/examples/projects.list.response.json +9 -0
- package/generated/v1/examples/schedules.archive.response.json +25 -0
- package/generated/v1/examples/schedules.cancel.response.json +24 -0
- package/generated/v1/examples/schedules.create.request.json +9 -0
- package/generated/v1/examples/schedules.create.response.json +25 -0
- package/generated/v1/examples/schedules.list.response.json +23 -0
- package/generated/v1/examples/schedules.pause.response.json +23 -0
- package/generated/v1/examples/schedules.read.response.json +25 -0
- package/generated/v1/examples/schedules.replace.request.json +8 -0
- package/generated/v1/examples/schedules.replace.response.json +25 -0
- package/generated/v1/examples/schedules.resume.response.json +23 -0
- package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
- package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
- package/generated/v1/examples/testsinks.configure.request.json +3 -0
- package/generated/v1/examples/testsinks.configure.response.json +13 -0
- package/generated/v1/examples/testsinks.create.request.json +8 -0
- package/generated/v1/examples/testsinks.create.response.json +13 -0
- package/generated/v1/examples/testsinks.delete.response.json +13 -0
- package/generated/v1/examples/testsinks.list.response.json +20 -0
- package/generated/v1/examples/testsinks.read.response.json +14 -0
- package/generated/v1/examples/usage.read.response.json +20 -0
- package/generated/v1/first-value-journey.json +436 -0
- package/generated/v1/manifest.json +1037 -0
- package/generated/v1/openapi.json +23665 -0
- package/generated/v1/policy-matrix.json +305 -0
- package/generated/v1/schemas/attempts.list.query.json +16 -0
- package/generated/v1/schemas/attempts.list.response.json +175 -0
- package/generated/v1/schemas/attempts.read.response.json +168 -0
- package/generated/v1/schemas/credentials.create.request.json +39 -0
- package/generated/v1/schemas/credentials.create.response.json +88 -0
- package/generated/v1/schemas/credentials.list.query.json +16 -0
- package/generated/v1/schemas/credentials.list.response.json +124 -0
- package/generated/v1/schemas/credentials.revoke.response.json +84 -0
- package/generated/v1/schemas/credentials.rotate.request.json +11 -0
- package/generated/v1/schemas/credentials.rotate.response.json +171 -0
- package/generated/v1/schemas/endpoints.create.request.json +12 -0
- package/generated/v1/schemas/endpoints.create.response.json +115 -0
- package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.disable.response.json +100 -0
- package/generated/v1/schemas/endpoints.list.query.json +16 -0
- package/generated/v1/schemas/endpoints.list.response.json +146 -0
- package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.read.response.json +100 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
- package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.update.request.json +12 -0
- package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.update.response.json +100 -0
- package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.verify.response.json +100 -0
- package/generated/v1/schemas/forecast.read.query.json +26 -0
- package/generated/v1/schemas/forecast.read.response.json +100 -0
- package/generated/v1/schemas/forecast.windows.query.json +26 -0
- package/generated/v1/schemas/forecast.windows.response.json +94 -0
- package/generated/v1/schemas/grants.list.response.json +98 -0
- package/generated/v1/schemas/grants.revoke.response.json +95 -0
- package/generated/v1/schemas/meta.scaffold.response.json +36 -0
- package/generated/v1/schemas/meta.version.response.json +19 -0
- package/generated/v1/schemas/occurrences.list.query.json +16 -0
- package/generated/v1/schemas/occurrences.list.response.json +83 -0
- package/generated/v1/schemas/occurrences.read.response.json +76 -0
- package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
- package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
- package/generated/v1/schemas/projects.create.request.json +12 -0
- package/generated/v1/schemas/projects.create.response.json +25 -0
- package/generated/v1/schemas/projects.list.response.json +35 -0
- package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.archive.response.json +189 -0
- package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.cancel.response.json +189 -0
- package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
- package/generated/v1/schemas/schedules.create.request.json +99 -0
- package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
- package/generated/v1/schemas/schedules.create.response.json +189 -0
- package/generated/v1/schemas/schedules.list.query.json +16 -0
- package/generated/v1/schemas/schedules.list.response.json +170 -0
- package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.pause.response.json +189 -0
- package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.read.response.json +189 -0
- package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.replace.request.json +99 -0
- package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.replace.response.json +189 -0
- package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.resume.response.json +189 -0
- package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
- package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
- package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
- package/generated/v1/schemas/testsinks.configure.request.json +66 -0
- package/generated/v1/schemas/testsinks.configure.response.json +115 -0
- package/generated/v1/schemas/testsinks.create.request.json +76 -0
- package/generated/v1/schemas/testsinks.create.response.json +115 -0
- package/generated/v1/schemas/testsinks.delete.response.json +115 -0
- package/generated/v1/schemas/testsinks.list.query.json +16 -0
- package/generated/v1/schemas/testsinks.list.response.json +161 -0
- package/generated/v1/schemas/testsinks.read.response.json +115 -0
- package/generated/v1/schemas/usage.read.query.json +15 -0
- package/generated/v1/schemas/usage.read.response.json +71 -0
- package/generated/v1/snippets/attempts.list.json +5 -0
- package/generated/v1/snippets/attempts.read.json +5 -0
- package/generated/v1/snippets/credentials.create.json +5 -0
- package/generated/v1/snippets/credentials.list.json +5 -0
- package/generated/v1/snippets/credentials.revoke.json +5 -0
- package/generated/v1/snippets/credentials.rotate.json +5 -0
- package/generated/v1/snippets/endpoints.create.json +5 -0
- package/generated/v1/snippets/endpoints.disable.json +5 -0
- package/generated/v1/snippets/endpoints.list.json +5 -0
- package/generated/v1/snippets/endpoints.read.json +5 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
- package/generated/v1/snippets/endpoints.update.json +5 -0
- package/generated/v1/snippets/endpoints.verify.json +5 -0
- package/generated/v1/snippets/forecast.read.json +5 -0
- package/generated/v1/snippets/forecast.windows.json +5 -0
- package/generated/v1/snippets/grants.list.json +5 -0
- package/generated/v1/snippets/grants.revoke.json +5 -0
- package/generated/v1/snippets/meta.scaffold.json +5 -0
- package/generated/v1/snippets/meta.version.json +5 -0
- package/generated/v1/snippets/occurrences.list.json +5 -0
- package/generated/v1/snippets/occurrences.read.json +5 -0
- package/generated/v1/snippets/occurrences.redeliver.json +5 -0
- package/generated/v1/snippets/projects.create.json +5 -0
- package/generated/v1/snippets/projects.list.json +5 -0
- package/generated/v1/snippets/schedules.archive.json +5 -0
- package/generated/v1/snippets/schedules.cancel.json +5 -0
- package/generated/v1/snippets/schedules.create.json +5 -0
- package/generated/v1/snippets/schedules.list.json +5 -0
- package/generated/v1/snippets/schedules.pause.json +5 -0
- package/generated/v1/snippets/schedules.read.json +5 -0
- package/generated/v1/snippets/schedules.replace.json +5 -0
- package/generated/v1/snippets/schedules.resume.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
- package/generated/v1/snippets/testsinks.configure.json +5 -0
- package/generated/v1/snippets/testsinks.create.json +5 -0
- package/generated/v1/snippets/testsinks.delete.json +5 -0
- package/generated/v1/snippets/testsinks.list.json +5 -0
- package/generated/v1/snippets/testsinks.read.json +5 -0
- package/generated/v1/snippets/usage.read.json +5 -0
- package/package.json +153 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import type { Authority } from "./authority.js";
|
|
2
|
+
import type { Attempt, AttemptView, LatencyClass, Occurrence, OccurrenceView, Schedule, ScheduleView } from "./scheduling.js";
|
|
3
|
+
/**
|
|
4
|
+
* The seam between the API Worker and a Scheduler Partition Durable Object.
|
|
5
|
+
*
|
|
6
|
+
* **This is not a public wire shape.** Nothing here is served to a customer,
|
|
7
|
+
* appears in OpenAPI, or is reachable from the internet — a Durable Object has
|
|
8
|
+
* no route, and only a Worker holding the namespace binding can address one.
|
|
9
|
+
* It lives in the contracts package for the single reason that two workspaces
|
|
10
|
+
* need to agree on it and this is the only package both `apps/api` and
|
|
11
|
+
* `apps/scheduler` may depend on. The alternative was each side writing the
|
|
12
|
+
* shape out separately, which is how `apps/scheduler/src/contract-runtime.ts`
|
|
13
|
+
* became a filed debt.
|
|
14
|
+
*
|
|
15
|
+
* It carries **no Zod and no runtime import of the contract barrel**. The
|
|
16
|
+
* addressing and routing functions below are a string join and a hash, and the
|
|
17
|
+
* rest is types, which are erased. That matters: `apps/scheduler` is the Worker
|
|
18
|
+
* every Durable Object cold start parses, and W2-08 measured the contract
|
|
19
|
+
* barrel's runtime exports at 3.4x its bundle (see
|
|
20
|
+
* `apps/scheduler/src/contract-runtime.ts`).
|
|
21
|
+
*
|
|
22
|
+
* **`@greatwait/contracts/partition` is a subpath export for that reason**,
|
|
23
|
+
* added by W2-05 when the scheduler needed the route function at runtime rather
|
|
24
|
+
* than only its types. Importing it through the barrel would have reached Zod
|
|
25
|
+
* and undone the whole of `contract-runtime.ts`. This is a narrow instance of
|
|
26
|
+
* what [FXO-1404](https://linear.app/flowxo/issue/FXO-1404) asks for in
|
|
27
|
+
* general — a Zod-free entry point — and it does not close that issue: the
|
|
28
|
+
* identifier, timestamp and problem helpers `contract-runtime.ts` copies still
|
|
29
|
+
* live in modules that do reach Zod.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The shard every partition had until W2-05, and the only one an unsharded
|
|
33
|
+
* deployment ever uses.
|
|
34
|
+
*
|
|
35
|
+
* Named rather than written as `"0"` at two call sites, because the API Worker
|
|
36
|
+
* and the partition must derive the same Durable Object name and a literal
|
|
37
|
+
* repeated across a workspace boundary is a literal that will eventually
|
|
38
|
+
* disagree with itself. `partitionShardFor` returns exactly this when the shard
|
|
39
|
+
* count is one, so every Durable Object name a single-shard deployment produces
|
|
40
|
+
* is byte-identical to the ones it produced before sharding existed.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SINGLE_SHARD = "0";
|
|
43
|
+
/**
|
|
44
|
+
* Projection intents handed over in one call.
|
|
45
|
+
*
|
|
46
|
+
* **Declared here because both sides of the seam enforce it, and a bound
|
|
47
|
+
* enforced twice from two workspaces is a bound that will eventually disagree
|
|
48
|
+
* with itself.** The API Worker asks for this many; the partition clamps what it
|
|
49
|
+
* hands over to it and *refuses* a settle carrying more. If the asking number
|
|
50
|
+
* ever exceeded the refusing one, every settle would be refused, no intent would
|
|
51
|
+
* ever be retired, the outbox would grow to `PARTITION_LIMITS.outboxRows` and
|
|
52
|
+
* creates would then start failing for bookkeeping — a customer-visible outage
|
|
53
|
+
* produced by two constants drifting apart. The test-gap audit named exactly
|
|
54
|
+
* that, and the fix is one declaration rather than a test comparing two.
|
|
55
|
+
*
|
|
56
|
+
* Thirty-two is one D1 batch's worth of work and a few kilobytes of response.
|
|
57
|
+
* The caller comes back for the rest, and `RetainedProjections.pending` is how
|
|
58
|
+
* it knows to.
|
|
59
|
+
*/
|
|
60
|
+
export declare const PROJECTION_DRAIN_LIMIT = 32;
|
|
61
|
+
/**
|
|
62
|
+
* The most shards one project/environment may be spread across.
|
|
63
|
+
*
|
|
64
|
+
* Eight, and the number is borrowed rather than invented:
|
|
65
|
+
* `apps/scheduler/src/routing.ts` clamps `HOST_EXECUTOR_SHARDS` at eight, and a
|
|
66
|
+
* repository carrying two different bounds on two knobs of the same shape is a
|
|
67
|
+
* repository where somebody eventually applies the wrong one. The costs differ
|
|
68
|
+
* but they point the same way — an executor shard divides a host budget, and a
|
|
69
|
+
* partition shard divides a project's schedules across Durable Objects.
|
|
70
|
+
*
|
|
71
|
+
* A listing had to fan out across all of them until W2-06 served listings from
|
|
72
|
+
* D1, which was the sharpest reason for a bound and is now gone. What is left is
|
|
73
|
+
* still a real fan-out: a listing whose projection is stale re-confirms every
|
|
74
|
+
* partition it covers, and a repair after an outage hands over from each in
|
|
75
|
+
* turn. Eight is where that stops being a request.
|
|
76
|
+
*
|
|
77
|
+
* A value above this is clamped rather than refused, which is upstream's
|
|
78
|
+
* judgement in `executorShardCount` and holds for the same reason: an operator
|
|
79
|
+
* asking for sixteen still wants sharding, just bounded.
|
|
80
|
+
*/
|
|
81
|
+
export declare const MAX_PARTITION_SHARDS = 8;
|
|
82
|
+
/**
|
|
83
|
+
* How many shards a deployment spreads one project/environment across.
|
|
84
|
+
*
|
|
85
|
+
* Reads the raw environment string rather than an `Env`, so both Workers parse
|
|
86
|
+
* it the same way without either of them naming the other's bindings. Missing,
|
|
87
|
+
* unparseable, non-integer and below-one all resolve to one — the unsharded
|
|
88
|
+
* default — because a partition count is the thing a *routing* decision is made
|
|
89
|
+
* from, and a routing decision made from a garbage value is worse than one made
|
|
90
|
+
* from the default.
|
|
91
|
+
*
|
|
92
|
+
* **Changing it moves where existing identifiers resolve, so it is retunable
|
|
93
|
+
* only under a precondition, and that is the most important thing to know about
|
|
94
|
+
* it.** A schedule's shard is derived from its identifier. Before W2-06 that was
|
|
95
|
+
* the only answer, so raising the count stranded every schedule already stored.
|
|
96
|
+
* Now the shard a schedule was actually stored in is *recorded* in the D1
|
|
97
|
+
* directory, and `resolveSchedule` falls back to it whenever the derived
|
|
98
|
+
* partition has nothing — so a schedule created under an older count keeps
|
|
99
|
+
* working, because its route was written down rather than recomputed.
|
|
100
|
+
*
|
|
101
|
+
* The precondition is that every schedule **has** such a route, which is exactly
|
|
102
|
+
* what `greatwait_projection_checkpoint` reporting `pending: 0` for every
|
|
103
|
+
* partition means. A schedule whose projection never landed has no recorded
|
|
104
|
+
* route and no derivable one, and it is unreachable until its partition is
|
|
105
|
+
* drained. `docs/operations/cloudflare-environments.md` carries the procedure
|
|
106
|
+
* and the two costs it does not remove — a keyed create's idempotency receipt is
|
|
107
|
+
* stranded for its 24-hour life, and detail reads of pre-change schedules cost
|
|
108
|
+
* an extra round trip for as long as they exist.
|
|
109
|
+
*
|
|
110
|
+
* It is still not `HOST_EXECUTOR_SHARDS`, which rolls `1 → 4 → 8` under load
|
|
111
|
+
* because an executor holds no durable state. This one holds all of it, and
|
|
112
|
+
* moving it is a planned operation rather than a dial.
|
|
113
|
+
*/
|
|
114
|
+
export declare function partitionShardCount(raw: string | undefined): number;
|
|
115
|
+
/**
|
|
116
|
+
* The shard a schedule identifier belongs to.
|
|
117
|
+
*
|
|
118
|
+
* Architecture section 4's route function: the stable opaque schedule ID hashed
|
|
119
|
+
* into a project/environment shard, with the count an operational configuration
|
|
120
|
+
* rather than a public contract. Nothing a *client* sends reaches this — the
|
|
121
|
+
* identifier is minted by the API Worker — so a caller can neither choose their
|
|
122
|
+
* shard nor learn how many there are.
|
|
123
|
+
*/
|
|
124
|
+
export declare function partitionShardFor(scheduleId: string, shardCount: number): string;
|
|
125
|
+
/**
|
|
126
|
+
* The shard a create carrying an idempotency key routes to.
|
|
127
|
+
*
|
|
128
|
+
* **A create is the one operation that cannot route by schedule identifier,
|
|
129
|
+
* because it is the operation that mints one.** Every other operation is handed
|
|
130
|
+
* an identifier and derives the shard from it; a create has to pick a partition
|
|
131
|
+
* first and name the schedule afterwards. Picking randomly would break
|
|
132
|
+
* idempotency the moment there is more than one shard: the retry that carries
|
|
133
|
+
* the same key would mint a different identifier, land in a different Durable
|
|
134
|
+
* Object, find no receipt there, and create a second schedule — the exact
|
|
135
|
+
* failure the receipt exists to prevent.
|
|
136
|
+
*
|
|
137
|
+
* So a keyed create routes on the key, and the identifier is then minted *into*
|
|
138
|
+
* that shard (`mintScheduleIdIn`), which keeps `partitionShardFor` the single
|
|
139
|
+
* answer for every later read. A create with no key routes on the identifier it
|
|
140
|
+
* mints, because there is nothing to be idempotent about.
|
|
141
|
+
*
|
|
142
|
+
* The `idk:` prefix separates the two domains, so an identifier and a key that
|
|
143
|
+
* happen to be the same string cannot be conflated into the same hash input.
|
|
144
|
+
* The key is customer input and it selects a shard **inside the caller's own
|
|
145
|
+
* authority tuple**: it cannot reach another organization, project or
|
|
146
|
+
* environment, because those are the other components of the Durable Object
|
|
147
|
+
* name and none of them comes from the request.
|
|
148
|
+
*/
|
|
149
|
+
export declare function partitionShardForIdempotencyKey(key: string, shardCount: number): string;
|
|
150
|
+
/**
|
|
151
|
+
* The Durable Object instance name for one authority tuple and shard.
|
|
152
|
+
*
|
|
153
|
+
* Deriving the name from the authority is what makes a cross-tenant read reach
|
|
154
|
+
* a *different object*, so tenancy is enforced by addressing before any code
|
|
155
|
+
* runs. The partition's own bound-identity check is then defence against a
|
|
156
|
+
* defect in this function rather than the only thing standing between two
|
|
157
|
+
* tenants.
|
|
158
|
+
*
|
|
159
|
+
* `:` is safe as a separator because every component is either a prefixed
|
|
160
|
+
* identifier drawn from Crockford base32 or the fixed literal `test`/`live`,
|
|
161
|
+
* and none of those can contain it. Callers must validate the tuple before
|
|
162
|
+
* calling: this function makes a name, it does not police one.
|
|
163
|
+
*/
|
|
164
|
+
export declare function partitionInstanceName(authority: Authority, shard: string): string;
|
|
165
|
+
/**
|
|
166
|
+
* Why a partition refused.
|
|
167
|
+
*
|
|
168
|
+
* `not-found` covers both "no such schedule" and "this partition is bound to
|
|
169
|
+
* someone else", deliberately: the caller must not be able to tell those apart,
|
|
170
|
+
* and a refusal that named the bound authority would be a refusal that named
|
|
171
|
+
* another tenant.
|
|
172
|
+
*
|
|
173
|
+
* `version-conflict` and `state-conflict` are separate answers because they
|
|
174
|
+
* have separate remedies. The first says somebody else wrote first and the
|
|
175
|
+
* caller should re-read and decide again; the second says nobody did and this
|
|
176
|
+
* transition is simply not available from where the schedule is. Collapsing
|
|
177
|
+
* them would make a client retry a conditional write that can never succeed.
|
|
178
|
+
*
|
|
179
|
+
* `invalid-request` and `unavailable` are separate for a sharper reason: they
|
|
180
|
+
* differ in **whose fault it is**. `invalid-request` is something the caller
|
|
181
|
+
* sent — an instant that is not one. `unavailable` is something we did: an
|
|
182
|
+
* authority tuple built from a verified principal that this partition cannot
|
|
183
|
+
* read, or a partition bound to a tuple other than the one that addressed it.
|
|
184
|
+
* One refusal covered both until W2-02's review, and the cost was the same
|
|
185
|
+
* internal fault surfacing as a 400 that told the caller their own tuple was
|
|
186
|
+
* malformed on six routes and a 503 on the seventh — with that 400 unpublished
|
|
187
|
+
* on the read operation, because no caller can cause it.
|
|
188
|
+
*/
|
|
189
|
+
export type PartitionRefusal = "idempotency-probe-miss" | "invalid-request" | "not-found" | "state-conflict" | "unavailable" | "version-conflict";
|
|
190
|
+
/**
|
|
191
|
+
* A refusal, and optionally what specifically could not be accepted.
|
|
192
|
+
*
|
|
193
|
+
* The detail exists because `invalid-request` stopped being a single answer the
|
|
194
|
+
* moment a schedule could be defined four ways. The adapter used to hard-code
|
|
195
|
+
* one sentence naming `at` and `delaySeconds`, which was true while those were
|
|
196
|
+
* the only arms and became a wrong answer for a cron expression that does not
|
|
197
|
+
* parse. It is a short, caller-facing phrase and never a stored value or an
|
|
198
|
+
* identifier: a refusal must not be able to carry another tenant's data out of
|
|
199
|
+
* a partition, which is why the refusals that could — `not-found` above — say
|
|
200
|
+
* nothing beyond themselves.
|
|
201
|
+
*/
|
|
202
|
+
export interface PartitionRefused {
|
|
203
|
+
readonly detail?: string;
|
|
204
|
+
readonly refusal: PartitionRefusal;
|
|
205
|
+
}
|
|
206
|
+
export type PartitionOutcome = PartitionRefused | {
|
|
207
|
+
readonly view: ScheduleView;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* One retained projection intent, with the schedule it is about.
|
|
211
|
+
*
|
|
212
|
+
* **The partition reads the schedule at drain time rather than carrying a copy
|
|
213
|
+
* in the outbox row.** The stored `body` is `{schedule, version}` and W2-06
|
|
214
|
+
* deliberately left it that way: widening it would have been a format change
|
|
215
|
+
* across a deployment window for no gain, because superseding already keeps at
|
|
216
|
+
* most one pending intent per schedule and that intent always names the newest
|
|
217
|
+
* version. Reading the row at drain time is therefore the same answer, and it is
|
|
218
|
+
* the answer that stays right if the schedule moved on in between.
|
|
219
|
+
*
|
|
220
|
+
* **`schedule` absent is a tombstone.** The partition retained an intent for a
|
|
221
|
+
* schedule it no longer holds, so the directory must stop listing it rather than
|
|
222
|
+
* keep a summary nothing can repair.
|
|
223
|
+
*
|
|
224
|
+
* **`scheduleId` absent is an intent this build cannot route.** The outbox is a
|
|
225
|
+
* general event log — `event_type` carries no CHECK precisely so adding a type
|
|
226
|
+
* is not a partition migration — so an intent naming no schedule is one a later
|
|
227
|
+
* story enqueued and this drainer does not understand. It is acknowledged rather
|
|
228
|
+
* than retained, which is the kernel's unknown-kind rule and it has the same
|
|
229
|
+
* consequence: **rolling the API Worker back past a story that adds an event
|
|
230
|
+
* type discards that story's pending intents**, exactly as rolling the scheduler
|
|
231
|
+
* back past W2-04 discards its `advance-series` tasks. A story adding an event
|
|
232
|
+
* type teaches the drainer about it in the same change.
|
|
233
|
+
*/
|
|
234
|
+
export interface ProjectionIntent {
|
|
235
|
+
/**
|
|
236
|
+
* How many hand-overs of this intent have already failed.
|
|
237
|
+
*
|
|
238
|
+
* Carried so the caller can back a failing intent off without keeping state
|
|
239
|
+
* of its own — and, more importantly, so it can decline to back off the
|
|
240
|
+
* *first* failure. A blanket delay would lock a schedule out of listings for
|
|
241
|
+
* the length of it after one transient error, which is the common case; a
|
|
242
|
+
* poison intent that fails every time is the rare one, and it is the one the
|
|
243
|
+
* backoff exists for.
|
|
244
|
+
*/
|
|
245
|
+
readonly attempts: number;
|
|
246
|
+
readonly eventId: string;
|
|
247
|
+
readonly eventType: string;
|
|
248
|
+
/** The schedule as the partition holds it now. Absent means it is gone. */
|
|
249
|
+
readonly schedule?: Schedule;
|
|
250
|
+
/** Absent on an intent that names no schedule. */
|
|
251
|
+
readonly scheduleId?: string;
|
|
252
|
+
/** The version the intent was enqueued for. Absent with `scheduleId`. */
|
|
253
|
+
readonly scheduleVersion?: number;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* What a partition is still holding for the control plane.
|
|
257
|
+
*
|
|
258
|
+
* **`pending` is the whole retained count and `intents` is only what fits in one
|
|
259
|
+
* hand-over**, which is what lets a caller tell "you have everything" from "come
|
|
260
|
+
* back for more" without a second call. It also counts intents that are
|
|
261
|
+
* *deferred* — postponed after a failed send and therefore not offered yet — so
|
|
262
|
+
* a directory that reports itself caught up while a partition is sitting on a
|
|
263
|
+
* backlog is not a state this shape can express.
|
|
264
|
+
*/
|
|
265
|
+
export interface RetainedProjections {
|
|
266
|
+
readonly intents: readonly ProjectionIntent[];
|
|
267
|
+
/** Intents retained in total, counted no further than the outbox bound. */
|
|
268
|
+
readonly pending: number;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* What a write answers.
|
|
272
|
+
*
|
|
273
|
+
* The same view a read answers, plus whatever the control plane still owes —
|
|
274
|
+
* which for a healthy partition is the one intent this write just enqueued, and
|
|
275
|
+
* after an outage is that one plus whatever else is waiting. Carried on the
|
|
276
|
+
* outcome rather than fetched by a second call, because the write and its intent
|
|
277
|
+
* were committed together and a round trip to ask "what did you just record" is
|
|
278
|
+
* a round trip to learn something the callee already knew.
|
|
279
|
+
*
|
|
280
|
+
* Carrying the *other* schedules' intents is what makes any write after an
|
|
281
|
+
* outage repair the schedules written during it, which is acceptance criterion 4
|
|
282
|
+
* on the cheapest path there is.
|
|
283
|
+
*/
|
|
284
|
+
export type ScheduleWriteOutcome = PartitionRefused | {
|
|
285
|
+
readonly projections: RetainedProjections;
|
|
286
|
+
readonly view: ScheduleView;
|
|
287
|
+
};
|
|
288
|
+
/** Read the intents a partition is still holding, oldest first. */
|
|
289
|
+
export interface DrainProjectionsQuery {
|
|
290
|
+
readonly authority: Authority;
|
|
291
|
+
readonly limit: number;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* What the partition is still holding.
|
|
295
|
+
*
|
|
296
|
+
* **Nothing is deleted here.** The intents stay until `settleProjections` says
|
|
297
|
+
* the control plane has them, which is acceptance criterion 1: an intent
|
|
298
|
+
* survives a drain whose D1 write never landed, and a partition that has never
|
|
299
|
+
* been initialized answers an empty drain rather than a refusal — a shard nobody
|
|
300
|
+
* has written to owes the directory nothing.
|
|
301
|
+
*/
|
|
302
|
+
export type DrainProjectionsOutcome = PartitionRefused | RetainedProjections;
|
|
303
|
+
/**
|
|
304
|
+
* Retire the intents the control plane accepted, and postpone the rest.
|
|
305
|
+
*
|
|
306
|
+
* Both halves in one call rather than two, because a drain has exactly one of
|
|
307
|
+
* these outcomes and the failing half must not need a healthy D1 to be recorded:
|
|
308
|
+
* the deferral is what stops a control-plane outage from making every subsequent
|
|
309
|
+
* request pay a D1 timeout for the same intent.
|
|
310
|
+
*/
|
|
311
|
+
export interface SettleProjectionsCommand {
|
|
312
|
+
/** Intents the control plane has durably applied, or cannot route. */
|
|
313
|
+
readonly acknowledged: readonly string[];
|
|
314
|
+
readonly authority: Authority;
|
|
315
|
+
/** Intents whose apply failed, and the instant to offer them again. */
|
|
316
|
+
readonly deferred?: {
|
|
317
|
+
readonly eventIds: readonly string[];
|
|
318
|
+
readonly nextAttemptAtMs: number;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export type SettleProjectionsOutcome = PartitionRefused | {
|
|
322
|
+
readonly acknowledged: number;
|
|
323
|
+
readonly deferred: number;
|
|
324
|
+
/** Intents still retained after this call. */
|
|
325
|
+
readonly pending: number;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* What the partition did with an idempotency key, and what it had on file.
|
|
329
|
+
*
|
|
330
|
+
* The partition reports rather than decides. `classifyIdempotency` is the
|
|
331
|
+
* decision and it is pure, so it runs once, in the adapter that also knows the
|
|
332
|
+
* caller's scope — and this carries back the only thing the adapter cannot
|
|
333
|
+
* know, which is whether a live receipt was already there and what request it
|
|
334
|
+
* was for. The partition's own contribution is atomicity: the receipt is
|
|
335
|
+
* written in the same transaction as the schedule, so a key can never be
|
|
336
|
+
* recorded for a create that did not happen, nor a create happen twice under
|
|
337
|
+
* one key.
|
|
338
|
+
*/
|
|
339
|
+
export interface IdempotencyDisposition {
|
|
340
|
+
/** The fingerprint on the receipt already on file, if there was one. */
|
|
341
|
+
readonly storedFingerprint?: string;
|
|
342
|
+
/** When that receipt was written, epoch milliseconds. */
|
|
343
|
+
readonly storedAtMs?: number;
|
|
344
|
+
/** False when a live receipt already existed, so nothing was written. */
|
|
345
|
+
readonly written: boolean;
|
|
346
|
+
}
|
|
347
|
+
export type CreateScheduleOutcome = PartitionRefused | {
|
|
348
|
+
readonly idempotency?: IdempotencyDisposition;
|
|
349
|
+
/** What the control plane still owes for this partition. */
|
|
350
|
+
readonly projections: RetainedProjections;
|
|
351
|
+
readonly view: ScheduleView;
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* How a schedule recurs, as the seam carries it and the partition stores it.
|
|
355
|
+
*
|
|
356
|
+
* Close to the published `Recurrence` and deliberately not the same type. This
|
|
357
|
+
* one's `at` arm carries the instant, because a create has to say *when*; the
|
|
358
|
+
* published one does not, because a client reads the instant off the
|
|
359
|
+
* occurrence and two copies of it would be two things to disagree after a
|
|
360
|
+
* replace. Everything else is identical, and the partition writes this shape
|
|
361
|
+
* into `greatwait_schedule.definition` verbatim — which for a one-shot is
|
|
362
|
+
* byte-identical to what W2-02 already wrote, so no stored row is rewritten.
|
|
363
|
+
*
|
|
364
|
+
* `timeZone` is required on the `cron` arm rather than optional: the adapter
|
|
365
|
+
* resolves the caller's omission to `UTC` once, at acceptance, so the stored
|
|
366
|
+
* definition always names the wall clock it is read against and no later reader
|
|
367
|
+
* has to know what the default was on the day it was created.
|
|
368
|
+
*/
|
|
369
|
+
export type ScheduleDefinition = {
|
|
370
|
+
readonly at: string;
|
|
371
|
+
readonly kind: "at";
|
|
372
|
+
} | {
|
|
373
|
+
readonly everySeconds: number;
|
|
374
|
+
readonly kind: "every";
|
|
375
|
+
} | {
|
|
376
|
+
readonly cron: string;
|
|
377
|
+
readonly kind: "cron";
|
|
378
|
+
readonly timeZone: string;
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Create one schedule in the partition owning `authority`.
|
|
382
|
+
*
|
|
383
|
+
* **`definition` is the whole of when it happens**, and which side of the seam
|
|
384
|
+
* resolves each arm is a deliberate split rather than an inconsistency. The
|
|
385
|
+
* `at` arm carries an instant and never a delay, because converting a delay is
|
|
386
|
+
* the adapter's job, done once against the clock the request arrived on — the
|
|
387
|
+
* partition is reached again by every retry and every replay, and would convert
|
|
388
|
+
* a second time and land the schedule somewhere the caller never asked for. The
|
|
389
|
+
* `every` and `cron` arms carry the *definition* and not a first instant,
|
|
390
|
+
* because resolving those needs a cron parser and a timezone database and
|
|
391
|
+
* belongs to whoever will resolve every later occurrence too. Two engines
|
|
392
|
+
* answering "when does this cron next fire" is the drift that matters here, and
|
|
393
|
+
* `.architecture-boundaries.json` does not permit `apps/api` to depend on
|
|
394
|
+
* `packages/scheduling` anyway.
|
|
395
|
+
*/
|
|
396
|
+
export interface CreateScheduleCommand {
|
|
397
|
+
/**
|
|
398
|
+
* **Transitional, and the reason this seam has a safe deployment order.**
|
|
399
|
+
*
|
|
400
|
+
* `apps/api` and `apps/scheduler` are separate Workers, deployed by separate
|
|
401
|
+
* sequential commands, so between them one runs new code and the other runs
|
|
402
|
+
* old. Before W2-04 this command carried `at: string` and no `definition`;
|
|
403
|
+
* replacing one field with the other would have broken **both** orderings —
|
|
404
|
+
* an old API Worker's command would reach a new partition with no
|
|
405
|
+
* `definition` at all, and a new API Worker's command would reach an old
|
|
406
|
+
* partition with no `at`. W2-02 could say "deploy the scheduler first"
|
|
407
|
+
* because it only *added* methods, and W2-03 needed no order at all because
|
|
408
|
+
* it changed no method; this is the first story to change the shape of one,
|
|
409
|
+
* and a shape change with no safe order is an outage during a routine
|
|
410
|
+
* deploy.
|
|
411
|
+
*
|
|
412
|
+
* So a partition accepts either: `definition` when the caller sends it, and
|
|
413
|
+
* this instant when it does not. That makes **scheduler first** safe, which
|
|
414
|
+
* is the order recorded in the W2-04 evidence. Remove it once both Workers
|
|
415
|
+
* are past this story — [FXO-1438](https://linear.app/flowxo/issue/FXO-1438/remove-the-transitional-at-field-from-the-partition-create-and-replace).
|
|
416
|
+
*
|
|
417
|
+
* It is declared here rather than only read defensively in the partition so
|
|
418
|
+
* that the compatibility window is visible to a reader of the seam, and so
|
|
419
|
+
* deleting it is a contract change somebody has to make on purpose.
|
|
420
|
+
*/
|
|
421
|
+
readonly at?: string;
|
|
422
|
+
readonly authority: Authority;
|
|
423
|
+
readonly definition: ScheduleDefinition;
|
|
424
|
+
/**
|
|
425
|
+
* The schedule's delivery settings, resolved by the adapter.
|
|
426
|
+
*
|
|
427
|
+
* Optional for the reason `at` above is: `apps/api` and `apps/scheduler` are
|
|
428
|
+
* deployed by separate commands, so an API Worker one release behind sends a
|
|
429
|
+
* command without it. The partition then stores the shipped defaults, which is
|
|
430
|
+
* exactly the behaviour every schedule had before this field existed — so
|
|
431
|
+
* either deployment order is safe and neither produces a schedule nobody
|
|
432
|
+
* asked for.
|
|
433
|
+
*/
|
|
434
|
+
readonly delivery?: {
|
|
435
|
+
readonly completion: "response" | "callback";
|
|
436
|
+
readonly maxDurationSeconds: number;
|
|
437
|
+
readonly onOverlap: "skip" | "queue";
|
|
438
|
+
};
|
|
439
|
+
/** Verified callback endpoint selected under `authority`, when this delivers. */
|
|
440
|
+
readonly endpointId?: string;
|
|
441
|
+
/** Present only when the caller sent an idempotency key. */
|
|
442
|
+
readonly idempotency?: {
|
|
443
|
+
readonly fingerprint: string;
|
|
444
|
+
readonly key: string;
|
|
445
|
+
/**
|
|
446
|
+
* Receipt lookup only. A miss returns `idempotency-probe-miss` without
|
|
447
|
+
* binding a partition, creating a schedule, or writing any other state.
|
|
448
|
+
*/
|
|
449
|
+
readonly probeOnly?: boolean;
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* How promptly the schedule's occurrences expect to be served.
|
|
453
|
+
*
|
|
454
|
+
* Resolved by the adapter, from the caller's `latencyClass` when they sent
|
|
455
|
+
* one and from the recurrence when they did not — the same derivation the
|
|
456
|
+
* copied `validate.ts` makes, and it is the adapter's because the alternative
|
|
457
|
+
* is the partition and the MCP surface each deriving it again. Optional here
|
|
458
|
+
* on the same terms as `at`: a partition addressed by an API Worker deployed
|
|
459
|
+
* before this story receives no class and defaults it, which is what makes
|
|
460
|
+
* **scheduler first** a safe deploy order.
|
|
461
|
+
*/
|
|
462
|
+
readonly latencyClass?: LatencyClass;
|
|
463
|
+
/**
|
|
464
|
+
* The caller's fairness lane within their own partition, when they named one.
|
|
465
|
+
*
|
|
466
|
+
* It selects a pacing lane and a telemetry label and nothing else. It cannot
|
|
467
|
+
* change the authority tuple, and — unlike the shard — it is not part of the
|
|
468
|
+
* Durable Object name, so no value of it can reach another object.
|
|
469
|
+
*/
|
|
470
|
+
readonly partitionKey?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Authenticated encrypted customer JSON. The plaintext never crosses this
|
|
473
|
+
* seam and is never stored in a D1 projection.
|
|
474
|
+
*/
|
|
475
|
+
readonly payloadEnvelope?: string;
|
|
476
|
+
/** UTF-8 byte length of the plaintext JSON, before encryption. */
|
|
477
|
+
readonly payloadBytes?: number;
|
|
478
|
+
/**
|
|
479
|
+
* The verified endpoint URL captured for durable host routing.
|
|
480
|
+
*
|
|
481
|
+
* This is internal metadata, not a public response field. The Host Executor
|
|
482
|
+
* re-reads the D1 endpoint row before signing and reroutes if the URL has
|
|
483
|
+
* changed. It does not resolve or revalidate the address: that boundary was
|
|
484
|
+
* removed by FXO-1566 and FXO-1581, and this comment claimed it afterwards.
|
|
485
|
+
*/
|
|
486
|
+
readonly routingUrl?: string;
|
|
487
|
+
/**
|
|
488
|
+
* The identifier the schedule will have, minted by the adapter.
|
|
489
|
+
*
|
|
490
|
+
* **The partition stopped minting this because the route function needs it
|
|
491
|
+
* first.** A schedule's shard is `partitionShardFor(scheduleId)`, so the
|
|
492
|
+
* caller has to know the identifier before it knows which Durable Object to
|
|
493
|
+
* address; an identifier minted inside the partition would be one whose shard
|
|
494
|
+
* nothing had routed to. Optional for the deployment window's sake — a
|
|
495
|
+
* partition still mints one when an older API Worker sends none, and at one
|
|
496
|
+
* shard the two agree by construction because every identifier resolves to
|
|
497
|
+
* `SINGLE_SHARD`.
|
|
498
|
+
*
|
|
499
|
+
* Remove the fallback once both Workers are past this story, on the same
|
|
500
|
+
* terms as `at`.
|
|
501
|
+
*/
|
|
502
|
+
readonly scheduleId?: string;
|
|
503
|
+
readonly shard: string;
|
|
504
|
+
}
|
|
505
|
+
/** Read one schedule from the partition owning `authority`. */
|
|
506
|
+
export interface ReadScheduleQuery {
|
|
507
|
+
readonly authority: Authority;
|
|
508
|
+
readonly scheduleId: string;
|
|
509
|
+
}
|
|
510
|
+
/** List the partition's schedules, oldest first. */
|
|
511
|
+
export interface ListSchedulesQuery {
|
|
512
|
+
readonly authority: Authority;
|
|
513
|
+
/** Where the previous page stopped. Absent for the first page. */
|
|
514
|
+
readonly after?: {
|
|
515
|
+
readonly createdAtMs: number;
|
|
516
|
+
readonly id: string;
|
|
517
|
+
};
|
|
518
|
+
/** Whether archived schedules are included. */
|
|
519
|
+
readonly includeArchived: boolean;
|
|
520
|
+
readonly limit: number;
|
|
521
|
+
}
|
|
522
|
+
export type ListSchedulesOutcome = PartitionRefused | {
|
|
523
|
+
readonly schedules: readonly Schedule[];
|
|
524
|
+
/**
|
|
525
|
+
* Whether a further page exists, decided from a row the partition
|
|
526
|
+
* scanned and did not serve. `schedules` is never longer than the limit
|
|
527
|
+
* asked for — an earlier version of this comment said it was, and a
|
|
528
|
+
* reader who believed it would trim the last item off every full page.
|
|
529
|
+
*/
|
|
530
|
+
readonly hasMore: boolean;
|
|
531
|
+
};
|
|
532
|
+
/** Stable keyset position in one schedule's occurrence history. */
|
|
533
|
+
export interface OccurrencePagePosition {
|
|
534
|
+
readonly createdAtMs: number;
|
|
535
|
+
readonly id: string;
|
|
536
|
+
}
|
|
537
|
+
/** List retained occurrences from their authoritative partition. */
|
|
538
|
+
export interface ListOccurrencesQuery {
|
|
539
|
+
/** Where the previous page stopped, including the identifier tie-break. */
|
|
540
|
+
readonly after?: OccurrencePagePosition;
|
|
541
|
+
readonly authority: Authority;
|
|
542
|
+
readonly limit: number;
|
|
543
|
+
readonly scheduleId: string;
|
|
544
|
+
}
|
|
545
|
+
export type ListOccurrencesOutcome = PartitionRefused | {
|
|
546
|
+
readonly hasMore: boolean;
|
|
547
|
+
readonly next?: OccurrencePagePosition;
|
|
548
|
+
readonly occurrences: readonly Occurrence[];
|
|
549
|
+
};
|
|
550
|
+
/** Read one retained occurrence, scoped through its owning schedule. */
|
|
551
|
+
export interface ReadOccurrenceQuery {
|
|
552
|
+
readonly authority: Authority;
|
|
553
|
+
readonly occurrenceId: string;
|
|
554
|
+
readonly scheduleId: string;
|
|
555
|
+
}
|
|
556
|
+
export type ReadOccurrenceOutcome = PartitionRefused | {
|
|
557
|
+
readonly view: OccurrenceView;
|
|
558
|
+
};
|
|
559
|
+
/** Stable keyset position in one occurrence's immutable attempt history. */
|
|
560
|
+
export interface AttemptPagePosition {
|
|
561
|
+
readonly id: string;
|
|
562
|
+
readonly startedAtMs: number;
|
|
563
|
+
}
|
|
564
|
+
/** List retained attempts from their occurrence's authoritative partition. */
|
|
565
|
+
export interface ListAttemptsQuery {
|
|
566
|
+
/** Where the previous page stopped, including the identifier tie-break. */
|
|
567
|
+
readonly after?: AttemptPagePosition;
|
|
568
|
+
readonly authority: Authority;
|
|
569
|
+
readonly limit: number;
|
|
570
|
+
readonly occurrenceId: string;
|
|
571
|
+
readonly scheduleId: string;
|
|
572
|
+
}
|
|
573
|
+
export type ListAttemptsOutcome = PartitionRefused | {
|
|
574
|
+
readonly attempts: readonly Attempt[];
|
|
575
|
+
readonly hasMore: boolean;
|
|
576
|
+
readonly next?: AttemptPagePosition;
|
|
577
|
+
};
|
|
578
|
+
/** Read one attempt, scoped through both its occurrence and schedule. */
|
|
579
|
+
export interface ReadAttemptQuery {
|
|
580
|
+
readonly attemptId: string;
|
|
581
|
+
readonly authority: Authority;
|
|
582
|
+
readonly occurrenceId: string;
|
|
583
|
+
readonly scheduleId: string;
|
|
584
|
+
}
|
|
585
|
+
export type ReadAttemptOutcome = PartitionRefused | {
|
|
586
|
+
readonly view: AttemptView;
|
|
587
|
+
};
|
|
588
|
+
/**
|
|
589
|
+
* Create one manual attempt under a retained occurrence.
|
|
590
|
+
*
|
|
591
|
+
* The body contributes no authority and no mutable attempt fields. The
|
|
592
|
+
* adapter fingerprints the fixed path action and the partition serializes the
|
|
593
|
+
* receipt with the new attempt, so a same-key race has one durable answer.
|
|
594
|
+
*/
|
|
595
|
+
export interface RedeliverOccurrenceCommand {
|
|
596
|
+
readonly authority: Authority;
|
|
597
|
+
readonly idempotency: {
|
|
598
|
+
readonly fingerprint: string;
|
|
599
|
+
readonly key: string;
|
|
600
|
+
};
|
|
601
|
+
readonly occurrenceId: string;
|
|
602
|
+
readonly scheduleId: string;
|
|
603
|
+
}
|
|
604
|
+
export type RedeliverOccurrenceOutcome = PartitionRefused | {
|
|
605
|
+
readonly idempotency: IdempotencyDisposition;
|
|
606
|
+
readonly view: AttemptView;
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* A write against an existing schedule.
|
|
610
|
+
*
|
|
611
|
+
* `expectedVersion` is the caller's precondition and is absent when they sent
|
|
612
|
+
* none. Absent means an unconditional write, not "whatever the current version
|
|
613
|
+
* is": the partition reads the row inside the same transaction it writes it, so
|
|
614
|
+
* an unconditional write is atomic without inventing a precondition nobody
|
|
615
|
+
* stated.
|
|
616
|
+
*/
|
|
617
|
+
export interface ScheduleWriteCommand {
|
|
618
|
+
readonly authority: Authority;
|
|
619
|
+
readonly expectedVersion?: number;
|
|
620
|
+
readonly scheduleId: string;
|
|
621
|
+
}
|
|
622
|
+
/** Replace a schedule's definition, starting a new generation. */
|
|
623
|
+
export interface ReplaceScheduleCommand extends ScheduleWriteCommand {
|
|
624
|
+
/**
|
|
625
|
+
* The replacement's delivery settings.
|
|
626
|
+
*
|
|
627
|
+
* Optional for the same deployment reason `CreateScheduleCommand.delivery`
|
|
628
|
+
* is: an API Worker one release behind sends nothing, and the partition then
|
|
629
|
+
* leaves the stored settings alone rather than resetting a paying customer's
|
|
630
|
+
* twelve-hour schedule to five seconds during a rolling deploy.
|
|
631
|
+
*/
|
|
632
|
+
readonly delivery?: {
|
|
633
|
+
readonly completion: "response" | "callback";
|
|
634
|
+
readonly maxDurationSeconds: number;
|
|
635
|
+
readonly onOverlap: "skip" | "queue";
|
|
636
|
+
};
|
|
637
|
+
/** Transitional, for the reason `CreateScheduleCommand.at` gives. */
|
|
638
|
+
readonly at?: string;
|
|
639
|
+
/** The replacement definition, on the same terms as a create's. */
|
|
640
|
+
readonly definition: ScheduleDefinition;
|
|
641
|
+
/** Replacement endpoint; absent preserves the stored endpoint. */
|
|
642
|
+
readonly endpointId?: string;
|
|
643
|
+
/**
|
|
644
|
+
* The class the replacement runs under, resolved the way a create's is.
|
|
645
|
+
*
|
|
646
|
+
* A replace carries this rather than inheriting it because the class is
|
|
647
|
+
* derived from the recurrence when the caller does not state one, and a
|
|
648
|
+
* replace is precisely the operation that changes the recurrence: a one-shot
|
|
649
|
+
* thirty seconds out that becomes a nightly cron is `realtime` becoming
|
|
650
|
+
* `bulk`, and keeping the old answer would pace a campaign in the lane a
|
|
651
|
+
* human is waiting in. Absent leaves the stored class alone, which is what an
|
|
652
|
+
* API Worker deployed before this story sends.
|
|
653
|
+
*/
|
|
654
|
+
readonly latencyClass?: LatencyClass;
|
|
655
|
+
/** Replacement encrypted payload; absent preserves the stored payload. */
|
|
656
|
+
readonly payloadEnvelope?: string;
|
|
657
|
+
/** Plaintext byte length paired with `payloadEnvelope`. */
|
|
658
|
+
readonly payloadBytes?: number;
|
|
659
|
+
/** Replacement routing URL paired with `endpointId`. */
|
|
660
|
+
readonly routingUrl?: string;
|
|
661
|
+
/**
|
|
662
|
+
* The fairness lane the schedule already runs in, echoed back unchanged.
|
|
663
|
+
*
|
|
664
|
+
* **Immutable after create, since FXO-1451.** W2-05 shipped it settable on
|
|
665
|
+
* replace and clearable by omission; `classifyChange` is what reversed that.
|
|
666
|
+
* Widening an accepted behaviour later is additive and narrowing it is
|
|
667
|
+
* breaking, so shipping immutable keeps both doors open and shipping mutable
|
|
668
|
+
* shuts one — and nobody has asked to move a schedule between fairness lanes.
|
|
669
|
+
*
|
|
670
|
+
* Not "reject any replace that carries it": the natural client flow is read,
|
|
671
|
+
* modify, write, and that body carries back the key it just read. So a value
|
|
672
|
+
* **equal** to the stored one is accepted, and a value that would change or
|
|
673
|
+
* set one is refused `invalid-request` naming the field.
|
|
674
|
+
*
|
|
675
|
+
* Omission means "leave it alone". An explicit `null` is a clear attempt and
|
|
676
|
+
* is refused like any other value that does not equal the stored key. The
|
|
677
|
+
* rolling Scheduler Durable Object compatibility rule that once treated
|
|
678
|
+
* `null` as omission ended when W8-10 retired that runtime.
|
|
679
|
+
*/
|
|
680
|
+
readonly partitionKey?: string;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Ask one partition what it expects to fire.
|
|
684
|
+
*
|
|
685
|
+
* The window is half-open — `fromMs <= instant < toMs` — and `bucketMs` must be
|
|
686
|
+
* a whole number of the partition's internal minute buckets, which the public
|
|
687
|
+
* request bound already guarantees. Sending anything else would make the
|
|
688
|
+
* re-aggregation approximate, and an approximate capacity number is one nobody
|
|
689
|
+
* can reconcile against a bill.
|
|
690
|
+
*/
|
|
691
|
+
export interface ForecastQuery {
|
|
692
|
+
readonly authority: Authority;
|
|
693
|
+
readonly bucketMs: number;
|
|
694
|
+
readonly fromMs: number;
|
|
695
|
+
readonly toMs: number;
|
|
696
|
+
}
|
|
697
|
+
/** One bucket, as one partition sees it. */
|
|
698
|
+
export interface ForecastPartitionBucket {
|
|
699
|
+
readonly bucketStartMs: number;
|
|
700
|
+
readonly byLatencyClass: Readonly<Record<LatencyClass, number>>;
|
|
701
|
+
readonly count: number;
|
|
702
|
+
/** Occurrences here that are the next firing of their schedule. */
|
|
703
|
+
readonly nextFireCount: number;
|
|
704
|
+
/**
|
|
705
|
+
* Named customer keys and their counts, ascending by key.
|
|
706
|
+
*
|
|
707
|
+
* **Pairs rather than an object**, because a null-prototype map is what stops
|
|
708
|
+
* a key called `__proto__` being swallowed by a prototype setter and
|
|
709
|
+
* structured clone refuses to serialize one — which arrives as an unreachable
|
|
710
|
+
* partition rather than as a type error. The caller rebuilds a
|
|
711
|
+
* null-prototype map where the answer is rendered.
|
|
712
|
+
*
|
|
713
|
+
* Named keys only: a schedule that chose none is in `unkeyedCount`.
|
|
714
|
+
*/
|
|
715
|
+
readonly partitionKeys: readonly {
|
|
716
|
+
readonly key: string;
|
|
717
|
+
readonly n: number;
|
|
718
|
+
}[];
|
|
719
|
+
readonly unkeyedCount: number;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* What one partition expects, and how old the answer is.
|
|
723
|
+
*
|
|
724
|
+
* `truncated` is a refusal in waiting rather than a caveat a caller may ignore.
|
|
725
|
+
* A partition that hit a projection bound is reporting a floor, and the one
|
|
726
|
+
* consumer this shape exists for reads a low number as a quiet book and scales
|
|
727
|
+
* *down* — so the API Worker turns it into `service_unavailable` rather than
|
|
728
|
+
* publishing a number that reads as smaller than the truth.
|
|
729
|
+
*/
|
|
730
|
+
export type ForecastOutcome = PartitionRefused | {
|
|
731
|
+
/**
|
|
732
|
+
* When this partition's snapshot was built.
|
|
733
|
+
*
|
|
734
|
+
* **Absent when it has none** — a partition nobody has written to holds
|
|
735
|
+
* nothing, so it has nothing to be stale about and must not vouch for
|
|
736
|
+
* freshness one way or the other. Reporting the read's own instant
|
|
737
|
+
* instead made the published `asOf` jitter between reads of the same
|
|
738
|
+
* unchanged book, because a project spans several shards and most of them
|
|
739
|
+
* are usually empty.
|
|
740
|
+
*/
|
|
741
|
+
readonly asOfMs?: number;
|
|
742
|
+
readonly buckets: readonly ForecastPartitionBucket[];
|
|
743
|
+
readonly truncated: boolean;
|
|
744
|
+
};
|
|
745
|
+
/**
|
|
746
|
+
* What a Scheduler Partition Durable Object answers.
|
|
747
|
+
*
|
|
748
|
+
* `SchedulerPartitionDO` declares `implements PartitionRpc`, so the two sides
|
|
749
|
+
* of this seam cannot drift without failing to compile. The API Worker brands
|
|
750
|
+
* its namespace binding with a `Rpc.DurableObjectBranded` extension of this
|
|
751
|
+
* interface; the branding is a Cloudflare type and stays in the adapter, which
|
|
752
|
+
* is the layer allowed to name one.
|
|
753
|
+
*/
|
|
754
|
+
export interface PartitionRpc {
|
|
755
|
+
archiveSchedule(command: ScheduleWriteCommand): Promise<ScheduleWriteOutcome>;
|
|
756
|
+
forecastOccurrences(query: ForecastQuery): Promise<ForecastOutcome>;
|
|
757
|
+
cancelSchedule(command: ScheduleWriteCommand): Promise<ScheduleWriteOutcome>;
|
|
758
|
+
createSchedule(command: CreateScheduleCommand): Promise<CreateScheduleOutcome>;
|
|
759
|
+
drainProjections(query: DrainProjectionsQuery): Promise<DrainProjectionsOutcome>;
|
|
760
|
+
listAttempts(query: ListAttemptsQuery): Promise<ListAttemptsOutcome>;
|
|
761
|
+
listOccurrences(query: ListOccurrencesQuery): Promise<ListOccurrencesOutcome>;
|
|
762
|
+
listSchedules(query: ListSchedulesQuery): Promise<ListSchedulesOutcome>;
|
|
763
|
+
pauseSchedule(command: ScheduleWriteCommand): Promise<ScheduleWriteOutcome>;
|
|
764
|
+
readAttempt(query: ReadAttemptQuery): Promise<ReadAttemptOutcome>;
|
|
765
|
+
readOccurrence(query: ReadOccurrenceQuery): Promise<ReadOccurrenceOutcome>;
|
|
766
|
+
readSchedule(query: ReadScheduleQuery): Promise<PartitionOutcome>;
|
|
767
|
+
redeliverOccurrence(command: RedeliverOccurrenceCommand): Promise<RedeliverOccurrenceOutcome>;
|
|
768
|
+
replaceSchedule(command: ReplaceScheduleCommand): Promise<ScheduleWriteOutcome>;
|
|
769
|
+
resumeSchedule(command: ScheduleWriteCommand): Promise<ScheduleWriteOutcome>;
|
|
770
|
+
settleProjections(command: SettleProjectionsCommand): Promise<SettleProjectionsOutcome>;
|
|
771
|
+
}
|