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.
- package/CHANGELOG.md +37 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +199 -11
- package/dist/Store.js +527 -48
- package/dist/cli/cli.js +141 -137
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -12
- package/dist/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- package/dist/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/internal/sse.js +32 -15
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/stripe/server/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +0 -25
- package/dist/tempo/internal/fee-payer.js +50 -3
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +98 -4
- package/dist/tempo/server/Methods.d.ts +40 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/server/ChannelStore.d.ts +5 -0
- package/dist/tempo/session/server/ChannelStore.js +2 -2
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/validation/core.js +2 -0
- 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)
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/**
|
|
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
|
|
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:
|
|
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
|
/**
|