@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.
Files changed (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authority.d.ts +217 -0
  4. package/dist/authority.js +261 -0
  5. package/dist/callback-ownership.d.ts +58 -0
  6. package/dist/callback-ownership.js +71 -0
  7. package/dist/callback-safety.d.ts +38 -0
  8. package/dist/callback-safety.js +403 -0
  9. package/dist/compatibility.d.ts +82 -0
  10. package/dist/compatibility.js +115 -0
  11. package/dist/delivery.d.ts +401 -0
  12. package/dist/delivery.js +311 -0
  13. package/dist/endpoints.d.ts +305 -0
  14. package/dist/endpoints.js +179 -0
  15. package/dist/events.d.ts +62 -0
  16. package/dist/events.js +38 -0
  17. package/dist/first-value-journey.d.ts +305 -0
  18. package/dist/first-value-journey.js +466 -0
  19. package/dist/forecast.d.ts +443 -0
  20. package/dist/forecast.js +729 -0
  21. package/dist/identifiers.d.ts +109 -0
  22. package/dist/identifiers.js +131 -0
  23. package/dist/identity.d.ts +196 -0
  24. package/dist/identity.js +182 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.js +27 -0
  27. package/dist/mcp.d.ts +51 -0
  28. package/dist/mcp.js +56 -0
  29. package/dist/oauth.d.ts +796 -0
  30. package/dist/oauth.js +708 -0
  31. package/dist/operations.d.ts +3985 -0
  32. package/dist/operations.js +1837 -0
  33. package/dist/partition.d.ts +771 -0
  34. package/dist/partition.js +206 -0
  35. package/dist/policy.d.ts +93 -0
  36. package/dist/policy.js +180 -0
  37. package/dist/problems.d.ts +158 -0
  38. package/dist/problems.js +104 -0
  39. package/dist/protocol.d.ts +308 -0
  40. package/dist/protocol.js +515 -0
  41. package/dist/scheduling-records.d.ts +395 -0
  42. package/dist/scheduling-records.js +284 -0
  43. package/dist/scheduling.d.ts +1098 -0
  44. package/dist/scheduling.js +1116 -0
  45. package/dist/secrets.d.ts +383 -0
  46. package/dist/secrets.js +272 -0
  47. package/dist/signatures.d.ts +704 -0
  48. package/dist/signatures.js +867 -0
  49. package/dist/tenancy.d.ts +178 -0
  50. package/dist/tenancy.js +145 -0
  51. package/dist/test-sink.d.ts +986 -0
  52. package/dist/test-sink.js +755 -0
  53. package/dist/time.d.ts +45 -0
  54. package/dist/time.js +128 -0
  55. package/dist/usage.d.ts +121 -0
  56. package/dist/usage.js +200 -0
  57. package/dist/webhooks.d.ts +116 -0
  58. package/dist/webhooks.js +161 -0
  59. package/dist/workspace.d.ts +92 -0
  60. package/dist/workspace.js +59 -0
  61. package/generated/v1/callback-delivery-signature.json +467 -0
  62. package/generated/v1/callback-endpoint-verification.json +59 -0
  63. package/generated/v1/callback-receiver.json +241 -0
  64. package/generated/v1/examples/attempts.list.response.json +24 -0
  65. package/generated/v1/examples/attempts.read.response.json +20 -0
  66. package/generated/v1/examples/credentials.create.request.json +5 -0
  67. package/generated/v1/examples/credentials.create.response.json +10 -0
  68. package/generated/v1/examples/credentials.list.response.json +17 -0
  69. package/generated/v1/examples/credentials.revoke.response.json +11 -0
  70. package/generated/v1/examples/credentials.rotate.request.json +3 -0
  71. package/generated/v1/examples/credentials.rotate.response.json +22 -0
  72. package/generated/v1/examples/endpoints.create.request.json +3 -0
  73. package/generated/v1/examples/endpoints.create.response.json +16 -0
  74. package/generated/v1/examples/endpoints.disable.response.json +15 -0
  75. package/generated/v1/examples/endpoints.list.response.json +21 -0
  76. package/generated/v1/examples/endpoints.read.response.json +14 -0
  77. package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
  78. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
  79. package/generated/v1/examples/endpoints.update.request.json +3 -0
  80. package/generated/v1/examples/endpoints.update.response.json +13 -0
  81. package/generated/v1/examples/endpoints.verify.response.json +15 -0
  82. package/generated/v1/examples/forecast.read.response.json +28 -0
  83. package/generated/v1/examples/forecast.windows.response.json +21 -0
  84. package/generated/v1/examples/grants.list.response.json +17 -0
  85. package/generated/v1/examples/grants.revoke.response.json +16 -0
  86. package/generated/v1/examples/meta.scaffold.response.json +5 -0
  87. package/generated/v1/examples/meta.version.response.json +5 -0
  88. package/generated/v1/examples/occurrences.list.response.json +17 -0
  89. package/generated/v1/examples/occurrences.read.response.json +15 -0
  90. package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
  91. package/generated/v1/examples/projects.create.request.json +3 -0
  92. package/generated/v1/examples/projects.create.response.json +5 -0
  93. package/generated/v1/examples/projects.list.response.json +9 -0
  94. package/generated/v1/examples/schedules.archive.response.json +25 -0
  95. package/generated/v1/examples/schedules.cancel.response.json +24 -0
  96. package/generated/v1/examples/schedules.create.request.json +9 -0
  97. package/generated/v1/examples/schedules.create.response.json +25 -0
  98. package/generated/v1/examples/schedules.list.response.json +23 -0
  99. package/generated/v1/examples/schedules.pause.response.json +23 -0
  100. package/generated/v1/examples/schedules.read.response.json +25 -0
  101. package/generated/v1/examples/schedules.replace.request.json +8 -0
  102. package/generated/v1/examples/schedules.replace.response.json +25 -0
  103. package/generated/v1/examples/schedules.resume.response.json +23 -0
  104. package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
  105. package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
  106. package/generated/v1/examples/testsinks.configure.request.json +3 -0
  107. package/generated/v1/examples/testsinks.configure.response.json +13 -0
  108. package/generated/v1/examples/testsinks.create.request.json +8 -0
  109. package/generated/v1/examples/testsinks.create.response.json +13 -0
  110. package/generated/v1/examples/testsinks.delete.response.json +13 -0
  111. package/generated/v1/examples/testsinks.list.response.json +20 -0
  112. package/generated/v1/examples/testsinks.read.response.json +14 -0
  113. package/generated/v1/examples/usage.read.response.json +20 -0
  114. package/generated/v1/first-value-journey.json +436 -0
  115. package/generated/v1/manifest.json +1037 -0
  116. package/generated/v1/openapi.json +23665 -0
  117. package/generated/v1/policy-matrix.json +305 -0
  118. package/generated/v1/schemas/attempts.list.query.json +16 -0
  119. package/generated/v1/schemas/attempts.list.response.json +175 -0
  120. package/generated/v1/schemas/attempts.read.response.json +168 -0
  121. package/generated/v1/schemas/credentials.create.request.json +39 -0
  122. package/generated/v1/schemas/credentials.create.response.json +88 -0
  123. package/generated/v1/schemas/credentials.list.query.json +16 -0
  124. package/generated/v1/schemas/credentials.list.response.json +124 -0
  125. package/generated/v1/schemas/credentials.revoke.response.json +84 -0
  126. package/generated/v1/schemas/credentials.rotate.request.json +11 -0
  127. package/generated/v1/schemas/credentials.rotate.response.json +171 -0
  128. package/generated/v1/schemas/endpoints.create.request.json +12 -0
  129. package/generated/v1/schemas/endpoints.create.response.json +115 -0
  130. package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
  131. package/generated/v1/schemas/endpoints.disable.response.json +100 -0
  132. package/generated/v1/schemas/endpoints.list.query.json +16 -0
  133. package/generated/v1/schemas/endpoints.list.response.json +146 -0
  134. package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
  135. package/generated/v1/schemas/endpoints.read.response.json +100 -0
  136. package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
  137. package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
  138. package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
  139. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
  140. package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
  141. package/generated/v1/schemas/endpoints.update.request.json +12 -0
  142. package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
  143. package/generated/v1/schemas/endpoints.update.response.json +100 -0
  144. package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
  145. package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
  146. package/generated/v1/schemas/endpoints.verify.response.json +100 -0
  147. package/generated/v1/schemas/forecast.read.query.json +26 -0
  148. package/generated/v1/schemas/forecast.read.response.json +100 -0
  149. package/generated/v1/schemas/forecast.windows.query.json +26 -0
  150. package/generated/v1/schemas/forecast.windows.response.json +94 -0
  151. package/generated/v1/schemas/grants.list.response.json +98 -0
  152. package/generated/v1/schemas/grants.revoke.response.json +95 -0
  153. package/generated/v1/schemas/meta.scaffold.response.json +36 -0
  154. package/generated/v1/schemas/meta.version.response.json +19 -0
  155. package/generated/v1/schemas/occurrences.list.query.json +16 -0
  156. package/generated/v1/schemas/occurrences.list.response.json +83 -0
  157. package/generated/v1/schemas/occurrences.read.response.json +76 -0
  158. package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
  159. package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
  160. package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
  161. package/generated/v1/schemas/projects.create.request.json +12 -0
  162. package/generated/v1/schemas/projects.create.response.json +25 -0
  163. package/generated/v1/schemas/projects.list.response.json +35 -0
  164. package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
  165. package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
  166. package/generated/v1/schemas/schedules.archive.response.json +189 -0
  167. package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
  168. package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
  169. package/generated/v1/schemas/schedules.cancel.response.json +189 -0
  170. package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
  171. package/generated/v1/schemas/schedules.create.request.json +99 -0
  172. package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
  173. package/generated/v1/schemas/schedules.create.response.json +189 -0
  174. package/generated/v1/schemas/schedules.list.query.json +16 -0
  175. package/generated/v1/schemas/schedules.list.response.json +170 -0
  176. package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
  177. package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
  178. package/generated/v1/schemas/schedules.pause.response.json +189 -0
  179. package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
  180. package/generated/v1/schemas/schedules.read.response.json +189 -0
  181. package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
  182. package/generated/v1/schemas/schedules.replace.request.json +99 -0
  183. package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
  184. package/generated/v1/schemas/schedules.replace.response.json +189 -0
  185. package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
  186. package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
  187. package/generated/v1/schemas/schedules.resume.response.json +189 -0
  188. package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
  189. package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
  190. package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
  191. package/generated/v1/schemas/testsinks.configure.request.json +66 -0
  192. package/generated/v1/schemas/testsinks.configure.response.json +115 -0
  193. package/generated/v1/schemas/testsinks.create.request.json +76 -0
  194. package/generated/v1/schemas/testsinks.create.response.json +115 -0
  195. package/generated/v1/schemas/testsinks.delete.response.json +115 -0
  196. package/generated/v1/schemas/testsinks.list.query.json +16 -0
  197. package/generated/v1/schemas/testsinks.list.response.json +161 -0
  198. package/generated/v1/schemas/testsinks.read.response.json +115 -0
  199. package/generated/v1/schemas/usage.read.query.json +15 -0
  200. package/generated/v1/schemas/usage.read.response.json +71 -0
  201. package/generated/v1/snippets/attempts.list.json +5 -0
  202. package/generated/v1/snippets/attempts.read.json +5 -0
  203. package/generated/v1/snippets/credentials.create.json +5 -0
  204. package/generated/v1/snippets/credentials.list.json +5 -0
  205. package/generated/v1/snippets/credentials.revoke.json +5 -0
  206. package/generated/v1/snippets/credentials.rotate.json +5 -0
  207. package/generated/v1/snippets/endpoints.create.json +5 -0
  208. package/generated/v1/snippets/endpoints.disable.json +5 -0
  209. package/generated/v1/snippets/endpoints.list.json +5 -0
  210. package/generated/v1/snippets/endpoints.read.json +5 -0
  211. package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
  212. package/generated/v1/snippets/endpoints.update.json +5 -0
  213. package/generated/v1/snippets/endpoints.verify.json +5 -0
  214. package/generated/v1/snippets/forecast.read.json +5 -0
  215. package/generated/v1/snippets/forecast.windows.json +5 -0
  216. package/generated/v1/snippets/grants.list.json +5 -0
  217. package/generated/v1/snippets/grants.revoke.json +5 -0
  218. package/generated/v1/snippets/meta.scaffold.json +5 -0
  219. package/generated/v1/snippets/meta.version.json +5 -0
  220. package/generated/v1/snippets/occurrences.list.json +5 -0
  221. package/generated/v1/snippets/occurrences.read.json +5 -0
  222. package/generated/v1/snippets/occurrences.redeliver.json +5 -0
  223. package/generated/v1/snippets/projects.create.json +5 -0
  224. package/generated/v1/snippets/projects.list.json +5 -0
  225. package/generated/v1/snippets/schedules.archive.json +5 -0
  226. package/generated/v1/snippets/schedules.cancel.json +5 -0
  227. package/generated/v1/snippets/schedules.create.json +5 -0
  228. package/generated/v1/snippets/schedules.list.json +5 -0
  229. package/generated/v1/snippets/schedules.pause.json +5 -0
  230. package/generated/v1/snippets/schedules.read.json +5 -0
  231. package/generated/v1/snippets/schedules.replace.json +5 -0
  232. package/generated/v1/snippets/schedules.resume.json +5 -0
  233. package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
  234. package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
  235. package/generated/v1/snippets/testsinks.configure.json +5 -0
  236. package/generated/v1/snippets/testsinks.create.json +5 -0
  237. package/generated/v1/snippets/testsinks.delete.json +5 -0
  238. package/generated/v1/snippets/testsinks.list.json +5 -0
  239. package/generated/v1/snippets/testsinks.read.json +5 -0
  240. package/generated/v1/snippets/usage.read.json +5 -0
  241. 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
+ }