mppx 0.17.3 → 0.17.4

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 (41) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/Errors.d.ts +94 -0
  3. package/dist/Errors.js +83 -0
  4. package/dist/Store.d.ts +199 -11
  5. package/dist/Store.js +527 -48
  6. package/dist/cli/cli.js +141 -137
  7. package/dist/cli/internal.js +28 -6
  8. package/dist/cli/plugins/index.js +16 -12
  9. package/dist/cli/validate/challenge.d.ts +4 -0
  10. package/dist/cli/validate/challenge.js +9 -6
  11. package/dist/cloudflare/ChannelStore.d.ts +97 -0
  12. package/dist/cloudflare/ChannelStore.js +304 -0
  13. package/dist/cloudflare/Channels.d.ts +92 -0
  14. package/dist/cloudflare/Channels.js +325 -0
  15. package/dist/cloudflare/index.d.ts +5 -0
  16. package/dist/cloudflare/index.js +5 -0
  17. package/dist/internal/sse.js +32 -15
  18. package/dist/internal/version.d.ts +2 -2
  19. package/dist/internal/version.js +1 -1
  20. package/dist/stripe/server/Methods.d.ts +2 -0
  21. package/dist/stripe/server/Methods.js +2 -1
  22. package/dist/stripe/server/internal/record-payment.d.ts +49 -0
  23. package/dist/stripe/server/internal/record-payment.js +75 -18
  24. package/dist/tempo/Methods.d.ts +8 -0
  25. package/dist/tempo/Methods.js +18 -2
  26. package/dist/tempo/client/Charge.d.ts +32 -1
  27. package/dist/tempo/client/Charge.js +201 -3
  28. package/dist/tempo/client/Methods.d.ts +16 -0
  29. package/dist/tempo/internal/fee-payer.d.ts +0 -25
  30. package/dist/tempo/internal/fee-payer.js +50 -3
  31. package/dist/tempo/internal/zone.d.ts +79 -0
  32. package/dist/tempo/internal/zone.js +133 -0
  33. package/dist/tempo/server/Charge.d.ts +7 -3
  34. package/dist/tempo/server/Charge.js +98 -4
  35. package/dist/tempo/server/Methods.d.ts +40 -0
  36. package/dist/tempo/server/internal/html.gen.js +1 -1
  37. package/dist/tempo/session/server/ChannelStore.d.ts +5 -0
  38. package/dist/tempo/session/server/ChannelStore.js +2 -2
  39. package/dist/tempo/session/server/CredentialVerification.js +1 -1
  40. package/dist/validation/core.js +2 -0
  41. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # mppx
2
2
 
3
+ ## 0.17.4
4
+
5
+ ### Patch Changes
6
+
7
+ - fa22d95: Fixed `ChannelStore` replacing the immediate send that a close request written during a failed queue send scheduled with the 60-second retry, and resending failed settlements forever when `minUnsettledAmount` is `0n` and the deposit is under 10 raw units: the threshold is now at least 1.
8
+ - 3e44f83: Fixed `ChannelStore` giving up on a channel during the server's cooperative-close marker, which left a payer close request merged into it unsent, and spending watchdog attempts on alarms set for a close marker that a failed close then cleared. `ChannelStore` now rejects non-finite or negative `retryMs`, `staleAfterMs` and `watchdogMs` and a `maxAttempts` that is not a positive integer.
9
+ - d11a756: Added `mppx/cloudflare`: a `ChannelStore` Durable Object that stores Tempo session channels and sends them to a settlement queue by amount or staleness, with a watchdog; `channels()`, the store and reusable-channel lookup for `tempo.session()`; and `settle()`, the queue consumer that settles them on-chain.
10
+ - 48d97fa: Fixed `mppx/cloudflare` settlement and channel lookup: a payer close request stamped at or before the server's own cooperative-close marker is now sent to the settlement queue once that marker can no longer land, with an alarm at the marker's end; and a lookup add skips a channel that no longer resumes, while a full lookup entry drops closed channels before an open one.
11
+ - b964dc4: Fixed `mppx/cloudflare` settlement and channel lookup: the server's own cooperative-close marker (recorded in the new `pendingCloseAt` channel field) no longer queues an immediate settlement, and `settle()` waits while that close may still land; a new payer close request after an earlier one cleared is sent at once; and each lookup entry keeps up to 8 of the payer's open channels, so closing one leaves the others findable.
12
+ - 8e812d3: Fixed `mppx/cloudflare`'s `channels()` lookup returning a channel opened with another operator for a challenge without one, which mppx verifies against the zero operator, and losing a matching channel behind eight newer ones with other operators: lookup entries are now keyed by operator too (`mppx:session-lookup:v3`), so channels written before resume again after their next write. Fixed `ChannelStore` replacing the immediate send that a close request written during a queue send scheduled with a later watchdog alarm.
13
+ - 7255375: Fixed `mppx/cloudflare` so `channels()` skips a channel opened with another operator than the challenge's, `settle()` renews its settlement lease while a settlement runs, and `ChannelStore` resends a closing channel within 2 minutes so every attempt fits in the escrow's close grace period.
14
+ - 4adc283: Fixed follow-ups in `Store.durableObject` replay shards and `mppx/cloudflare`. Replay shards kept their claims in a table named `mppx_replay_claims`, so the shard alarm no longer clears a subclass's own `replay_claims` table. `ChannelStore` sent a channel to the settlement queue at once on a close request or a threshold's worth of new vouchers, instead of waiting for the watchdog after a failed job. `channels()` no longer let a delayed lookup write for a closed channel replace the entry of a channel the payer can still resume, and its lookup skipped entries whose channel had closed.
15
+ - a9b2cbc: Added `onWrite` and `alarm` hooks to `Store.DurableObjectStore`: a subclass sees every applied write with its previous and next values in one storage transaction with the write (so the hook's own writes commit with it, and a hook that throws rolls it back), and can schedule and handle alarms through the protected `storage`, `state` and `env`.
16
+ - 0968d3f: Renamed `Store.DurableObjectStore`'s store operations to `storeGet`, `storePut`, `storeDelete`, `storeSwap` and `storeClaim`, so Workers RPC reaches them on Workers whose Durable Object stubs keep the legacy `get`, `put` and `delete` methods, and `fetch` no longer calls a subclass's own methods of those names. `Store.DurableObjectStore.rpc()` now keeps a subclass's static members.
17
+
18
+ Fixed `DurableObjectStore.isReplayShard()` to match a replay shard's whole object name, so an ordinary key that starts with `mppx:replay:` is not taken for a shard, and the base `alarm()` clears claims only in an object that holds them.
19
+
20
+ - 5ebce56: Added `DurableObjectStore.isReplayShard()`, so a subclass skips its own work (in `alarm`, handlers or its constructor, by the object's name) in the replay shards of `Store.durableObject`'s `replayShards`, which are objects of the same class, and documented that values stored at a key don't block its sharded replay claims.
21
+ - 56e0c8c: Added `replayShards` and `replayShardVersion` options to `Store.durableObject`: replay claims spread over a fixed number of SQLite-backed shard objects, each claimed with one SQL statement and cleared by an alarm once expired, instead of one Durable Object per claimed key.
22
+ - c776057: Added `transport: 'rpc'` and `encoding: 'native'` options to `Store.durableObject`: the store calls the object's methods over Workers RPC (with a class from `Store.DurableObjectStore.rpc(DurableObject)`), and native encoding stores structured-clone values, so bigints stay intact and values a Durable Object wrote with `storage.put` read back.
23
+ - 2f96b5b: Improved the internal SSE parser's throughput on line-dense `text/event-stream` bodies by scanning with `indexOf` instead of a global RegExp.
24
+ - 94d1e36: Fixed `Store.DurableObjectStore`'s native compare-and-swap treating a length-tracking view of a resizable `ArrayBuffer` as equal to a fixed-length one.
25
+ - 5ba7bf5: Fixed `Store.durableObject` native compare-and-swap to compare object keys, map entries and set members in order, compare sparse arrays by their own elements instead of scanning to their length, and tell resizable `ArrayBuffer`s apart.
26
+ - fa22d95: Changed `Store.DurableObjectStore`'s native encoding to reject views of resizable `ArrayBuffer`s, as it rejects other values it cannot compare, instead of probing their length tracking with a copy of the whole buffer resized to its maximum on every compare-and-swap.
27
+ - 8e2f7fe: Fixed `tempo.charge` with `waitForConfirmation: false` to throw `Errors.BroadcastOutcomeUnknownError` with the transaction hash after an ambiguous broadcast failure, instead of the raw RPC error. `stripe.recordPayment` now returns the PaymentIntent's Stripe status as `paymentIntentStatus`.
28
+ - ca6cc56: Fixed `Store.durableObject` storing a key named like a replay shard (`mppx:replay:v<version>:<shard>`) in that shard's object, where `DurableObjectStore.isReplayShard()` then reported it as a shard. Such keys, and keys starting with `mppx:key:`, now live in an object named `mppx:key:<key>`, so values stored at them before are no longer found.
29
+ - d631e07: Fixed `Store.DurableObjectStore.rpc()` throwing when a subclass shadows a static member or method that an ancestor made non-configurable.
30
+ - ed12b58: Added `Errors.BroadcastOutcomeUnknownError`, which `tempo.charge` now throws with the transaction hash and `validBefore` when an ambiguous broadcast leaves no receipt, instead of rethrowing the RPC error (still its `cause`). Added `stripe.recordPayment` to record a settled payment as a Stripe PaymentIntent, idempotent per transaction hash.
31
+ - f54a1c9: Added `Errors.PaymentSimulationError`, which wraps a failed fee-sponsored preflight simulation with its `phase` (`sender` or `sponsored`), `sender`, `feePayer` and `paymentToken`, so callers can tell a payer's insufficient balance from a sponsor's. Fixed the sponsored simulation dropping a zero `maxPriorityFeePerGas`, which broke a hosted fee payer's signature.
32
+ - a89d5f5: Added Zone payments to the `tempo` charge method. A server lists the Zones it accepts with `zones: [3, 7]`, or accepts any Zone with `zones: true`. A client pays from a Zone balance with `zone`: it withdraws to the recipient with a challenge-bound memo, waits for the withdrawal to settle on Tempo, and sends a `zone` credential with the settlement transaction hash, which the server verifies.
33
+
34
+ If the wait fails, the client throws `Errors.ZoneSettlementPendingError` and resumes the same withdrawal when the challenge is retried.
35
+
36
+ - 3f60dcc: Fixed `mppx validate` and paid CLI retries rejecting every mppx server's retry Challenge as changed: the comparison now ignores the per-issuance `_mppx_nonce` and `_mppx_identity` in `opaque`. The malformed-Credential probe now sends the `-H` headers and puts the Credential in the header the server's Challenge names.
37
+ - 64d852c: Fixed `mppx validate` error-handling hints to name the Credential header the server's Challenge uses instead of always `Authorization`.
38
+ - 9ba2807: Fixed Zone ID validation to reject IDs that alias another Zone's portal address.
39
+
3
40
  ## 0.17.3
4
41
 
5
42
  ### Patch Changes
package/dist/Errors.d.ts CHANGED
@@ -196,6 +196,100 @@ export declare namespace PaymentOutcomeUnknownError {
196
196
  transactionHash?: string | undefined;
197
197
  };
198
198
  }
199
+ /**
200
+ * A server broadcast a payment transaction, the broadcast failed in a way that does not say whether
201
+ * the node accepted it, and no receipt appeared before settlement stopped polling. The transaction
202
+ * may still be included until `validBefore`.
203
+ *
204
+ * Settle it by looking up the receipt for `transactionHash`: once `validBefore` has passed, the
205
+ * transaction can no longer be included, so one lookup after it is final. This is not a
206
+ * `PaymentError`: HTTP handlers still answer with a generic 500 `InternalPaymentError`, whose
207
+ * `cause` is this error.
208
+ */
209
+ export declare class BroadcastOutcomeUnknownError extends Error {
210
+ readonly name = "BroadcastOutcomeUnknownError";
211
+ /** Hash of the broadcast transaction, as it will appear on chain if it is included. */
212
+ readonly transactionHash: `0x${string}`;
213
+ /** Unix time in seconds after which the transaction can no longer be included, when it expires. */
214
+ readonly validBefore: number | undefined;
215
+ constructor(options: BroadcastOutcomeUnknownError.Options);
216
+ }
217
+ export declare namespace BroadcastOutcomeUnknownError {
218
+ type Options = {
219
+ /** The broadcast error that left the outcome unknown. */
220
+ cause?: unknown;
221
+ /** Hash of the broadcast transaction. */
222
+ transactionHash: `0x${string}`;
223
+ /** Unix time in seconds after which the transaction can no longer be included. */
224
+ validBefore?: number | undefined;
225
+ };
226
+ }
227
+ /**
228
+ * A pre-broadcast simulation of a fee-sponsored payment failed, so the payment was not broadcast.
229
+ *
230
+ * `phase` says which simulation failed: `sender` runs the sender's calls before sponsorship, and
231
+ * `sponsored` runs the final envelope billed to `feePayer`. Callers use it, with `sender`,
232
+ * `feePayer` and `paymentToken`, to tell a payer's insufficient balance from a sponsor's without
233
+ * matching node messages. The node's error is the `cause`. This is not a `PaymentError`: HTTP
234
+ * handlers still answer with a generic 500 `InternalPaymentError`, whose `cause` is this error.
235
+ */
236
+ export declare class PaymentSimulationError extends Error {
237
+ readonly name = "PaymentSimulationError";
238
+ /** Fee payer the sponsored simulation billed. Set only in the `sponsored` phase. */
239
+ readonly feePayer: `0x${string}` | undefined;
240
+ /** TIP-20 token of a single `transfer` or `transferWithMemo` call, when the transaction is one. */
241
+ readonly paymentToken: `0x${string}` | undefined;
242
+ /** Simulation that failed. */
243
+ readonly phase: PaymentSimulationError.Phase;
244
+ /** Sender of the simulated transaction. */
245
+ readonly sender: `0x${string}` | undefined;
246
+ constructor(options: PaymentSimulationError.Options);
247
+ }
248
+ export declare namespace PaymentSimulationError {
249
+ /** `sender`: the sender's calls before sponsorship. `sponsored`: the final sponsored envelope. */
250
+ type Phase = 'sender' | 'sponsored';
251
+ type Options = {
252
+ /** The simulation error. */
253
+ cause?: unknown;
254
+ /** Fee payer of the sponsored simulation. */
255
+ feePayer?: `0x${string}` | undefined;
256
+ /** TIP-20 token of a single transfer call. */
257
+ paymentToken?: `0x${string}` | undefined;
258
+ /** Simulation that failed. */
259
+ phase: Phase;
260
+ /** Sender of the simulated transaction. */
261
+ sender?: `0x${string}` | undefined;
262
+ };
263
+ }
264
+ /**
265
+ * A client withdrew from a Zone to pay a challenge, but the withdrawal did not settle on Tempo
266
+ * before the client stopped waiting. The withdrawal cannot be undone and may still settle.
267
+ *
268
+ * Retry the same challenge with the same `tempo.charge` client to resume waiting without
269
+ * withdrawing again, or find the settlement with `senderTag` on the Zone's portal.
270
+ */
271
+ export declare class ZoneSettlementPendingError extends Error {
272
+ readonly name = "ZoneSettlementPendingError";
273
+ /** ID of the challenge the withdrawal pays. */
274
+ readonly challengeId: string;
275
+ /** Sender tag of the withdrawal, as the portal's `WithdrawalProcessed` event carries it. */
276
+ readonly senderTag: `0x${string}`;
277
+ /** ID of the Zone the withdrawal left. */
278
+ readonly zoneId: number;
279
+ constructor(options: ZoneSettlementPendingError.Options);
280
+ }
281
+ export declare namespace ZoneSettlementPendingError {
282
+ type Options = {
283
+ /** The error that ended the wait. */
284
+ cause?: unknown;
285
+ /** ID of the challenge the withdrawal pays. */
286
+ challengeId: string;
287
+ /** Sender tag of the withdrawal. */
288
+ senderTag: `0x${string}`;
289
+ /** ID of the Zone the withdrawal left. */
290
+ zoneId: number;
291
+ };
292
+ }
199
293
  /**
200
294
  * No credential was provided but payment is required.
201
295
  */
package/dist/Errors.js CHANGED
@@ -160,6 +160,89 @@ export class PaymentOutcomeUnknownError extends PaymentError {
160
160
  this.transactionHash = transactionHash;
161
161
  }
162
162
  }
163
+ /**
164
+ * A server broadcast a payment transaction, the broadcast failed in a way that does not say whether
165
+ * the node accepted it, and no receipt appeared before settlement stopped polling. The transaction
166
+ * may still be included until `validBefore`.
167
+ *
168
+ * Settle it by looking up the receipt for `transactionHash`: once `validBefore` has passed, the
169
+ * transaction can no longer be included, so one lookup after it is final. This is not a
170
+ * `PaymentError`: HTTP handlers still answer with a generic 500 `InternalPaymentError`, whose
171
+ * `cause` is this error.
172
+ */
173
+ export class BroadcastOutcomeUnknownError extends Error {
174
+ name = 'BroadcastOutcomeUnknownError';
175
+ /** Hash of the broadcast transaction, as it will appear on chain if it is included. */
176
+ transactionHash;
177
+ /** Unix time in seconds after which the transaction can no longer be included, when it expires. */
178
+ validBefore;
179
+ constructor(options) {
180
+ const { cause, transactionHash, validBefore } = options;
181
+ super(`Broadcast outcome is unknown for transaction ${transactionHash}. Look up its receipt before retrying.`);
182
+ this.transactionHash = transactionHash;
183
+ this.validBefore = validBefore;
184
+ // Defined manually (non-enumerable, like native `cause`) so consumers on pre-ES2022 libs still typecheck.
185
+ if (cause !== undefined)
186
+ Object.defineProperty(this, 'cause', { configurable: true, value: cause, writable: true });
187
+ }
188
+ }
189
+ /**
190
+ * A pre-broadcast simulation of a fee-sponsored payment failed, so the payment was not broadcast.
191
+ *
192
+ * `phase` says which simulation failed: `sender` runs the sender's calls before sponsorship, and
193
+ * `sponsored` runs the final envelope billed to `feePayer`. Callers use it, with `sender`,
194
+ * `feePayer` and `paymentToken`, to tell a payer's insufficient balance from a sponsor's without
195
+ * matching node messages. The node's error is the `cause`. This is not a `PaymentError`: HTTP
196
+ * handlers still answer with a generic 500 `InternalPaymentError`, whose `cause` is this error.
197
+ */
198
+ export class PaymentSimulationError extends Error {
199
+ name = 'PaymentSimulationError';
200
+ /** Fee payer the sponsored simulation billed. Set only in the `sponsored` phase. */
201
+ feePayer;
202
+ /** TIP-20 token of a single `transfer` or `transferWithMemo` call, when the transaction is one. */
203
+ paymentToken;
204
+ /** Simulation that failed. */
205
+ phase;
206
+ /** Sender of the simulated transaction. */
207
+ sender;
208
+ constructor(options) {
209
+ const { cause, feePayer, paymentToken, phase, sender } = options;
210
+ super('Payment simulation failed');
211
+ this.feePayer = feePayer;
212
+ this.paymentToken = paymentToken;
213
+ this.phase = phase;
214
+ this.sender = sender;
215
+ // Defined manually (non-enumerable, like native `cause`) so consumers on pre-ES2022 libs still typecheck.
216
+ if (cause !== undefined)
217
+ Object.defineProperty(this, 'cause', { configurable: true, value: cause, writable: true });
218
+ }
219
+ }
220
+ /**
221
+ * A client withdrew from a Zone to pay a challenge, but the withdrawal did not settle on Tempo
222
+ * before the client stopped waiting. The withdrawal cannot be undone and may still settle.
223
+ *
224
+ * Retry the same challenge with the same `tempo.charge` client to resume waiting without
225
+ * withdrawing again, or find the settlement with `senderTag` on the Zone's portal.
226
+ */
227
+ export class ZoneSettlementPendingError extends Error {
228
+ name = 'ZoneSettlementPendingError';
229
+ /** ID of the challenge the withdrawal pays. */
230
+ challengeId;
231
+ /** Sender tag of the withdrawal, as the portal's `WithdrawalProcessed` event carries it. */
232
+ senderTag;
233
+ /** ID of the Zone the withdrawal left. */
234
+ zoneId;
235
+ constructor(options) {
236
+ const { cause, challengeId, senderTag, zoneId } = options;
237
+ super(`Zone ${zoneId} withdrawal ${senderTag} for challenge "${challengeId}" has not settled on Tempo yet. Retry the challenge to resume waiting instead of paying again.`);
238
+ this.challengeId = challengeId;
239
+ this.senderTag = senderTag;
240
+ this.zoneId = zoneId;
241
+ // Defined manually (non-enumerable, like native `cause`) so consumers on pre-ES2022 libs still typecheck.
242
+ if (cause !== undefined)
243
+ Object.defineProperty(this, 'cause', { configurable: true, value: cause, writable: true });
244
+ }
245
+ }
163
246
  /**
164
247
  * No credential was provided but payment is required.
165
248
  */
package/dist/Store.d.ts CHANGED
@@ -102,7 +102,9 @@ export declare namespace cloudflare {
102
102
  /**
103
103
  * Atomic store backed by Cloudflare Durable Objects.
104
104
  *
105
- * Each key lives in its own Durable Object instance (`idFromName(key)`), so
105
+ * Each key lives in its own Durable Object instance (`idFromName(key)`; a key named
106
+ * like a replay shard, `mppx:replay:v<version>:<shard>`, or starting with `mppx:key:`
107
+ * uses `mppx:key:<key>`), so
106
108
  * reads and writes of one key are linearizable across every Worker isolate and
107
109
  * location, while different keys (such as different payment channels) scale
108
110
  * out across instances. {@link AtomicStore.update} runs `fn` in the Worker and
@@ -112,7 +114,9 @@ export declare namespace cloudflare {
112
114
  * object call.
113
115
  *
114
116
  * Bind {@link DurableObjectStore} as a SQLite-backed Durable Object and pass
115
- * its namespace.
117
+ * its namespace. With `transport: 'rpc'`, the store calls the object's methods
118
+ * over Workers RPC instead of `fetch`, and `encoding: 'native'` then stores
119
+ * values as structured-clone values instead of JSON text.
116
120
  *
117
121
  * @example
118
122
  * ```ts
@@ -127,15 +131,63 @@ export declare namespace cloudflare {
127
131
  * },
128
132
  * }
129
133
  * ```
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { DurableObject } from 'cloudflare:workers'
138
+ * import { Store } from 'mppx/server'
139
+ *
140
+ * // Workers RPC needs a class that extends `DurableObject`.
141
+ * export class MppxStore extends Store.DurableObjectStore.rpc(DurableObject) {}
142
+ *
143
+ * const store = Store.durableObject(env.MPPX_STORE, { transport: 'rpc', encoding: 'native' })
144
+ * ```
130
145
  */
131
146
  export declare function durableObject(namespace: durableObject.Namespace, options?: durableObject.Options): AtomicStore;
132
147
  export declare namespace durableObject {
133
148
  type Options = {
149
+ /**
150
+ * How values are stored. `json` stores JSON text (bigints as ox encodes them);
151
+ * `native` stores structured-clone values, so bigints and nested objects stay
152
+ * intact and values written directly with `storage.put` read back. `native`
153
+ * requires `transport: 'rpc'`. Stored values don't record their encoding, so keep
154
+ * one encoding per namespace (or `keyPrefix`): a `native` store reads `json` values
155
+ * as their JSON text, and `json` clients cannot read what it writes. @default 'json'
156
+ */
157
+ encoding?: Encoding | undefined;
134
158
  /** Prefix prepended to every backing store key. */
135
159
  keyPrefix?: string | undefined;
136
160
  /** Compare-and-swap attempts before {@link AtomicStore.update} gives up. @default 32 */
137
161
  maxAttempts?: number | undefined;
162
+ /**
163
+ * Spreads replay claims ({@link AtomicStore.tryClaim}) over this many shard
164
+ * objects, named `mppx:replay:v<version>:<shard>` by an FNV-1a hash of the
165
+ * key, instead of one object per key: each shard holds its claims in a SQLite
166
+ * table (`mppx_replay_claims`) and clears expired ones with an alarm, so short-lived claims leave no
167
+ * objects behind. The class must be SQLite-backed. Claims then live only in
168
+ * the shards: `get` and `update` of a claimed key don't see them, a value
169
+ * stored at the key doesn't block a claim (mppx stores only expiring claims
170
+ * at its replay keys), and `delete` releases them. Changing the count moves keys between shards, so
171
+ * bump `replayShardVersion` with it to start from empty shards. A claim lives
172
+ * only where it was made: enabling sharding, changing the count or version, or
173
+ * running two settings side by side lets a key claimed under the old setting
174
+ * be claimed again until that claim expires. Change it when no unexpired
175
+ * claim matters, such as together with the secret that signs Challenges.
176
+ * Shards are objects of the bound class, so a subclass's `alarm`, handlers and
177
+ * constructor run in them too: skip the subclass's own work there with
178
+ * {@link DurableObjectStore.isReplayShard}, and never clear a shard's storage.
179
+ */
180
+ replayShards?: number | undefined;
181
+ /** Version in the shard objects' names, to start a fresh set of shards. @default 1 */
182
+ replayShardVersion?: number | undefined;
183
+ /**
184
+ * How the store calls the object: `fetch` requests, or Workers RPC method
185
+ * calls, which need the class from {@link DurableObjectStore.rpc}. @default 'fetch'
186
+ */
187
+ transport?: 'fetch' | 'rpc' | undefined;
138
188
  };
189
+ /** How {@link durableObject} stores values: JSON text or structured-clone values. */
190
+ type Encoding = 'json' | 'native';
139
191
  /** The Durable Object namespace binding of a {@link DurableObjectStore} class. */
140
192
  type Namespace = {
141
193
  idFromName: (name: string) => unknown;
@@ -149,7 +201,8 @@ export declare namespace durableObject {
149
201
  *
150
202
  * Export a subclass of it from your Worker and bind it in
151
203
  * `wrangler.jsonc` with a `new_sqlite_classes` migration. It needs no
152
- * `cloudflare:workers` import, so it bundles anywhere.
204
+ * `cloudflare:workers` import, so it bundles anywhere; for Workers RPC, extend
205
+ * {@link DurableObjectStore.rpc} instead.
153
206
  *
154
207
  * @example
155
208
  * ```jsonc
@@ -160,26 +213,154 @@ export declare namespace durableObject {
160
213
  * ```
161
214
  */
162
215
  export declare class DurableObjectStore {
163
- #private;
164
- constructor(state: {
165
- storage: DurableObjectStore.Storage;
166
- }, _env?: unknown);
167
- /** Handles one store operation. Each runs without awaiting other I/O between its read and write, so the object's input gate makes it atomic. */
216
+ /** The object's state (`ctx`), for subclasses that use its storage or alarms. */
217
+ protected state: DurableObjectStore.State;
218
+ /** The Worker's bindings, for subclasses. */
219
+ protected env: unknown;
220
+ /**
221
+ * The object's storage: a subclass schedules alarms with it from {@link DurableObjectStore.onWrite}.
222
+ * A plain field, like `state` and `env`, so subclasses that assign `this.storage = state.storage` keep working.
223
+ */
224
+ protected storage: DurableObjectStore.Storage;
225
+ constructor(state: DurableObjectStore.State, env?: unknown);
226
+ /**
227
+ * Returns this class rebuilt on `base`, the `DurableObject` class from
228
+ * `cloudflare:workers`: Workers RPC calls only reach classes that extend it,
229
+ * and taking it as an argument keeps this module free of that import. Called
230
+ * on a subclass, it keeps the subclass's methods (such as `onWrite` and
231
+ * `alarm`) and static members, but not its constructor, field initializers or `#private` members,
232
+ * which the copied methods cannot reach: subclass the returned class, or set
233
+ * state up lazily in ordinary members.
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * import { DurableObject } from 'cloudflare:workers'
238
+ *
239
+ * export class MppxStore extends Store.DurableObjectStore.rpc(DurableObject) {}
240
+ * ```
241
+ */
242
+ static rpc<store extends typeof DurableObjectStore>(this: store, base: abstract new (state: any, env: any) => object): store;
243
+ /**
244
+ * Called for every applied put, delete, swap and claim (except claims in replay shards,
245
+ * which live in the shard's own table), after the storage write and before the
246
+ * response, with the decoded values (`null` when the key is absent). When the
247
+ * storage has `transaction()`, the write and this hook run in one transaction,
248
+ * passed as `storage`: make the hook's writes and alarms
249
+ * through it so they commit with the store's write, and throw to roll the
250
+ * write back. Make only storage calls and alarms here: awaiting other I/O
251
+ * (`fetch`, a queue send) opens the object's input gate before the write
252
+ * commits, so a concurrent swap or claim could act on the old value. Do that
253
+ * work from {@link DurableObjectStore.alarm} instead. Does nothing by default.
254
+ */
255
+ protected onWrite(_key: string, _previous: unknown | null, _next: unknown | null, _storage: DurableObjectStore.Storage): void | Promise<void>;
256
+ /**
257
+ * Handles the object's alarm, which a subclass sets through
258
+ * {@link DurableObjectStore.storage}. In a replay shard it clears expired claims, so
259
+ * a subclass that overrides it must call `super.alarm()`, or expired claims stay stored.
260
+ * Elsewhere, including on key-value-backed objects, it does nothing. The subclass's own
261
+ * alarm work runs in shards too: skip it there with {@link DurableObjectStore.isReplayShard}.
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * override async alarm() {
266
+ * await super.alarm()
267
+ * if (this.isReplayShard()) return
268
+ * // The subclass's own work.
269
+ * }
270
+ * ```
271
+ */
272
+ alarm(): Promise<void>;
273
+ /**
274
+ * Whether this object is a replay shard of {@link durableObject}'s `replayShards`: an
275
+ * object of the same class that holds only replay claims, in its own SQL table. A
276
+ * subclass skips its own work there, and must not clear its storage. It tells by the
277
+ * object's whole name (`mppx:replay:v<version>:<shard>`) where the runtime gives it (`ctx.id.name`, so it
278
+ * works in the constructor of a fresh shard), and otherwise by the claims table, which a
279
+ * shard creates on its first claim.
280
+ */
281
+ protected isReplayShard(): boolean;
282
+ /** Whether this object holds the replay claims table, which a shard creates on its first claim. */
283
+ private hasReplayClaims;
284
+ /**
285
+ * Claims `key` until `expires` in this replay shard, unless it holds an
286
+ * unexpired claim: one `INSERT … ON CONFLICT DO UPDATE … WHERE` statement, so
287
+ * concurrent claims have exactly one winner. Schedules the alarm that clears it.
288
+ */
289
+ claimReplay(key: string, expires: number): Promise<boolean>;
290
+ /** Releases `key`'s claim in this replay shard, if it has one. */
291
+ releaseReplay(key: string): Promise<void>;
292
+ /** Reads a key: its JSON text, or its stored value with `native` encoding. */
293
+ storeGet(key: string, _encoding?: durableObject.Encoding): Promise<unknown>;
294
+ /** Writes a key: JSON text, or any structured-clone value with `native` encoding. */
295
+ storePut(key: string, value: unknown, encoding?: durableObject.Encoding): Promise<void>;
296
+ /** Deletes a key. */
297
+ storeDelete(key: string, encoding?: durableObject.Encoding): Promise<void>;
298
+ /**
299
+ * Writes `value` (or deletes, for `null`) only if the key holds `expected`:
300
+ * the same JSON text, or with `native` encoding a structurally equal value.
301
+ * It reads and writes without awaiting other I/O in between, so the object's
302
+ * input gate makes it atomic.
303
+ */
304
+ storeSwap(key: string, expected: unknown, value: unknown, encoding?: durableObject.Encoding): Promise<DurableObjectStore.SwapResult<unknown>>;
305
+ /** Records a replay marker until `expires` unless the key holds a value or an unexpired marker. */
306
+ storeClaim(key: string, expires: number, encoding?: durableObject.Encoding): Promise<boolean>;
307
+ /** Handles one store operation sent by `fetch`, as JSON text. */
168
308
  fetch(request: Request): Promise<Response>;
309
+ /** The replay shard's claims table, created on first use. */
310
+ private replayClaims;
311
+ /** Writes or deletes (`null`) a key and runs {@link DurableObjectStore.onWrite}. `previous` is the value the caller already read, if any. */
312
+ private commit;
169
313
  }
170
314
  export declare namespace DurableObjectStore {
315
+ /** The subset of `DurableObjectState` the store uses. */
316
+ type State = {
317
+ /** The object's ID; `name` is set for objects addressed with `idFromName`. */
318
+ id?: {
319
+ readonly name?: string | undefined;
320
+ } | undefined;
321
+ storage: Storage;
322
+ };
171
323
  /** The subset of `DurableObjectStorage` the store uses. */
172
324
  type Storage = {
173
325
  get: (key: string) => Promise<unknown>;
174
- put: (key: string, value: string) => Promise<void>;
326
+ put(key: string, value: unknown): Promise<void>;
175
327
  delete: (key: string) => Promise<unknown>;
328
+ /** The scheduled alarm time in milliseconds, or `null`. */
329
+ getAlarm?: (() => Promise<number | null>) | undefined;
330
+ /** Schedules the object's alarm, replacing any earlier one. */
331
+ setAlarm?: ((scheduledTime: number | Date) => Promise<void>) | undefined;
332
+ /** Cancels the object's alarm. */
333
+ deleteAlarm?: (() => Promise<void>) | undefined;
334
+ /** Runs `closure` in a transaction that rolls back when it throws. */
335
+ transaction?<result>(closure: (txn: Storage) => Promise<result>): Promise<result>;
336
+ /** The object's SQLite database, on SQLite-backed objects: replay shards need it. */
337
+ sql?: Sql | undefined;
338
+ };
339
+ /** The subset of `SqlStorage` replay shards use. */
340
+ type Sql = {
341
+ exec: (query: string, ...bindings: unknown[]) => {
342
+ toArray: () => Record<string, unknown>[];
343
+ };
344
+ };
345
+ /** A replay shard's methods that {@link durableObject} calls over Workers RPC. */
346
+ type ReplayShard = {
347
+ claimReplay: (key: string, expires: number) => Promise<boolean>;
348
+ releaseReplay: (key: string) => Promise<unknown>;
176
349
  };
177
350
  /** Result of a compare-and-swap: on a conflict, the stored value, so a retry needs no read. */
178
- type SwapResult = {
351
+ type SwapResult<value = string | null> = {
179
352
  applied: true;
180
353
  } | {
181
354
  applied: false;
182
- current: string | null;
355
+ current: value;
356
+ };
357
+ /** The object's methods that {@link durableObject} calls, positionally, over Workers RPC. */
358
+ type Stub = {
359
+ storeGet: (key: string, encoding?: durableObject.Encoding) => Promise<unknown>;
360
+ storePut: (key: string, value: unknown, encoding?: durableObject.Encoding) => Promise<unknown>;
361
+ storeDelete: (key: string, encoding?: durableObject.Encoding) => Promise<unknown>;
362
+ storeSwap: (key: string, expected: unknown, value: unknown, encoding?: durableObject.Encoding) => Promise<SwapResult<unknown>>;
363
+ storeClaim: (key: string, expires: number, encoding?: durableObject.Encoding) => Promise<boolean>;
183
364
  };
184
365
  /** An operation sent from {@link durableObject} to the object. */
185
366
  type Request = {
@@ -201,6 +382,13 @@ export declare namespace DurableObjectStore {
201
382
  op: 'claim';
202
383
  key: string;
203
384
  expires: number;
385
+ } | {
386
+ op: 'claimReplay';
387
+ key: string;
388
+ expires: number;
389
+ } | {
390
+ op: 'releaseReplay';
391
+ key: string;
204
392
  };
205
393
  }
206
394
  /**