@happyvertical/smrt-core 0.51.27 → 0.51.29

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 (76) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +47 -0
  3. package/agents/build-knowledge.md +16 -0
  4. package/agents/change-feed.md +95 -4
  5. package/agents/change-signals.md +6 -1
  6. package/agents/run-once.md +268 -0
  7. package/agents/schema-paths.md +17 -5
  8. package/dist/browser.js +4 -3
  9. package/dist/change-feed-authz.d.ts +141 -0
  10. package/dist/change-feed-authz.d.ts.map +1 -0
  11. package/dist/change-feed-authz.js +260 -0
  12. package/dist/change-feed-authz.js.map +1 -0
  13. package/dist/change-feed.d.ts +15 -0
  14. package/dist/change-feed.d.ts.map +1 -1
  15. package/dist/change-feed.js +4 -0
  16. package/dist/change-feed.js.map +1 -1
  17. package/dist/errors.d.ts +28 -0
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +36 -1
  20. package/dist/errors.js.map +1 -1
  21. package/dist/generators/changes-route.d.ts.map +1 -1
  22. package/dist/generators/changes-route.js +13 -3
  23. package/dist/generators/changes-route.js.map +1 -1
  24. package/dist/generators/events-route.d.ts +35 -4
  25. package/dist/generators/events-route.d.ts.map +1 -1
  26. package/dist/generators/events-route.js +78 -14
  27. package/dist/generators/events-route.js.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -3
  31. package/dist/knowledge-graph.d.ts.map +1 -1
  32. package/dist/knowledge-graph.js +1 -1
  33. package/dist/knowledge-graph.js.map +1 -1
  34. package/dist/manifest/static-manifest.js +1 -1
  35. package/dist/manifest/static-manifest.js.map +1 -1
  36. package/dist/manifest/store.js +1 -1
  37. package/dist/manifest.json +1 -1
  38. package/dist/migrations/differ.d.ts +6 -3
  39. package/dist/migrations/differ.d.ts.map +1 -1
  40. package/dist/migrations/differ.js +11 -5
  41. package/dist/migrations/differ.js.map +1 -1
  42. package/dist/registry/class-registration.d.ts.map +1 -1
  43. package/dist/registry/class-registration.js +21 -1
  44. package/dist/registry/class-registration.js.map +1 -1
  45. package/dist/registry/types.d.ts +3 -1
  46. package/dist/registry/types.d.ts.map +1 -1
  47. package/dist/run-once.d.ts +80 -0
  48. package/dist/run-once.d.ts.map +1 -0
  49. package/dist/run-once.js +311 -0
  50. package/dist/run-once.js.map +1 -0
  51. package/dist/smrt-knowledge.json +20 -14
  52. package/dist/system/index.d.ts +1 -0
  53. package/dist/system/index.d.ts.map +1 -1
  54. package/dist/system/index.js +3 -2
  55. package/dist/system/retention.d.ts +53 -4
  56. package/dist/system/retention.d.ts.map +1 -1
  57. package/dist/system/retention.js +67 -14
  58. package/dist/system/retention.js.map +1 -1
  59. package/dist/system/schema.d.ts +33 -1
  60. package/dist/system/schema.d.ts.map +1 -1
  61. package/dist/system/schema.js +60 -3
  62. package/dist/system/schema.js.map +1 -1
  63. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  64. package/dist/vite-plugin/changes-route.js +21 -5
  65. package/dist/vite-plugin/changes-route.js.map +1 -1
  66. package/dist/vite-plugin/events-route.d.ts.map +1 -1
  67. package/dist/vite-plugin/events-route.js +9 -0
  68. package/dist/vite-plugin/events-route.js.map +1 -1
  69. package/dist/vite-plugin/index.d.ts.map +1 -1
  70. package/dist/vite-plugin/index.js +14 -0
  71. package/dist/vite-plugin/index.js.map +1 -1
  72. package/dist/vite-plugin/worker-registration.d.ts +34 -0
  73. package/dist/vite-plugin/worker-registration.d.ts.map +1 -0
  74. package/dist/vite-plugin/worker-registration.js +108 -0
  75. package/dist/vite-plugin/worker-registration.js.map +1 -0
  76. package/package.json +4 -4
@@ -928,7 +928,9 @@ export interface RegisteredClass {
928
928
  * (which is snake_case and can be overridden/uncountable). Stored here so
929
929
  * runtime consumers (e.g. `createResourceListHandler`) can build URLs that
930
930
  * match the generated routes instead of re-deriving from `tableName`.
931
- * See smrt#1311 consumer-migration finding.
931
+ * See smrt#1311 consumer-migration finding. A class registered before its
932
+ * manifest takes its registered STI base's collection when it is an STI
933
+ * subtype, and a manifest registered later reconciles it (#3125).
932
934
  */
933
935
  collection?: string;
934
936
  /** Parent class name (for inheritance chain tracking) */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/registry/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ import { RUN_ONCE_CLAIMS_TABLE } from './system/schema.js';
3
+ export { RUN_ONCE_CLAIMS_TABLE };
4
+ /** Input identifying one submission for {@link runOnce}. */
5
+ export interface RunOnceParams {
6
+ /** Transaction-capable database handle. May itself already be inside a transaction. */
7
+ db: DatabaseInterface;
8
+ /** Tenant scope. Part of the claim key: claims never cross tenants. */
9
+ tenantId: string;
10
+ /** The submitting principal. Part of the claim key: claims never cross actors. */
11
+ actor: string;
12
+ /** The per-form/per-submission token minted by the caller (e.g. a hidden form field). */
13
+ token: string;
14
+ /**
15
+ * The content being written: plain JSON data, or a `FormData` (digested by
16
+ * its ordered entries; see {@link digestRunOnceContent}). Only its
17
+ * digest is persisted — never the raw value.
18
+ */
19
+ content: unknown;
20
+ }
21
+ interface RunOnceClaimRow {
22
+ claim_key: string;
23
+ status: string;
24
+ result: string | null;
25
+ }
26
+ /**
27
+ * Deterministic content digest: sha256 of sorted-key JSON.
28
+ *
29
+ * Property order never changes the digest, so callers do not need to worry
30
+ * about object construction order producing two different claims for what is
31
+ * semantically the same submission. Content is normalized exactly once (see
32
+ * {@link normalizeRunOnceContent}): `toJSON()` values such as `Date` digest as
33
+ * what they serialize to, a `FormData` digests as its ordered entries in a
34
+ * separate hash domain, and values JSON would reduce to `{}` (`Map`, `Set`,
35
+ * `RegExp`, …) are rejected.
36
+ *
37
+ * @throws {TypeError} when `content` contains a value with no faithful JSON form.
38
+ */
39
+ export declare function digestRunOnceContent(content: unknown): string;
40
+ /**
41
+ * Derive the {@link runOnce} claim key.
42
+ *
43
+ * NOT the token alone — see the module doc for why that collapses distinct
44
+ * submissions. Folds in `tenantId`, `actor`, the raw `token`, and the
45
+ * content digest, all through sorted-key JSON before hashing.
46
+ */
47
+ export declare function deriveRunOnceClaimKey(params: {
48
+ tenantId: string;
49
+ actor: string;
50
+ token: string;
51
+ contentDigest: string;
52
+ }): string;
53
+ /**
54
+ * Resolve an existing claim row into either a replayable result or a typed
55
+ * error. Pulled out of {@link runOnce} as a pure function so both branches
56
+ * (`completed` vs. anything else vs. unreadable) are directly unit-testable
57
+ * without forcing a genuine database race.
58
+ */
59
+ export declare function resolveExistingRunOnceClaim<T>(existing: RunOnceClaimRow | null, claimKey: string): T;
60
+ /**
61
+ * Run `work` exactly once per submission.
62
+ *
63
+ * ```ts
64
+ * const purchaseOrderId = await runOnce(
65
+ * { db, tenantId, actor, token, content: formData },
66
+ * async (tx) => createPurchaseOrder(tx, formData),
67
+ * );
68
+ * ```
69
+ *
70
+ * Resolves to `work`'s return value on both a fresh run and a replay of a
71
+ * prior completed run — the caller cannot tell the difference from the
72
+ * return value alone (by design: both are "your submission is durably
73
+ * applied"). Rejects with `RunOnceClaimError` (`./errors.ts`) when the claim
74
+ * cannot be resolved to a definite result right now; see the module doc for
75
+ * the `RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN` distinction. Any
76
+ * other rejection (including one from `work` itself) propagates unchanged,
77
+ * and the claim is rolled back with the transaction so a retry can run again.
78
+ */
79
+ export declare function runOnce<T>(params: RunOnceParams, work: (tx: DatabaseInterface) => Promise<T>): Promise<T>;
80
+ //# sourceMappingURL=run-once.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-once.d.ts","sourceRoot":"","sources":["../src/run-once.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4GG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,uFAAuF;IACvF,EAAE,EAAE,iBAAiB,CAAC;IACtB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AA0ID;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAS7D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAQT;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAC3C,QAAQ,EAAE,eAAe,GAAG,IAAI,EAChC,QAAQ,EAAE,MAAM,GACf,CAAC,CAQH;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAC7B,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GAC1C,OAAO,CAAC,CAAC,CAAC,CAmDZ"}
@@ -0,0 +1,311 @@
1
+ import { RunOnceClaimError } from "./errors.js";
2
+ import { RUN_ONCE_CLAIMS_TABLE } from "./system/schema.js";
3
+ import { stableStringify } from "./knowledge-graph.js";
4
+ import { createHash } from "node:crypto";
5
+ //#region src/run-once.ts
6
+ /**
7
+ * Idempotency seam: run an action exactly once per submission (#3080).
8
+ *
9
+ * Every consumer with a form, a retried request, or a double-tapped button
10
+ * needs the same guard: a claim row, insert-only against a UNIQUE column
11
+ * (`claim_key`) in the SAME transaction as the work it guards, so a retry
12
+ * either runs the work once or replays the first attempt's stored result.
13
+ * This module is that primitive. It is DB/transaction-level — it takes a
14
+ * `DatabaseInterface`, not a `SmrtObject`/`SmrtCollection` — so it works
15
+ * outside SvelteKit and outside the ORM: any caller with a database handle
16
+ * and a JSON-serializable work result can use it. The claim insert itself
17
+ * is raw SQL (`## Concurrent submissions` below explains why), not the
18
+ * ORM's `SmrtObject`-level `_insertOnly` option.
19
+ *
20
+ * ## Key derivation (not the token alone)
21
+ *
22
+ * The claim key is the sha256 hex digest of the sorted-key-JSON encoding of
23
+ * `[tenantId, actor, token, contentDigest]`, where `contentDigest` is itself
24
+ * the sha256 hex digest of the sorted-key-JSON encoding of `content`
25
+ * ({@link digestRunOnceContent}). Sorted-key JSON ({@link stableStringify},
26
+ * `./knowledge-graph.ts`) makes the digest stable regardless of property
27
+ * insertion order.
28
+ *
29
+ * The token alone is NOT the key. A first attempt at this exact problem
30
+ * (reported against #3080) keyed on the token alone and silently collapsed
31
+ * two genuinely different submissions that happened to reuse one form's
32
+ * token. Folding in the content digest means a retry of the SAME submission
33
+ * (same token, same content) replays; a DIFFERENT submission through the
34
+ * same token (edited content before a resubmit) runs as its own claim.
35
+ * Folding in `tenantId` and `actor` means two tenants or two actors can never
36
+ * collide on the same claim even if a token were somehow shared between them.
37
+ *
38
+ * Neither the raw token nor the raw content is ever persisted — only their
39
+ * digests — so the claims table (`_smrt_run_once_claims`,
40
+ * `./system/schema.ts`) cannot leak submitted business data.
41
+ *
42
+ * ## Atomicity
43
+ *
44
+ * `runOnce()` opens its own `db.transaction()` (or nests under the caller's,
45
+ * per `@happyvertical/sql`'s own SAVEPOINT/`NestedTransactionError` rules —
46
+ * see that package's `DatabaseInterface.transaction` doc) and, inside it:
47
+ *
48
+ * 1. Inserts the claim row (`status: 'in_progress'`).
49
+ * 2. Runs `work(tx)`, passing the SAME transaction-bound handle, so the
50
+ * caller's writes and the claim insert commit or roll back together.
51
+ * 3. Updates the claim row to `status: 'completed'` with the JSON-serialized
52
+ * result, still inside the same transaction.
53
+ *
54
+ * If `work()` throws, the callback throws, and `db.transaction()`'s own
55
+ * "commits on success, rolls back on error" contract undoes the claim insert
56
+ * along with whatever `work()` partially wrote — a claim is never stranded
57
+ * `'in_progress'` by a failed attempt, and a retry with the same key can
58
+ * always run again.
59
+ *
60
+ * ## Concurrent submissions
61
+ *
62
+ * The claim insert uses `INSERT ... ON CONFLICT (claim_key) DO NOTHING
63
+ * RETURNING claim_key` (the same portable idiom `_smrt_migrations` already
64
+ * uses in `system/bootstrap.ts`), not a plain `INSERT` caught for a
65
+ * classified `unique_violation`. This matters on PostgreSQL specifically: a
66
+ * statement that raises an error aborts the WHOLE transaction ("current
67
+ * transaction is aborted, commands ignored until end of transaction block"),
68
+ * so a plain `INSERT` failing on the claim key would make every subsequent
69
+ * statement in the same transaction — including the very `SELECT` this
70
+ * function needs to read the winner's stored result back — fail too.
71
+ * `DO NOTHING` never raises: it returns zero rows instead, which this
72
+ * function reads as "lost the race" without ever touching the transaction's
73
+ * error state. SQLite and DuckDB both support the same `ON CONFLICT ... DO
74
+ * NOTHING` / `RETURNING` syntax, so one query works on every adapter.
75
+ *
76
+ * `@happyvertical/sql`'s single-connection adapters (SQLite, DuckDB, JSON)
77
+ * serialize `transaction()` calls on one connection: a second, concurrent
78
+ * `runOnce()` call does not begin its own transaction until the first one
79
+ * ends. PostgreSQL pools, but this `INSERT`'s arbiter still has to check the
80
+ * unique index against another transaction's uncommitted row of the same
81
+ * PRIMARY KEY, and blocks until that transaction resolves, then either
82
+ * no-ops (committed — the row exists) or inserts (rolled back — no row).
83
+ * Either way, two concurrent callers with the same key never both run
84
+ * `work()`: exactly one does, and the loser's `INSERT` returns zero rows —
85
+ * never an error — at which point it reads the now-committed claim row back
86
+ * and replays its stored result.
87
+ *
88
+ * ## Typed answers for a claim that is not (yet) a definite result
89
+ *
90
+ * A `runOnce()` conflict is a *definite* decision — the row exists — but
91
+ * whether it is safe to hand back a result depends on `status`. This mirrors
92
+ * the "refused vs unknown outcome" distinction #2990 drew for AssistantDock
93
+ * actions (`./errors.ts#RunOnceClaimError` carries the two codes):
94
+ *
95
+ * - `status: 'completed'` — replay the stored `result`. No error.
96
+ * - `status` anything else (still `'in_progress'`) — some execution holds
97
+ * this key right now. `RunOnceClaimError.inFlight()`. Under this module's
98
+ * own transaction semantics this is normally unreachable through
99
+ * `runOnce()`-to-`runOnce()` races alone (the losing transaction can only
100
+ * ever observe a COMMITTED row, and this module only ever commits a row
101
+ * once it is `'completed'`) — it is reachable if a claim row was inserted
102
+ * outside this module's own completion discipline, and is kept as a
103
+ * defined answer rather than an assumption.
104
+ * - the row cannot be read back at all after `DO NOTHING` reported zero
105
+ * rows inserted — `RunOnceClaimError.outcomeUnknown()`. Also normally
106
+ * unreachable (the zero-rows case only happens when a committed row
107
+ * already exists); kept for adapters/drivers where that and a subsequent
108
+ * read are not guaranteed to observe the same committed state.
109
+ *
110
+ * Both errors carry `claimKey` and are safe to retry with the SAME token and
111
+ * content; `runOnce()` never retries them itself.
112
+ *
113
+ * @module
114
+ */
115
+ /** Key tagging a normalized `FormData` inside the FormData hash domain. */
116
+ var FORM_DATA_TAG = "\0FormData";
117
+ /**
118
+ * Prefix of the hash input for content containing a `FormData`. Sorted-key
119
+ * JSON always starts with a JSON token, never `\u0001`, so FormData digests
120
+ * can never equal a plain-JSON digest, and plain-JSON digests stay exactly
121
+ * what #3080 stored.
122
+ */
123
+ var FORM_DATA_DOMAIN = "run-once-formdata-v1\n";
124
+ /** A captured `FormData`: its ordered entries, already normalized. */
125
+ var NormalizedFormData = class {
126
+ entries;
127
+ constructor(entries) {
128
+ this.entries = entries;
129
+ }
130
+ };
131
+ /** Marks a value JSON omits from an object (and writes as `null` in an array). */
132
+ var OMIT = Symbol("run-once-omit");
133
+ /**
134
+ * Reduce `content` to plain JSON data before hashing, following JSON
135
+ * serialization exactly — `toJSON(key)` is called with the property name or
136
+ * array index (so a `Date` digests as its ISO string), and `undefined`,
137
+ * function and symbol values are omitted from objects and written as `null` in
138
+ * arrays — with two differences:
139
+ *
140
+ * - A native `FormData` has no own enumerable properties, so JSON reduces
141
+ * every form to `{}` and would collapse distinct submissions (#3136). It
142
+ * becomes its ordered entry list instead — repeated fields and their order
143
+ * are significant — and a `File`/`Blob` entry digests by name, type and
144
+ * size, never its bytes.
145
+ * - Values JSON would silently misrepresent (`Map`, `Set`, `RegExp`, any other
146
+ * non-plain object without `toJSON()`) or cannot serialize (`bigint`) are
147
+ * rejected, not hashed as `{}`.
148
+ */
149
+ function normalizeRunOnceContent(input, state, key) {
150
+ let value = input;
151
+ if (value !== null && (typeof value === "object" || typeof value === "bigint") && typeof value.toJSON === "function") value = value.toJSON(key);
152
+ if (value === null) return null;
153
+ switch (typeof value) {
154
+ case "undefined":
155
+ case "function":
156
+ case "symbol": return OMIT;
157
+ case "string":
158
+ case "boolean": return value;
159
+ case "number": return Number.isFinite(value) ? value : null;
160
+ case "bigint": throw new TypeError("runOnce content cannot contain a bigint; pass plain JSON data");
161
+ }
162
+ if (typeof FormData !== "undefined" && value instanceof FormData) {
163
+ state.sawFormData = true;
164
+ const entries = [];
165
+ value.forEach((entry, name) => {
166
+ entries.push([name, normalizeRunOnceContent(entry, state, name)]);
167
+ });
168
+ return new NormalizedFormData(entries);
169
+ }
170
+ if (typeof Blob !== "undefined" && value instanceof Blob) return {
171
+ name: value.name ?? null,
172
+ size: value.size,
173
+ type: value.type
174
+ };
175
+ if (Array.isArray(value)) return value.map((item, index) => {
176
+ const normalized = normalizeRunOnceContent(item, state, String(index));
177
+ return normalized === OMIT ? null : normalized;
178
+ });
179
+ const prototype = Object.getPrototypeOf(value);
180
+ if (prototype !== Object.prototype && prototype !== null) {
181
+ const name = value.constructor?.name || "non-plain object";
182
+ throw new TypeError(`runOnce content cannot contain a ${name}; convert it to plain JSON data`);
183
+ }
184
+ const result = Object.create(null);
185
+ for (const [member, memberValue] of Object.entries(value)) {
186
+ const normalized = normalizeRunOnceContent(memberValue, state, member);
187
+ if (normalized !== OMIT) result[member] = normalized;
188
+ }
189
+ return result;
190
+ }
191
+ /**
192
+ * Map a normalized tree containing {@link NormalizedFormData} into the
193
+ * FormData hash domain: each form becomes `{ [FORM_DATA_TAG]: entries }`, and
194
+ * user keys starting with NUL are escaped (NUL doubled) so no plain object can
195
+ * forge the tag. Works on the captured tree, so no getter or `toJSON()` on the
196
+ * caller's content runs twice.
197
+ */
198
+ function toFormDataDomain(value) {
199
+ if (value instanceof NormalizedFormData) return { [FORM_DATA_TAG]: value.entries.map(([key, entry]) => [key, toFormDataDomain(entry)]) };
200
+ if (Array.isArray(value)) return value.map(toFormDataDomain);
201
+ if (value !== null && typeof value === "object") {
202
+ const result = Object.create(null);
203
+ for (const [key, member] of Object.entries(value)) result[key.startsWith("\0") ? `\u0000${key}` : key] = toFormDataDomain(member);
204
+ return result;
205
+ }
206
+ return value;
207
+ }
208
+ /**
209
+ * Deterministic content digest: sha256 of sorted-key JSON.
210
+ *
211
+ * Property order never changes the digest, so callers do not need to worry
212
+ * about object construction order producing two different claims for what is
213
+ * semantically the same submission. Content is normalized exactly once (see
214
+ * {@link normalizeRunOnceContent}): `toJSON()` values such as `Date` digest as
215
+ * what they serialize to, a `FormData` digests as its ordered entries in a
216
+ * separate hash domain, and values JSON would reduce to `{}` (`Map`, `Set`,
217
+ * `RegExp`, …) are rejected.
218
+ *
219
+ * @throws {TypeError} when `content` contains a value with no faithful JSON form.
220
+ */
221
+ function digestRunOnceContent(content) {
222
+ const state = { sawFormData: false };
223
+ const captured = normalizeRunOnceContent(content, state, "");
224
+ const normalized = captured === OMIT ? null : captured;
225
+ const input = state.sawFormData ? FORM_DATA_DOMAIN + stableStringify(toFormDataDomain(normalized)) : stableStringify(normalized);
226
+ return createHash("sha256").update(input).digest("hex");
227
+ }
228
+ /**
229
+ * Derive the {@link runOnce} claim key.
230
+ *
231
+ * NOT the token alone — see the module doc for why that collapses distinct
232
+ * submissions. Folds in `tenantId`, `actor`, the raw `token`, and the
233
+ * content digest, all through sorted-key JSON before hashing.
234
+ */
235
+ function deriveRunOnceClaimKey(params) {
236
+ const canonical = stableStringify([
237
+ params.tenantId,
238
+ params.actor,
239
+ params.token,
240
+ params.contentDigest
241
+ ]);
242
+ return createHash("sha256").update(canonical).digest("hex");
243
+ }
244
+ /**
245
+ * Resolve an existing claim row into either a replayable result or a typed
246
+ * error. Pulled out of {@link runOnce} as a pure function so both branches
247
+ * (`completed` vs. anything else vs. unreadable) are directly unit-testable
248
+ * without forcing a genuine database race.
249
+ */
250
+ function resolveExistingRunOnceClaim(existing, claimKey) {
251
+ if (!existing) throw RunOnceClaimError.outcomeUnknown(claimKey);
252
+ if (existing.status !== "completed") throw RunOnceClaimError.inFlight(claimKey);
253
+ return JSON.parse(existing.result ?? "null");
254
+ }
255
+ /**
256
+ * Run `work` exactly once per submission.
257
+ *
258
+ * ```ts
259
+ * const purchaseOrderId = await runOnce(
260
+ * { db, tenantId, actor, token, content: formData },
261
+ * async (tx) => createPurchaseOrder(tx, formData),
262
+ * );
263
+ * ```
264
+ *
265
+ * Resolves to `work`'s return value on both a fresh run and a replay of a
266
+ * prior completed run — the caller cannot tell the difference from the
267
+ * return value alone (by design: both are "your submission is durably
268
+ * applied"). Rejects with `RunOnceClaimError` (`./errors.ts`) when the claim
269
+ * cannot be resolved to a definite result right now; see the module doc for
270
+ * the `RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN` distinction. Any
271
+ * other rejection (including one from `work` itself) propagates unchanged,
272
+ * and the claim is rolled back with the transaction so a retry can run again.
273
+ */
274
+ async function runOnce(params, work) {
275
+ const { db, tenantId, actor, token, content } = params;
276
+ if (!db.transaction) throw new Error("runOnce: the supplied database does not support transaction()");
277
+ const contentDigest = digestRunOnceContent(content);
278
+ const claimKey = deriveRunOnceClaimKey({
279
+ tenantId,
280
+ actor,
281
+ token,
282
+ contentDigest
283
+ });
284
+ return db.transaction(async (tx) => {
285
+ if (!await tx.single`
286
+ INSERT INTO _smrt_run_once_claims
287
+ (claim_key, tenant_id, actor, content_digest, status, result, created_at)
288
+ VALUES
289
+ (${claimKey}, ${tenantId}, ${actor}, ${contentDigest}, ${"in_progress"}, ${null}, ${/* @__PURE__ */ new Date()})
290
+ ON CONFLICT (claim_key) DO NOTHING
291
+ RETURNING claim_key
292
+ `) return resolveExistingRunOnceClaim(await tx.get(RUN_ONCE_CLAIMS_TABLE, {
293
+ claim_key: claimKey,
294
+ tenant_id: tenantId
295
+ }), claimKey);
296
+ const result = await work(tx);
297
+ await tx.update(RUN_ONCE_CLAIMS_TABLE, {
298
+ claim_key: claimKey,
299
+ tenant_id: tenantId
300
+ }, {
301
+ status: "completed",
302
+ result: JSON.stringify(result ?? null),
303
+ completed_at: /* @__PURE__ */ new Date()
304
+ });
305
+ return result;
306
+ });
307
+ }
308
+ //#endregion
309
+ export { RUN_ONCE_CLAIMS_TABLE, deriveRunOnceClaimKey, digestRunOnceContent, resolveExistingRunOnceClaim, runOnce };
310
+
311
+ //# sourceMappingURL=run-once.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-once.js","names":[],"sources":["../src/run-once.ts"],"sourcesContent":["/**\n * Idempotency seam: run an action exactly once per submission (#3080).\n *\n * Every consumer with a form, a retried request, or a double-tapped button\n * needs the same guard: a claim row, insert-only against a UNIQUE column\n * (`claim_key`) in the SAME transaction as the work it guards, so a retry\n * either runs the work once or replays the first attempt's stored result.\n * This module is that primitive. It is DB/transaction-level — it takes a\n * `DatabaseInterface`, not a `SmrtObject`/`SmrtCollection` — so it works\n * outside SvelteKit and outside the ORM: any caller with a database handle\n * and a JSON-serializable work result can use it. The claim insert itself\n * is raw SQL (`## Concurrent submissions` below explains why), not the\n * ORM's `SmrtObject`-level `_insertOnly` option.\n *\n * ## Key derivation (not the token alone)\n *\n * The claim key is the sha256 hex digest of the sorted-key-JSON encoding of\n * `[tenantId, actor, token, contentDigest]`, where `contentDigest` is itself\n * the sha256 hex digest of the sorted-key-JSON encoding of `content`\n * ({@link digestRunOnceContent}). Sorted-key JSON ({@link stableStringify},\n * `./knowledge-graph.ts`) makes the digest stable regardless of property\n * insertion order.\n *\n * The token alone is NOT the key. A first attempt at this exact problem\n * (reported against #3080) keyed on the token alone and silently collapsed\n * two genuinely different submissions that happened to reuse one form's\n * token. Folding in the content digest means a retry of the SAME submission\n * (same token, same content) replays; a DIFFERENT submission through the\n * same token (edited content before a resubmit) runs as its own claim.\n * Folding in `tenantId` and `actor` means two tenants or two actors can never\n * collide on the same claim even if a token were somehow shared between them.\n *\n * Neither the raw token nor the raw content is ever persisted — only their\n * digests — so the claims table (`_smrt_run_once_claims`,\n * `./system/schema.ts`) cannot leak submitted business data.\n *\n * ## Atomicity\n *\n * `runOnce()` opens its own `db.transaction()` (or nests under the caller's,\n * per `@happyvertical/sql`'s own SAVEPOINT/`NestedTransactionError` rules —\n * see that package's `DatabaseInterface.transaction` doc) and, inside it:\n *\n * 1. Inserts the claim row (`status: 'in_progress'`).\n * 2. Runs `work(tx)`, passing the SAME transaction-bound handle, so the\n * caller's writes and the claim insert commit or roll back together.\n * 3. Updates the claim row to `status: 'completed'` with the JSON-serialized\n * result, still inside the same transaction.\n *\n * If `work()` throws, the callback throws, and `db.transaction()`'s own\n * \"commits on success, rolls back on error\" contract undoes the claim insert\n * along with whatever `work()` partially wrote — a claim is never stranded\n * `'in_progress'` by a failed attempt, and a retry with the same key can\n * always run again.\n *\n * ## Concurrent submissions\n *\n * The claim insert uses `INSERT ... ON CONFLICT (claim_key) DO NOTHING\n * RETURNING claim_key` (the same portable idiom `_smrt_migrations` already\n * uses in `system/bootstrap.ts`), not a plain `INSERT` caught for a\n * classified `unique_violation`. This matters on PostgreSQL specifically: a\n * statement that raises an error aborts the WHOLE transaction (\"current\n * transaction is aborted, commands ignored until end of transaction block\"),\n * so a plain `INSERT` failing on the claim key would make every subsequent\n * statement in the same transaction — including the very `SELECT` this\n * function needs to read the winner's stored result back — fail too.\n * `DO NOTHING` never raises: it returns zero rows instead, which this\n * function reads as \"lost the race\" without ever touching the transaction's\n * error state. SQLite and DuckDB both support the same `ON CONFLICT ... DO\n * NOTHING` / `RETURNING` syntax, so one query works on every adapter.\n *\n * `@happyvertical/sql`'s single-connection adapters (SQLite, DuckDB, JSON)\n * serialize `transaction()` calls on one connection: a second, concurrent\n * `runOnce()` call does not begin its own transaction until the first one\n * ends. PostgreSQL pools, but this `INSERT`'s arbiter still has to check the\n * unique index against another transaction's uncommitted row of the same\n * PRIMARY KEY, and blocks until that transaction resolves, then either\n * no-ops (committed — the row exists) or inserts (rolled back — no row).\n * Either way, two concurrent callers with the same key never both run\n * `work()`: exactly one does, and the loser's `INSERT` returns zero rows —\n * never an error — at which point it reads the now-committed claim row back\n * and replays its stored result.\n *\n * ## Typed answers for a claim that is not (yet) a definite result\n *\n * A `runOnce()` conflict is a *definite* decision — the row exists — but\n * whether it is safe to hand back a result depends on `status`. This mirrors\n * the \"refused vs unknown outcome\" distinction #2990 drew for AssistantDock\n * actions (`./errors.ts#RunOnceClaimError` carries the two codes):\n *\n * - `status: 'completed'` — replay the stored `result`. No error.\n * - `status` anything else (still `'in_progress'`) — some execution holds\n * this key right now. `RunOnceClaimError.inFlight()`. Under this module's\n * own transaction semantics this is normally unreachable through\n * `runOnce()`-to-`runOnce()` races alone (the losing transaction can only\n * ever observe a COMMITTED row, and this module only ever commits a row\n * once it is `'completed'`) — it is reachable if a claim row was inserted\n * outside this module's own completion discipline, and is kept as a\n * defined answer rather than an assumption.\n * - the row cannot be read back at all after `DO NOTHING` reported zero\n * rows inserted — `RunOnceClaimError.outcomeUnknown()`. Also normally\n * unreachable (the zero-rows case only happens when a committed row\n * already exists); kept for adapters/drivers where that and a subsequent\n * read are not guaranteed to observe the same committed state.\n *\n * Both errors carry `claimKey` and are safe to retry with the SAME token and\n * content; `runOnce()` never retries them itself.\n *\n * @module\n */\n\nimport { createHash } from 'node:crypto';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { RunOnceClaimError } from './errors.js';\nimport { stableStringify } from './knowledge-graph.js';\nimport { RUN_ONCE_CLAIMS_TABLE } from './system/schema.js';\n\nexport { RUN_ONCE_CLAIMS_TABLE };\n\n/** Input identifying one submission for {@link runOnce}. */\nexport interface RunOnceParams {\n /** Transaction-capable database handle. May itself already be inside a transaction. */\n db: DatabaseInterface;\n /** Tenant scope. Part of the claim key: claims never cross tenants. */\n tenantId: string;\n /** The submitting principal. Part of the claim key: claims never cross actors. */\n actor: string;\n /** The per-form/per-submission token minted by the caller (e.g. a hidden form field). */\n token: string;\n /**\n * The content being written: plain JSON data, or a `FormData` (digested by\n * its ordered entries; see {@link digestRunOnceContent}). Only its\n * digest is persisted — never the raw value.\n */\n content: unknown;\n}\n\ninterface RunOnceClaimRow {\n claim_key: string;\n status: string;\n result: string | null;\n}\n\n/** Key tagging a normalized `FormData` inside the FormData hash domain. */\nconst FORM_DATA_TAG = '\\u0000FormData';\n\n/**\n * Prefix of the hash input for content containing a `FormData`. Sorted-key\n * JSON always starts with a JSON token, never `\\u0001`, so FormData digests\n * can never equal a plain-JSON digest, and plain-JSON digests stay exactly\n * what #3080 stored.\n */\nconst FORM_DATA_DOMAIN = '\\u0001run-once-formdata-v1\\n';\n\n/** A captured `FormData`: its ordered entries, already normalized. */\nclass NormalizedFormData {\n constructor(readonly entries: Array<[string, unknown]>) {}\n}\n\ninterface NormalizeState {\n sawFormData: boolean;\n}\n\n/** Marks a value JSON omits from an object (and writes as `null` in an array). */\nconst OMIT: unique symbol = Symbol('run-once-omit');\n\n/**\n * Reduce `content` to plain JSON data before hashing, following JSON\n * serialization exactly — `toJSON(key)` is called with the property name or\n * array index (so a `Date` digests as its ISO string), and `undefined`,\n * function and symbol values are omitted from objects and written as `null` in\n * arrays — with two differences:\n *\n * - A native `FormData` has no own enumerable properties, so JSON reduces\n * every form to `{}` and would collapse distinct submissions (#3136). It\n * becomes its ordered entry list instead — repeated fields and their order\n * are significant — and a `File`/`Blob` entry digests by name, type and\n * size, never its bytes.\n * - Values JSON would silently misrepresent (`Map`, `Set`, `RegExp`, any other\n * non-plain object without `toJSON()`) or cannot serialize (`bigint`) are\n * rejected, not hashed as `{}`.\n */\nfunction normalizeRunOnceContent(\n input: unknown,\n state: NormalizeState,\n key: string,\n): unknown {\n let value = input;\n if (\n value !== null &&\n (typeof value === 'object' || typeof value === 'bigint') &&\n typeof (value as { toJSON?: unknown }).toJSON === 'function'\n ) {\n value = (value as { toJSON: (key: string) => unknown }).toJSON(key);\n }\n if (value === null) return null;\n switch (typeof value) {\n case 'undefined':\n case 'function':\n case 'symbol':\n return OMIT;\n case 'string':\n case 'boolean':\n return value;\n case 'number':\n return Number.isFinite(value) ? value : null;\n case 'bigint':\n throw new TypeError(\n 'runOnce content cannot contain a bigint; pass plain JSON data',\n );\n }\n if (typeof FormData !== 'undefined' && value instanceof FormData) {\n state.sawFormData = true;\n const entries: Array<[string, unknown]> = [];\n // `forEach`, not `entries()`: consumers type-check core's source against\n // DOM libs without `DOM.Iterable`, where FormData is not iterable.\n value.forEach((entry, name) => {\n entries.push([name, normalizeRunOnceContent(entry, state, name)]);\n });\n return new NormalizedFormData(entries);\n }\n if (typeof Blob !== 'undefined' && value instanceof Blob) {\n return {\n name: (value as Blob & { name?: string }).name ?? null,\n size: value.size,\n type: value.type,\n };\n }\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n const normalized = normalizeRunOnceContent(item, state, String(index));\n return normalized === OMIT ? null : normalized;\n });\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n const name =\n (value as { constructor?: { name?: string } }).constructor?.name ||\n 'non-plain object';\n throw new TypeError(\n `runOnce content cannot contain a ${name}; convert it to plain JSON data`,\n );\n }\n const result: Record<string, unknown> = Object.create(null);\n for (const [member, memberValue] of Object.entries(value as object)) {\n const normalized = normalizeRunOnceContent(memberValue, state, member);\n if (normalized !== OMIT) result[member] = normalized;\n }\n return result;\n}\n\n/**\n * Map a normalized tree containing {@link NormalizedFormData} into the\n * FormData hash domain: each form becomes `{ [FORM_DATA_TAG]: entries }`, and\n * user keys starting with NUL are escaped (NUL doubled) so no plain object can\n * forge the tag. Works on the captured tree, so no getter or `toJSON()` on the\n * caller's content runs twice.\n */\nfunction toFormDataDomain(value: unknown): unknown {\n if (value instanceof NormalizedFormData) {\n return {\n [FORM_DATA_TAG]: value.entries.map(([key, entry]) => [\n key,\n toFormDataDomain(entry),\n ]),\n };\n }\n if (Array.isArray(value)) return value.map(toFormDataDomain);\n if (value !== null && typeof value === 'object') {\n const result: Record<string, unknown> = Object.create(null);\n for (const [key, member] of Object.entries(value)) {\n result[key.startsWith('\\u0000') ? `\\u0000${key}` : key] =\n toFormDataDomain(member);\n }\n return result;\n }\n return value;\n}\n\n/**\n * Deterministic content digest: sha256 of sorted-key JSON.\n *\n * Property order never changes the digest, so callers do not need to worry\n * about object construction order producing two different claims for what is\n * semantically the same submission. Content is normalized exactly once (see\n * {@link normalizeRunOnceContent}): `toJSON()` values such as `Date` digest as\n * what they serialize to, a `FormData` digests as its ordered entries in a\n * separate hash domain, and values JSON would reduce to `{}` (`Map`, `Set`,\n * `RegExp`, …) are rejected.\n *\n * @throws {TypeError} when `content` contains a value with no faithful JSON form.\n */\nexport function digestRunOnceContent(content: unknown): string {\n const state: NormalizeState = { sawFormData: false };\n const captured = normalizeRunOnceContent(content, state, '');\n // JSON.stringify of a top-level omitted value is `undefined`; #3080 hashed it as null.\n const normalized = captured === OMIT ? null : captured;\n const input = state.sawFormData\n ? FORM_DATA_DOMAIN + stableStringify(toFormDataDomain(normalized))\n : stableStringify(normalized);\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Derive the {@link runOnce} claim key.\n *\n * NOT the token alone — see the module doc for why that collapses distinct\n * submissions. Folds in `tenantId`, `actor`, the raw `token`, and the\n * content digest, all through sorted-key JSON before hashing.\n */\nexport function deriveRunOnceClaimKey(params: {\n tenantId: string;\n actor: string;\n token: string;\n contentDigest: string;\n}): string {\n const canonical = stableStringify([\n params.tenantId,\n params.actor,\n params.token,\n params.contentDigest,\n ]);\n return createHash('sha256').update(canonical).digest('hex');\n}\n\n/**\n * Resolve an existing claim row into either a replayable result or a typed\n * error. Pulled out of {@link runOnce} as a pure function so both branches\n * (`completed` vs. anything else vs. unreadable) are directly unit-testable\n * without forcing a genuine database race.\n */\nexport function resolveExistingRunOnceClaim<T>(\n existing: RunOnceClaimRow | null,\n claimKey: string,\n): T {\n if (!existing) {\n throw RunOnceClaimError.outcomeUnknown(claimKey);\n }\n if (existing.status !== 'completed') {\n throw RunOnceClaimError.inFlight(claimKey);\n }\n return JSON.parse(existing.result ?? 'null') as T;\n}\n\n/**\n * Run `work` exactly once per submission.\n *\n * ```ts\n * const purchaseOrderId = await runOnce(\n * { db, tenantId, actor, token, content: formData },\n * async (tx) => createPurchaseOrder(tx, formData),\n * );\n * ```\n *\n * Resolves to `work`'s return value on both a fresh run and a replay of a\n * prior completed run — the caller cannot tell the difference from the\n * return value alone (by design: both are \"your submission is durably\n * applied\"). Rejects with `RunOnceClaimError` (`./errors.ts`) when the claim\n * cannot be resolved to a definite result right now; see the module doc for\n * the `RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN` distinction. Any\n * other rejection (including one from `work` itself) propagates unchanged,\n * and the claim is rolled back with the transaction so a retry can run again.\n */\nexport async function runOnce<T>(\n params: RunOnceParams,\n work: (tx: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n const { db, tenantId, actor, token, content } = params;\n if (!db.transaction) {\n throw new Error(\n 'runOnce: the supplied database does not support transaction()',\n );\n }\n\n const contentDigest = digestRunOnceContent(content);\n const claimKey = deriveRunOnceClaimKey({\n tenantId,\n actor,\n token,\n contentDigest,\n });\n\n return db.transaction(async (tx) => {\n // `ON CONFLICT ... DO NOTHING` never raises — see the module doc for why\n // a plain INSERT-and-catch aborts the whole transaction on PostgreSQL,\n // taking the recovery SELECT below down with it. `RETURNING claim_key`\n // comes back with one row when this call won the claim, zero when it\n // lost it — that is the only signal this function needs.\n const won = await tx.single`\n INSERT INTO _smrt_run_once_claims\n (claim_key, tenant_id, actor, content_digest, status, result, created_at)\n VALUES\n (${claimKey}, ${tenantId}, ${actor}, ${contentDigest}, ${'in_progress'}, ${null}, ${new Date()})\n ON CONFLICT (claim_key) DO NOTHING\n RETURNING claim_key\n `;\n\n if (!won) {\n const existing = (await tx.get(RUN_ONCE_CLAIMS_TABLE, {\n claim_key: claimKey,\n tenant_id: tenantId,\n })) as RunOnceClaimRow | null;\n return resolveExistingRunOnceClaim<T>(existing, claimKey);\n }\n\n const result = await work(tx);\n await tx.update(\n RUN_ONCE_CLAIMS_TABLE,\n { claim_key: claimKey, tenant_id: tenantId },\n {\n status: 'completed',\n result: JSON.stringify(result ?? null),\n completed_at: new Date(),\n },\n );\n return result;\n });\n}\n"],"mappings":"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"}