@volter/twin-planetscale 0.1.0

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 (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +473 -0
  3. package/api/src/fetch.ts +50 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/manifest.ts +136 -0
  8. package/api/src/screens/deploy-request.tsx +111 -0
  9. package/api/src/screens/service-tokens.tsx +141 -0
  10. package/api/src/screens/session.tsx +117 -0
  11. package/api/src/semantics/audit.ts +82 -0
  12. package/api/src/semantics/backups.ts +258 -0
  13. package/api/src/semantics/branches.ts +201 -0
  14. package/api/src/semantics/deploy-requests.ts +493 -0
  15. package/api/src/semantics/index.ts +371 -0
  16. package/api/src/semantics/shared.ts +141 -0
  17. package/api/src/semantics/time.ts +77 -0
  18. package/api/src/token-gate.ts +96 -0
  19. package/dist/api/src/fetch.d.ts +8 -0
  20. package/dist/api/src/fetch.js +51 -0
  21. package/dist/api/src/fetch.ts +50 -0
  22. package/dist/api/src/generated/surface.gen.json +1 -0
  23. package/dist/api/src/generated/ui.gen.json +1 -0
  24. package/dist/api/src/index.ts +19 -0
  25. package/dist/api/src/manifest.d.ts +2 -0
  26. package/dist/api/src/manifest.js +113 -0
  27. package/dist/api/src/manifest.ts +136 -0
  28. package/dist/api/src/screens/deploy-request.d.ts +7 -0
  29. package/dist/api/src/screens/deploy-request.js +106 -0
  30. package/dist/api/src/screens/deploy-request.tsx +111 -0
  31. package/dist/api/src/screens/service-tokens.d.ts +3 -0
  32. package/dist/api/src/screens/service-tokens.js +134 -0
  33. package/dist/api/src/screens/service-tokens.tsx +141 -0
  34. package/dist/api/src/screens/session.d.ts +11 -0
  35. package/dist/api/src/screens/session.js +108 -0
  36. package/dist/api/src/screens/session.tsx +117 -0
  37. package/dist/api/src/semantics/audit.d.ts +31 -0
  38. package/dist/api/src/semantics/audit.js +80 -0
  39. package/dist/api/src/semantics/audit.ts +82 -0
  40. package/dist/api/src/semantics/backups.d.ts +37 -0
  41. package/dist/api/src/semantics/backups.js +264 -0
  42. package/dist/api/src/semantics/backups.ts +258 -0
  43. package/dist/api/src/semantics/branches.d.ts +53 -0
  44. package/dist/api/src/semantics/branches.js +197 -0
  45. package/dist/api/src/semantics/branches.ts +201 -0
  46. package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
  47. package/dist/api/src/semantics/deploy-requests.js +491 -0
  48. package/dist/api/src/semantics/deploy-requests.ts +493 -0
  49. package/dist/api/src/semantics/index.d.ts +20 -0
  50. package/dist/api/src/semantics/index.js +381 -0
  51. package/dist/api/src/semantics/index.ts +371 -0
  52. package/dist/api/src/semantics/shared.d.ts +36 -0
  53. package/dist/api/src/semantics/shared.js +132 -0
  54. package/dist/api/src/semantics/shared.ts +141 -0
  55. package/dist/api/src/semantics/time.d.ts +2 -0
  56. package/dist/api/src/semantics/time.js +81 -0
  57. package/dist/api/src/semantics/time.ts +77 -0
  58. package/dist/api/src/token-gate.d.ts +9 -0
  59. package/dist/api/src/token-gate.js +97 -0
  60. package/dist/api/src/token-gate.ts +96 -0
  61. package/dist/src/cli.d.ts +2 -0
  62. package/dist/src/cli.js +61 -0
  63. package/dist/src/generated/surface.gen.json +1 -0
  64. package/dist/src/generated/ui.gen.json +1 -0
  65. package/dist/src/index.d.ts +26 -0
  66. package/dist/src/index.js +156 -0
  67. package/dist/src/manifest.d.ts +2 -0
  68. package/dist/src/manifest.js +41 -0
  69. package/dist/src/planetscale-budget.d.ts +78 -0
  70. package/dist/src/planetscale-budget.js +305 -0
  71. package/dist/src/planetscale-capabilities.d.ts +10 -0
  72. package/dist/src/planetscale-capabilities.js +3977 -0
  73. package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
  74. package/dist/src/planetscale-collation-weights.gen.js +12 -0
  75. package/dist/src/planetscale-collation.d.ts +70 -0
  76. package/dist/src/planetscale-collation.js +391 -0
  77. package/dist/src/planetscale-conformance.d.ts +8 -0
  78. package/dist/src/planetscale-conformance.js +213 -0
  79. package/dist/src/planetscale-connector.d.ts +150 -0
  80. package/dist/src/planetscale-connector.js +532 -0
  81. package/dist/src/planetscale-deploy.d.ts +26 -0
  82. package/dist/src/planetscale-deploy.js +235 -0
  83. package/dist/src/planetscale-information-schema.d.ts +32 -0
  84. package/dist/src/planetscale-information-schema.js +299 -0
  85. package/dist/src/planetscale-mysql.d.ts +33 -0
  86. package/dist/src/planetscale-mysql.js +547 -0
  87. package/dist/src/planetscale-roles.d.ts +11 -0
  88. package/dist/src/planetscale-roles.js +60 -0
  89. package/dist/src/planetscale-row.d.ts +12 -0
  90. package/dist/src/planetscale-row.js +39 -0
  91. package/dist/src/planetscale-server.d.ts +42 -0
  92. package/dist/src/planetscale-server.js +137 -0
  93. package/dist/src/planetscale-sql.d.ts +701 -0
  94. package/dist/src/planetscale-sql.js +7167 -0
  95. package/dist/src/planetscale-store.d.ts +126 -0
  96. package/dist/src/planetscale-store.js +827 -0
  97. package/dist/src/planetscale-twin.d.ts +48 -0
  98. package/dist/src/planetscale-twin.js +290 -0
  99. package/dist/src/planetscale-values.d.ts +139 -0
  100. package/dist/src/planetscale-values.js +719 -0
  101. package/dist/src/planetscale-wire.d.ts +110 -0
  102. package/dist/src/planetscale-wire.js +188 -0
  103. package/dist/src/semantics/psdb.d.ts +18 -0
  104. package/dist/src/semantics/psdb.js +30 -0
  105. package/package.json +58 -0
  106. package/src/cli.ts +58 -0
  107. package/src/generated/surface.gen.json +1 -0
  108. package/src/generated/ui.gen.json +1 -0
  109. package/src/index.ts +267 -0
  110. package/src/manifest.ts +60 -0
  111. package/src/planetscale-budget.ts +347 -0
  112. package/src/planetscale-capabilities.ts +3862 -0
  113. package/src/planetscale-collation-weights.gen.ts +13 -0
  114. package/src/planetscale-collation.ts +378 -0
  115. package/src/planetscale-conformance.ts +237 -0
  116. package/src/planetscale-connector.ts +571 -0
  117. package/src/planetscale-deploy.ts +197 -0
  118. package/src/planetscale-information-schema.ts +322 -0
  119. package/src/planetscale-mysql.ts +339 -0
  120. package/src/planetscale-roles.ts +71 -0
  121. package/src/planetscale-row.ts +43 -0
  122. package/src/planetscale-server.ts +162 -0
  123. package/src/planetscale-sql.ts +5957 -0
  124. package/src/planetscale-store.ts +869 -0
  125. package/src/planetscale-twin.ts +338 -0
  126. package/src/planetscale-values.ts +572 -0
  127. package/src/planetscale-wire.ts +274 -0
  128. package/src/semantics/psdb.ts +57 -0
@@ -0,0 +1,347 @@
1
+ // PLANETSCALE'S CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus
2
+ // `guardPlanetscaleClient`, the choke point every live PlanetScale call goes through. The MECHANISM
3
+ // — the durable token-keyed ledger, the rolling window, reserve-under-lock, the `Retry-After`/429
4
+ // cooldown, fail-CLOSED on a corrupt ledger — lives ONCE in the vendor-agnostic kernel
5
+ // (`@volter/world-core` → `rateBudget.ts`). Read that module's header for the full rationale AND for the
6
+ // honest list of what the guard does NOT guarantee. This module is modeled on
7
+ // `notion-budget.ts` / `upstash-budget.ts`, the reference injected-client decorators.
8
+ //
9
+ // ── THE NUMBER, AND WHY IT IS THE KERNEL FALLBACK RATHER THAN A TRANSCRIBED LIMIT ─────────────
10
+ // PlanetScale publishes system limits and this build LIVE-READ them
11
+ // (planetscale.com/docs/reference/planetscale-system-limits, 2026-08-31). Every published figure is
12
+ // a PER-QUERY or PER-SCHEMA bound, not a rate:
13
+ // • per-query rows returned/updated/deleted: 100k • per-query result set: 64 MiB
14
+ // • autocommit timeout: 900s • transaction timeout: 20s
15
+ // • tables per schema: 2048 • columns per table: 1017
16
+ // There is NO published requests-per-second or requests-per-minute figure for the psdb HTTP API,
17
+ // and this build did not find one. Per ADDING_A_TWIN's rule for exactly that case, the reason says
18
+ // so plainly and the ceiling stays AT the kernel's austere fallback — 60 units / 60s at the default
19
+ // weight of 2 = 30 calls per minute — rather than dressing a guess up as a vendor fact. Nothing
20
+ // here is more permissive than `DEFAULT_RATE_BUDGET`, so no `VENDOR_BURST_ANCHOR` figure is owed.
21
+ //
22
+ // ── WHAT THE RISK ACTUALLY IS ─────────────────────────────────────────────────────────────────
23
+ // PlanetScale meters ROWS READ, not requests. One `SELECT` against a large table is a single call
24
+ // that can read millions of rows, so a request-count ceiling bounds the SHAPE of a runaway loop
25
+ // without bounding its cost. That is a real limitation of a request-count guard against this vendor
26
+ // and it is stated rather than papered over: the connector's own defence is that every pull takes a
27
+ // hard result `limit` (not a storage rows-examined guarantee), and `transaction` is priced at 4x because ONE guarded call fans out into an
28
+ // unbounded number of statements behind it (`Connection.transaction` in dist/index.js runs the
29
+ // caller's whole callback).
30
+ import {
31
+ declareRateBudget,
32
+ rateBudgetPath,
33
+ rateBudgetWeight,
34
+ RateBudget,
35
+ RateBudgetError,
36
+ type RateBudgetDeclaration,
37
+ type RateBudgetOptions,
38
+ type RateBudgetReservation,
39
+ type RateBudgetSnapshot,
40
+ } from '@volter/world-core';
41
+ import type { PlanetscaleLikeClient } from './planetscale-connector.ts';
42
+
43
+ const VENDOR = 'planetscale';
44
+
45
+ /** Rolling window, in ms. Spend older than this is pruned. */
46
+ export const PLANETSCALE_BUDGET_WINDOW_MS = 60_000;
47
+
48
+ /** Weighted units allowed inside one window. See the header for where this number comes from. */
49
+ export const PLANETSCALE_BUDGET_CEILING = 60;
50
+
51
+ /** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly. */
52
+ export const PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S = 300;
53
+
54
+ /** Per-call cost, keyed by the client method the guard is about to invoke. See the header. */
55
+ export const PLANETSCALE_CALL_WEIGHTS = {
56
+ /** `transaction` — ONE guarded call that runs an unbounded number of statements behind it. */
57
+ transaction: 8,
58
+ /** `execute` / `refresh` / anything unclassified. */
59
+ other: 2,
60
+ } as const;
61
+
62
+ /**
63
+ * The client methods this pack PRICES BY NAME. `@planetscale/database`'s `Client` and `Connection`
64
+ * expose exactly these three (dist/index.d.ts), and the connector calls only `execute`.
65
+ *
66
+ * NOT a closed list: a method absent from it is still priced at `defaultWeight` through the
67
+ * passthrough proxy. An unmodeled call must never be free — free would also make it invisible.
68
+ */
69
+ export const PLANETSCALE_BUDGETED_METHODS = ['execute', 'transaction', 'refresh'] as const;
70
+
71
+ /** THE PACK'S DECLARATION — pure data, the only PlanetScale-specific thing in the whole budget. */
72
+ export const PLANETSCALE_RATE_BUDGET: RateBudgetDeclaration = {
73
+ windowMs: PLANETSCALE_BUDGET_WINDOW_MS,
74
+ ceiling: PLANETSCALE_BUDGET_CEILING,
75
+ defaultWeight: PLANETSCALE_CALL_WEIGHTS.other,
76
+ maxRetryAfterSeconds: PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S,
77
+ // Ordered: the kernel prices FIRST-MATCH-WINS, so the most expensive tier is listed first.
78
+ rules: [
79
+ { match: '(^|\\.)transaction$', weight: PLANETSCALE_CALL_WEIGHTS.transaction },
80
+ ],
81
+ reason:
82
+ 'PlanetScale publishes NO requests-per-second or requests-per-minute limit for the psdb HTTP API. Its '
83
+ + 'system-limits page (https://planetscale.com/docs/reference/planetscale-system-limits, live-read '
84
+ + '2026-08-31) publishes only PER-QUERY and PER-SCHEMA bounds: 100k rows returned/updated/deleted per '
85
+ + 'query, a 64 MiB per-query result set, a 900s autocommit timeout, a 20s transaction timeout, 2048 tables '
86
+ + 'per schema and 1017 columns per table. None of those is a call rate, so — per the ADDING_A_TWIN rule for '
87
+ + 'a vendor that publishes no scalar rate — this declaration states that plainly and stays AT the kernel '
88
+ + "fallback (60 units / 60s at weight 2 = 30 calls per minute) instead of inventing a number. It is "
89
+ + 'therefore never more permissive than DEFAULT_RATE_BUDGET and owes no VENDOR_BURST_ANCHOR figure. The '
90
+ + 'real cost driver is metered ROWS READ rather than request count, which a request-count ceiling can only '
91
+ + 'bound crudely; that limitation is disclosed rather than hidden, and the connector answers it separately '
92
+ + 'by bounding returned pages on every pull; SQL LIMIT does not cap rows examined or billed. `transaction` is priced at 8 because ONE guarded call runs '
93
+ + "the caller's entire callback — an unbounded number of statements — behind it "
94
+ + '(@planetscale/database@1.20.1 dist/index.js, `Connection.transaction`).',
95
+ };
96
+
97
+ // Declared at module load, so merely importing this module (which `planetscale-connector.ts` does)
98
+ // is enough to arm the real ceiling.
99
+ declareRateBudget(VENDOR, PLANETSCALE_RATE_BUDGET);
100
+
101
+ /** Price one PlanetScale call by its client method name (`execute`, `transaction`, …). */
102
+ export function planetscaleCallWeight(method: string): number {
103
+ return rateBudgetWeight(VENDOR, method);
104
+ }
105
+
106
+ /** Where PlanetScale's ledger lives. Token-keyed and cwd-independent by default (the vendor meters
107
+ * per credential, so a cwd-scoped ledger would hand the same credential a fresh allowance in every
108
+ * checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
109
+ export function planetscaleBudgetPath(opts: { root?: string; token?: string } | string = {}): string {
110
+ const o = typeof opts === 'string' ? { root: opts } : opts;
111
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
112
+ // excess-property check only catches object literals) must not redirect this pack's ledger.
113
+ return rateBudgetPath({ ...o, vendor: VENDOR });
114
+ }
115
+
116
+ /** Construction options for PlanetScale's budget. The vendor is fixed; everything else may only TIGHTEN. */
117
+ export type PlanetscaleBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
118
+
119
+ /**
120
+ * PlanetScale's budget — the shared kernel guard bound to this vendor's declaration. A real
121
+ * subclass, not an alias, so `budget instanceof PlanetscaleBudget` means "a budget that accounts
122
+ * against this vendor's ledger under this vendor's ceiling".
123
+ */
124
+ export class PlanetscaleBudget extends RateBudget {
125
+ constructor(opts: PlanetscaleBudgetOptions = {}) {
126
+ super({ ...opts, vendor: VENDOR });
127
+ }
128
+ }
129
+
130
+ export type { RateBudgetErrorKind as PlanetscaleBudgetErrorKind } from '@volter/world-core';
131
+ export { RateBudgetError as PlanetscaleBudgetError } from '@volter/world-core';
132
+ export type PlanetscaleBudgetReservation = RateBudgetReservation;
133
+ export type PlanetscaleBudgetSnapshot = RateBudgetSnapshot;
134
+
135
+ /** What every budgeted connector entrypoint accepts. There is deliberately no option that turns the
136
+ * guard OFF — only ones that say WHICH ledger and clock to account against. */
137
+ export type PlanetscaleBudgetedOptions = {
138
+ /** An existing budget to share across calls. Omit and one is constructed. Cannot be null. */
139
+ budget?: PlanetscaleBudget;
140
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
141
+ budgetOptions?: PlanetscaleBudgetOptions;
142
+ };
143
+
144
+ /** Pull the budget wiring out of a caller's opts bag, so an entrypoint can forward it verbatim. */
145
+ export function planetscaleBudgetOf(opts: PlanetscaleBudgetedOptions): PlanetscaleBudgetedOptions {
146
+ return {
147
+ ...(opts.budget !== undefined ? { budget: opts.budget } : {}),
148
+ ...(opts.budgetOptions !== undefined ? { budgetOptions: opts.budgetOptions } : {}),
149
+ };
150
+ }
151
+
152
+ // ── the choke point ─────────────────────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * Marks a client this module has already wrapped, so guarding twice cannot charge twice.
156
+ *
157
+ * A MODULE-PRIVATE `Symbol()`, deliberately not `Symbol.for()`: a global-registry symbol is
158
+ * reachable BY NAME, so any caller could stamp the brand on a RAW client and the guard would hand it
159
+ * straight back UNGUARDED — a one-line bypass of the whole budget.
160
+ */
161
+ const GUARDED: unique symbol = Symbol('@volter/twin-planetscale.budget.guarded');
162
+
163
+ type Guarded = { [GUARDED]?: unknown };
164
+
165
+ /** Is this client already behind a budget? Returns the budget it is behind, if so. */
166
+ export function planetscaleClientBudget(client: unknown): RateBudget | undefined {
167
+ const mark = (client as Guarded | null)?.[GUARDED];
168
+ return mark instanceof RateBudget ? mark : undefined;
169
+ }
170
+
171
+ const NEVER_WRAP = new Set(['then', 'catch', 'finally', 'constructor', 'prototype', 'toJSON', 'toString', 'valueOf', 'inspect']);
172
+
173
+ /** The HTTP status a value carries, if it looks like one (an SDK error, or a raw response). */
174
+ function responseStatus(v: unknown): number | undefined {
175
+ const s = (v as { status?: unknown } | null)?.status;
176
+ return typeof s === 'number' && Number.isFinite(s) ? s : undefined;
177
+ }
178
+
179
+ /**
180
+ * Response/error headers as a plain lower-cased record, or `undefined` when there are none. Accepts
181
+ * a `Headers` instance, a `Map`, or a plain object — `DatabaseError`/`UnknownError` carry a
182
+ * `context.headers` record (dist/index.js), and a raw `Response` carries a `Headers`.
183
+ */
184
+ function responseHeaders(v: unknown): Record<string, string> | undefined {
185
+ const direct = (v as { headers?: unknown } | null)?.headers;
186
+ const nested = (v as { context?: { headers?: unknown } } | null)?.context?.headers;
187
+ const h = direct ?? nested;
188
+ if (!h || typeof h !== 'object') return undefined;
189
+ const out: Record<string, string> = {};
190
+ if (typeof (h as Headers).forEach === 'function') {
191
+ (h as Headers).forEach((value: string, key: string) => { out[String(key).toLowerCase()] = String(value); });
192
+ } else {
193
+ for (const [k, value] of Object.entries(h as Record<string, unknown>)) out[k.toLowerCase()] = String(value);
194
+ }
195
+ return Object.keys(out).length > 0 ? out : undefined;
196
+ }
197
+
198
+ // Guard ownership stays module-private; known deployment batches can reserve once without
199
+ // exposing an unguarded client to callers or interrupting rollback with a second admission.
200
+ const rawClients = new WeakMap<object, PlanetscaleLikeClient>();
201
+ function settleBudget(budget: RateBudget, weight: number, reservation: RateBudgetReservation, value: unknown): void {
202
+ if (value instanceof AggregateError) {
203
+ for (const error of value.errors) settleBudget(budget, weight, reservation, error);
204
+ return;
205
+ }
206
+ budget.recordCall(weight, responseHeaders(value), { status: responseStatus(value), reservation });
207
+ }
208
+ /** Settle once the call RESOLVED: PlanetScale has answered. recordCall arms any cooldown before it
209
+ * throws (a back-off beyond the cap), so that refusal is swallowed and the result kept — a write
210
+ * that committed is never reported failed and run again on retry. A non-2xx status still throws. */
211
+ function settleAnswered(budget: RateBudget, weight: number, reservation: RateBudgetReservation, value: unknown): void {
212
+ try {
213
+ settleBudget(budget, weight, reservation, value);
214
+ } catch (error) {
215
+ const status = responseStatus(value);
216
+ if (!(error instanceof RateBudgetError) || (status !== undefined && (status < 200 || status >= 300))) throw error;
217
+ }
218
+ }
219
+ async function charged<T>(budget: RateBudget, weight: number, invoke: () => Promise<T>): Promise<T> {
220
+ const reservation = budget.checkBudget(weight);
221
+ try {
222
+ const result = await invoke(); settleAnswered(budget, weight, reservation, result); return result;
223
+ } catch (error) { settleBudget(budget, weight, reservation, error); throw error; }
224
+ }
225
+
226
+ /** Reserve every known row call plus session, BEGIN, COMMIT and possible ROLLBACK before IO. */
227
+ export async function runPlanetscaleTransaction<T>(client: PlanetscaleLikeClient, statementCount: number,
228
+ run: (tx: unknown) => Promise<T>, opts: PlanetscaleBudgetedOptions = {},
229
+ ): Promise<T> {
230
+ if (!Number.isSafeInteger(statementCount) || statementCount < 0) throw new Error('Invalid PlanetScale statement count');
231
+ const guarded = guardPlanetscaleClient(client, opts);
232
+ const raw = rawClients.get(guarded)!;
233
+ if (!raw?.transaction) throw new Error('PlanetScale deployment requires a transactional client');
234
+ const budget = planetscaleClientBudget(guarded)!;
235
+ return charged(budget, Math.max(budget.weightFor('transaction'), (statementCount + 4) * budget.weightFor('execute')), () => raw.transaction!(run));
236
+ }
237
+
238
+ /**
239
+ * Wrap an INJECTED PlanetScale client so EVERY call it makes is charged against the shared budget
240
+ * BEFORE the request goes out. This pack never constructs the transport itself (the consumer injects
241
+ * something satisfying `PlanetscaleLikeClient` — a real `Client` or `Connection` is assignable
242
+ * as-is), so the guard is a DECORATOR rather than a factory, which is exactly why every connector
243
+ * entrypoint applies it UNCONDITIONALLY instead of trusting the caller.
244
+ *
245
+ * IDEMPOTENT: wrapping an already-guarded client returns it unchanged.
246
+ *
247
+ * A method that THROWS is still inspected: `@planetscale/database` RAISES `DatabaseError` /
248
+ * `UnknownError` on a non-2xx rather than returning it, and that error's status and headers are
249
+ * exactly the signal that must become a persisted cooldown. The original error is always re-raised
250
+ * afterwards — EXCEPT when the back-off is beyond the cap, where the budget's own louder "stop
251
+ * calling" error takes precedence.
252
+ */
253
+ export function guardPlanetscaleClient(client: PlanetscaleLikeClient, opts: PlanetscaleBudgetedOptions = {}): PlanetscaleLikeClient {
254
+ // There is no value a caller can pass to end up with an UNGUARDED client. Validated BEFORE the
255
+ // already-guarded early return, so `guard(alreadyGuarded, { budget: impostor })` is refused too.
256
+ if (opts.budget !== undefined && opts.budget !== null && !(opts.budget instanceof PlanetscaleBudget)) {
257
+ throw new Error('guardPlanetscaleClient: `budget` must be a PlanetscaleBudget — refusing to guard a PlanetScale client with an unverified rate guard');
258
+ }
259
+ if (planetscaleClientBudget(client)) return client;
260
+ const budget = opts.budget instanceof PlanetscaleBudget
261
+ ? opts.budget
262
+ : new PlanetscaleBudget({
263
+ // The default ledger is keyed by a hash of the credential — the vendor meters per credential,
264
+ // so a cwd-scoped ledger would hand it a fresh allowance per worktree/CI leg.
265
+ //
266
+ // HONESTLY: this pack does NOT hold the credential — the consumer's injected client does — so
267
+ // `PLANETSCALE_DATABASE_URL` is a BEST-EFFORT stand-in, not the real thing. If it names a
268
+ // different database than the injected client, spend is booked against the wrong ledger; if it
269
+ // is unset, every unattributed PlanetScale credential on the machine shares one (over-tight,
270
+ // the safe direction). A caller who knows the credential says so with `budgetOptions: { token }`,
271
+ // which is spread last and therefore wins.
272
+ ...(process.env.PLANETSCALE_DATABASE_URL !== undefined ? { token: process.env.PLANETSCALE_DATABASE_URL } : {}),
273
+ ...(opts.budgetOptions ?? {}),
274
+ });
275
+
276
+ /** Settle a reservation from whatever the call produced. May THROW (a back-off past the cap). */
277
+ const settle = (weight: number, reservation: RateBudgetReservation, value: unknown): void => settleBudget(budget, weight, reservation, value);
278
+
279
+ /**
280
+ * Charge, call, settle. Refusing REJECTS rather than throwing synchronously, so
281
+ * `client.execute(...).catch(…)` behaves exactly as it does on an unguarded client.
282
+ * `checkBudget` RESERVES under lock, so nothing after its line runs when the budget refuses:
283
+ * the request is never made.
284
+ */
285
+ const chargeAsync = (method: string, invoke: (...args: unknown[]) => unknown) =>
286
+ async (...args: unknown[]): Promise<unknown> => charged(budget, budget.weightFor(method), async () => invoke(...args));
287
+
288
+ const guarded: Record<string, unknown> & Guarded = {};
289
+ for (const name of PLANETSCALE_BUDGETED_METHODS) {
290
+ const raw = (client as unknown as Record<string, unknown>)[name];
291
+ if (typeof raw !== 'function') continue;
292
+ // Resolved at CALL time, not here, so a client whose method is swapped later is still charged.
293
+ guarded[name] = chargeAsync(name, (...args) => ((client as unknown as Record<string, unknown>)[name] as (...a: unknown[]) => unknown).apply(client, args));
294
+ }
295
+ Object.defineProperty(guarded, GUARDED, { value: budget, enumerable: false });
296
+
297
+ // A real client has MORE than the three methods above (`connection()`, `config`, …) and a consumer
298
+ // who needs one must not be forced to keep the RAW client alongside — every call through that
299
+ // would be unbudgeted. So the guarded object is a Proxy: modeled members come from the map above,
300
+ // anything else is taken from the real client and PRICED at `defaultWeight`.
301
+ const proxy = new Proxy(guarded, {
302
+ get(target, prop, receiver) {
303
+ if (typeof prop === 'symbol') {
304
+ const own = Reflect.get(target, prop, receiver);
305
+ return own !== undefined ? own : (client as unknown as Record<symbol, unknown>)[prop];
306
+ }
307
+ const name = String(prop);
308
+ if (NEVER_WRAP.has(name)) return Reflect.get(target, prop, receiver);
309
+ const own = Reflect.get(target, prop, receiver);
310
+ if (own !== undefined) return own;
311
+ const from = (client as unknown as Record<string, unknown>)[name];
312
+ // `connection()` returns a NEW Connection whose `execute` would otherwise escape the budget
313
+ // entirely, so a function member is charged AND its result is re-guarded when it looks like a
314
+ // client. Anything else passes through.
315
+ if (typeof from === 'function') {
316
+ return (...args: unknown[]) => {
317
+ const weight = budget.weightFor(name);
318
+ const reservation = budget.checkBudget(weight);
319
+ let out: unknown;
320
+ try {
321
+ out = (from as (...a: unknown[]) => unknown).apply(client, args);
322
+ } catch (e) {
323
+ settle(weight, reservation, e);
324
+ throw e;
325
+ }
326
+ if (out !== null && typeof out === 'object' && typeof (out as { then?: unknown }).then === 'function') {
327
+ return (out as Promise<unknown>).then(
328
+ (res) => { settleAnswered(budget, weight, reservation, res); return res; },
329
+ (e: unknown) => { settle(weight, reservation, e); throw e; },
330
+ );
331
+ }
332
+ settle(weight, reservation, undefined);
333
+ if (out !== null && typeof out === 'object' && typeof (out as { execute?: unknown }).execute === 'function') {
334
+ return guardPlanetscaleClient(out as PlanetscaleLikeClient, { budget });
335
+ }
336
+ return out;
337
+ };
338
+ }
339
+ return from;
340
+ },
341
+ has(target, prop) {
342
+ return Reflect.has(target, prop) || Reflect.has(client as object, prop);
343
+ },
344
+ }) as unknown as PlanetscaleLikeClient;
345
+ rawClients.set(proxy, client);
346
+ return proxy;
347
+ }