okengine 0.23.0 → 0.23.2
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/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +4 -4
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/signal/broadcast.mdx +4 -2
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/errors.mdx +31 -19
- package/site/content/docs/reference/fx.mdx +12 -4
- package/src/auth/tenants.ts +17 -0
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/compiler/aot.test.ts +3 -2
- package/src/compiler/aot.ts +45 -1
- package/src/compiler/dynamic.ts +1 -1
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +87 -16
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +11 -1
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/http-parse.test.ts +101 -0
- package/src/compiler/http-parse.ts +188 -9
- package/src/compiler/interpret.ts +25 -2
- package/src/console/index.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-DDKkhT9v.js → access-page-zBMhZnFm.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-62Xts8Xi.js → agent-disclosure-CBj9f7Ju.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-DyeoKHKA.js → cache-glyph-D3MY49h3.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DvfpLXsU.js → call-pii-button-L9JxdBhp.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BY03SeCg.js → collapsible-BvFaX-Zt.js} +1 -1
- package/src/console/ui-next/dist/assets/{decisions-page-CgkKlA56.js → decisions-page-DgB76m-X.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-Y6HLjyJ5.js → duration-tone-l1DSJ2Vz.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DLPfNy-_.js → flows-page-Bb9t1lu6.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-D7nNzpgB.js → highlighted-json-DMvsi3Ti.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DdL19zzh.js → http-method-BwhJBTcQ.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C0lc_s9d.js → index-RhV2jT_7.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CRlcyLCC.js → observability-page-BRfUILBq.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-Bb3FWUu9.js → replica-lag-BsceAzIG.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DYBMWhX8.js → request-meta-C6lTyCXp.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CcE-SXC8.js → store-page-YmE806bJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CKMSEQ4s.js → trace-detail-sheet-Di4irS49.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-QOfk0M0H.js → tree-expand-toggle-DYeL6Rye.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DKC1CDDd.js → units-page-07agasRA.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-B54v4Yit.js → vault-page-CHBck4n_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/seed-parked-approval.ts +5 -1
- package/src/drivers/journal-postgres.test.ts +106 -2
- package/src/drivers/journal-postgres.ts +475 -14
- package/src/drivers/postgres.test.ts +60 -0
- package/src/drivers/postgres.ts +79 -5
- package/src/drivers/signal-compete.test.ts +238 -0
- package/src/drivers/signal-postgres.test.ts +170 -0
- package/src/drivers/signal-postgres.ts +486 -213
- package/src/drivers/signal-redis.test.ts +224 -0
- package/src/drivers/signal-redis.ts +934 -93
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/approval.test.ts +15 -3
- package/src/elements/ai/approval.ts +15 -9
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/runtime.ts +20 -19
- package/src/elements/channel/runtime.ts +42 -4
- package/src/elements/channel/sql-ledger.test.ts +404 -0
- package/src/elements/channel/sql-ledger.ts +395 -0
- package/src/elements/clock/chaos-child.ts +12 -2
- package/src/elements/clock/durable.ts +11 -0
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache-bus.test.ts +121 -0
- package/src/elements/store/cache-bus.ts +278 -0
- package/src/elements/store/cache.test.ts +179 -15
- package/src/elements/store/cache.ts +357 -51
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/runtime.ts +44 -2
- package/src/elements/store/sql-errors.test.ts +131 -0
- package/src/elements/store/sql-errors.ts +39 -16
- package/src/elements/store/sql-nested-tx.test.ts +344 -0
- package/src/elements/store/sql-session.test.ts +2 -0
- package/src/elements/store/sql-session.ts +275 -9
- package/src/elements/store.ts +1 -0
- package/src/kernel/app.ts +207 -43
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/auto-registry.test.ts +5 -1
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +72 -25
- package/src/kernel/boot-bind/store.ts +16 -1
- package/src/kernel/boot.test.ts +2 -2
- package/src/kernel/boot.ts +54 -10
- package/src/kernel/boundary-contract.ts +7 -0
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/call.test.ts +1 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +16 -0
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +11 -7
- package/src/kernel/fx-sql-handle.ts +52 -7
- package/src/kernel/fx.test.ts +1 -3
- package/src/kernel/fx.ts +27 -16
- package/src/kernel/horizontal-child.ts +54 -1
- package/src/kernel/horizontal.integration.test.ts +131 -7
- package/src/kernel/idempotency.test.ts +4 -4
- package/src/kernel/journal-boot.test.ts +83 -6
- package/src/kernel/journal.test.ts +158 -0
- package/src/kernel/journal.ts +515 -54
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/mcp/authorization.ts +5 -11
- package/src/mcp/confirmation.ts +203 -38
- package/src/mcp/docs-server.ts +26 -4
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp.test.ts +404 -33
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +31 -5
- package/src/mcp/session.ts +7 -0
- package/src/mcp/tools.ts +68 -19
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/runtime/bun.ts +5 -0
- package/src/runtime/types.ts +5 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { DomainDdlMode } from "../../config/index.ts";
|
|
9
|
+
import type { SignalEmitOptions, SignalTransaction } from "../../drivers/signal-types.ts";
|
|
9
10
|
import type { ClassificationMap, SqlConnection, SqlRow } from "../../drivers/types.ts";
|
|
10
11
|
import {
|
|
11
12
|
buildRlsIdentityPreludeSql,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
import { throwOke } from "../../kernel/errors.ts";
|
|
17
18
|
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
18
19
|
import { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
20
|
+
import { currentSignalTransaction, runInSignalTransaction } from "../../kernel/signal-tx.ts";
|
|
19
21
|
import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
|
|
20
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
23
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
@@ -51,6 +53,24 @@ export type { WhereMap } from "./sql-condition.ts";
|
|
|
51
53
|
*/
|
|
52
54
|
export type InferSelectRow<T> = T extends { readonly $inferSelect: infer R } ? R : SqlRow;
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Insert row from a Drizzle table's `$inferInsert`, or {@link SqlRow}.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam T - Table handle or Drizzle table
|
|
60
|
+
*/
|
|
61
|
+
export type InferInsertRow<T> = T extends { readonly $inferInsert: infer R } ? R : SqlRow;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Equality map or a Drizzle SQL fragment (`queryChunks`).
|
|
65
|
+
* A bare `unknown` would swallow the row type.
|
|
66
|
+
*
|
|
67
|
+
* @typeParam TRow - Selected or inserted row
|
|
68
|
+
*/
|
|
69
|
+
export type SqlPredicate<TRow> =
|
|
70
|
+
| { readonly [K in keyof TRow]?: TRow[K] }
|
|
71
|
+
| { readonly queryChunks: readonly unknown[] }
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
54
74
|
/** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
|
|
55
75
|
const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
|
|
56
76
|
|
|
@@ -119,6 +139,29 @@ function notifySqlCdc(event: {
|
|
|
119
139
|
*/
|
|
120
140
|
const cdcMutationStorage = new AsyncLocalStorage<{ readonly mutationId: string }>();
|
|
121
141
|
|
|
142
|
+
/**
|
|
143
|
+
* One frame of {@link SqlStoreHandle.transaction}.
|
|
144
|
+
* Depth 0 is the driver `BEGIN`. Deeper frames are savepoints on that pin.
|
|
145
|
+
*/
|
|
146
|
+
interface SqlTxFrame {
|
|
147
|
+
readonly connection: SqlConnection;
|
|
148
|
+
/** 0 for the driver transaction; 1+ for `SAVEPOINT`. */
|
|
149
|
+
readonly depth: number;
|
|
150
|
+
/**
|
|
151
|
+
* Promise chain for direct child transactions on this pin.
|
|
152
|
+
* Siblings append here so their statements cannot interleave. A child
|
|
153
|
+
* waits on this chain, not on a connection-wide tail, so a nested call
|
|
154
|
+
* does not deadlock behind its own parent.
|
|
155
|
+
*/
|
|
156
|
+
childTail: Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Pinned connection for {@link SqlStoreHandle.transaction}. */
|
|
160
|
+
const sqlTxStorage = new AsyncLocalStorage<SqlTxFrame>();
|
|
161
|
+
|
|
162
|
+
/** Monotonic savepoint counter, keyed by the pinned connection. */
|
|
163
|
+
const savepointSeq = new WeakMap<SqlConnection, number>();
|
|
164
|
+
|
|
122
165
|
/**
|
|
123
166
|
* Read the ambient mutation id, or `undefined` outside a stamped request.
|
|
124
167
|
*/
|
|
@@ -193,6 +236,110 @@ async function withRlsStampLock<T>(connection: SqlConnection, fn: () => Promise<
|
|
|
193
236
|
}
|
|
194
237
|
}
|
|
195
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Next `oke_sp_<depth>_<n>` name. `n` is per pinned connection so siblings
|
|
241
|
+
* and nested savepoints never reuse a name, even after `ROLLBACK TO`.
|
|
242
|
+
*
|
|
243
|
+
* @param connection - Pinned connection
|
|
244
|
+
* @param depth - Savepoint depth (1 for the first nested call)
|
|
245
|
+
*/
|
|
246
|
+
function nextSavepointId(connection: SqlConnection, depth: number): string {
|
|
247
|
+
const n = (savepointSeq.get(connection) ?? 0) + 1;
|
|
248
|
+
savepointSeq.set(connection, n);
|
|
249
|
+
return `oke_sp_${String(depth)}_${String(n)}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Run `fn` after earlier direct children of `frame` finish.
|
|
254
|
+
*
|
|
255
|
+
* @param frame - Parent transaction frame
|
|
256
|
+
* @param fn - Child body, including savepoint control
|
|
257
|
+
*/
|
|
258
|
+
async function withSqlChildLock<T>(frame: SqlTxFrame, fn: () => Promise<T>): Promise<T> {
|
|
259
|
+
const prev = frame.childTail;
|
|
260
|
+
let release!: () => void;
|
|
261
|
+
const gate = new Promise<void>((resolve) => {
|
|
262
|
+
release = resolve;
|
|
263
|
+
});
|
|
264
|
+
frame.childTail = prev.then(
|
|
265
|
+
() => gate,
|
|
266
|
+
() => gate,
|
|
267
|
+
);
|
|
268
|
+
await prev.catch(() => undefined);
|
|
269
|
+
try {
|
|
270
|
+
return await fn();
|
|
271
|
+
} finally {
|
|
272
|
+
release();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** One staged signal op inside a nested transaction. */
|
|
277
|
+
type StagedSignalOp =
|
|
278
|
+
| { readonly kind: "write"; readonly key: string; readonly value: unknown }
|
|
279
|
+
| {
|
|
280
|
+
readonly kind: "emit";
|
|
281
|
+
readonly signal: string;
|
|
282
|
+
readonly payload: unknown;
|
|
283
|
+
readonly options?: SignalEmitOptions;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Child staging scope. Emits buffer here and merge into `parent` on success.
|
|
288
|
+
* Discard drops them. Neither path publishes — the outermost commit does.
|
|
289
|
+
*
|
|
290
|
+
* @param parent - Ambient signal transaction
|
|
291
|
+
*/
|
|
292
|
+
function openChildSignalScope(parent: SignalTransaction): {
|
|
293
|
+
readonly transaction: SignalTransaction;
|
|
294
|
+
readonly merge: () => Promise<void>;
|
|
295
|
+
readonly discard: () => void;
|
|
296
|
+
} {
|
|
297
|
+
const staged: StagedSignalOp[] = [];
|
|
298
|
+
let open = true;
|
|
299
|
+
|
|
300
|
+
function assertOpen(): void {
|
|
301
|
+
if (!open) throw new Error("transaction finished");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const transaction: SignalTransaction = {
|
|
305
|
+
async write(key, value) {
|
|
306
|
+
assertOpen();
|
|
307
|
+
staged.push({ kind: "write", key, value });
|
|
308
|
+
},
|
|
309
|
+
async emit(signal, payload, options) {
|
|
310
|
+
assertOpen();
|
|
311
|
+
staged.push(
|
|
312
|
+
options === undefined
|
|
313
|
+
? { kind: "emit", signal, payload }
|
|
314
|
+
: { kind: "emit", signal, payload, options },
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
async commit() {
|
|
318
|
+
throw new Error("nested signal scope publishes on the outermost commit");
|
|
319
|
+
},
|
|
320
|
+
async rollback() {
|
|
321
|
+
open = false;
|
|
322
|
+
staged.length = 0;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
transaction,
|
|
328
|
+
async merge() {
|
|
329
|
+
if (!open) return;
|
|
330
|
+
open = false;
|
|
331
|
+
for (const op of staged) {
|
|
332
|
+
if (op.kind === "write") await parent.write(op.key, op.value);
|
|
333
|
+
else await parent.emit(op.signal, op.payload, op.options);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
discard() {
|
|
337
|
+
open = false;
|
|
338
|
+
staged.length = 0;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
196
343
|
/** Options for a SQL session. */
|
|
197
344
|
export interface SqlSessionOptions {
|
|
198
345
|
/** Open connection (already role-routed). */
|
|
@@ -274,7 +421,7 @@ export interface SelectFromBuilder<TRow = SqlRow> extends PromiseLike<TRow[]> {
|
|
|
274
421
|
*
|
|
275
422
|
* @param where - Condition
|
|
276
423
|
*/
|
|
277
|
-
where(where:
|
|
424
|
+
where(where: SqlPredicate<TRow>): SelectWhereBuilder<TRow>;
|
|
278
425
|
/**
|
|
279
426
|
* Order rows with Drizzle `asc()` / `desc()` terms.
|
|
280
427
|
*
|
|
@@ -312,14 +459,18 @@ export interface SelectBuilder<TLocked extends SqlRow | undefined = undefined> {
|
|
|
312
459
|
): SelectFromBuilder<TLocked extends undefined ? InferSelectRow<TTable> : TLocked>;
|
|
313
460
|
}
|
|
314
461
|
|
|
315
|
-
/**
|
|
316
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Fluent insert builder.
|
|
464
|
+
*
|
|
465
|
+
* @typeParam TRow - `$inferInsert` when the table is a Drizzle table
|
|
466
|
+
*/
|
|
467
|
+
export interface InsertBuilder<TRow = SqlRow> {
|
|
317
468
|
/**
|
|
318
469
|
* Provide row values.
|
|
319
470
|
*
|
|
320
471
|
* @param row - Row to insert
|
|
321
472
|
*/
|
|
322
|
-
values(row:
|
|
473
|
+
values(row: TRow): InsertValuesBuilder;
|
|
323
474
|
}
|
|
324
475
|
|
|
325
476
|
/**
|
|
@@ -340,7 +491,7 @@ export interface DeleteBuilder {
|
|
|
340
491
|
*
|
|
341
492
|
* @param where - Equality map or Drizzle SQL
|
|
342
493
|
*/
|
|
343
|
-
where(where:
|
|
494
|
+
where(where: SqlPredicate<SqlRow>): Promise<number>;
|
|
344
495
|
}
|
|
345
496
|
|
|
346
497
|
/** Fluent update builder. */
|
|
@@ -360,7 +511,7 @@ export interface UpdateSetBuilder {
|
|
|
360
511
|
*
|
|
361
512
|
* @param where - Equality map or Drizzle SQL
|
|
362
513
|
*/
|
|
363
|
-
where(where:
|
|
514
|
+
where(where: SqlPredicate<SqlRow>): Promise<number>;
|
|
364
515
|
}
|
|
365
516
|
|
|
366
517
|
/**
|
|
@@ -393,7 +544,7 @@ export interface SqlStoreHandle {
|
|
|
393
544
|
*
|
|
394
545
|
* @param table - Target table
|
|
395
546
|
*/
|
|
396
|
-
insert(table:
|
|
547
|
+
insert<TTable>(table: TTable): InsertBuilder<InferInsertRow<TTable>>;
|
|
397
548
|
/**
|
|
398
549
|
* Start an update on `table`.
|
|
399
550
|
*
|
|
@@ -415,6 +566,25 @@ export interface SqlStoreHandle {
|
|
|
415
566
|
*/
|
|
416
567
|
delete(table: TableHandle | unknown): DeleteBuilder;
|
|
417
568
|
delete(table: TableHandle | unknown, id: string): Promise<boolean>;
|
|
569
|
+
/**
|
|
570
|
+
* Run `fn` on one pinned connection. `fx.emit` inside `fn` stages on the
|
|
571
|
+
* signal outbox and publishes only after this SQL transaction commits.
|
|
572
|
+
* A call that already has a pinned connection opens `SAVEPOINT` on that
|
|
573
|
+
* connection. Nested emits stage on a child scope and merge into the
|
|
574
|
+
* parent on success; only the outermost commit publishes.
|
|
575
|
+
*
|
|
576
|
+
* @param fn - Transaction body. `tx` is this handle, bound to the pin.
|
|
577
|
+
*/
|
|
578
|
+
transaction<T>(fn: (tx: SqlStoreHandle) => Promise<T>): Promise<T>;
|
|
579
|
+
/**
|
|
580
|
+
* Execute a Drizzle select, insert, update, or delete builder.
|
|
581
|
+
* Calls `toSQL()` so `drizzle-orm` stays off the edge graph.
|
|
582
|
+
*
|
|
583
|
+
* @param query - Builder with `toSQL()`
|
|
584
|
+
*/
|
|
585
|
+
run<T extends SqlRow = SqlRow>(query: {
|
|
586
|
+
toSQL(): { readonly sql: string; readonly params: readonly unknown[] };
|
|
587
|
+
}): Promise<T[]>;
|
|
418
588
|
/**
|
|
419
589
|
* True when at least one row matches.
|
|
420
590
|
*
|
|
@@ -564,6 +734,10 @@ export function createSqlStoreHandle(
|
|
|
564
734
|
}
|
|
565
735
|
}
|
|
566
736
|
|
|
737
|
+
function activeConnection(): SqlConnection {
|
|
738
|
+
return sqlTxStorage.getStore()?.connection ?? connection;
|
|
739
|
+
}
|
|
740
|
+
|
|
567
741
|
function query(sql: string, params: readonly unknown[] = []): Promise<SqlRow[]> {
|
|
568
742
|
return withSchemaGuard(() => withRlsStamp((conn) => conn.query(sql, params), sql));
|
|
569
743
|
}
|
|
@@ -572,9 +746,29 @@ export function createSqlStoreHandle(
|
|
|
572
746
|
return withSchemaGuard(() => withRlsStamp((conn) => conn.exec(sql, params), sql));
|
|
573
747
|
}
|
|
574
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Rewrite Drizzle `$1` placeholders to the connection's `?` form.
|
|
751
|
+
*
|
|
752
|
+
* @param sql - Dialect SQL
|
|
753
|
+
* @param params - Bindings in `$n` order (1-based)
|
|
754
|
+
*/
|
|
755
|
+
function fromDialect(
|
|
756
|
+
sql: string,
|
|
757
|
+
params: readonly unknown[],
|
|
758
|
+
): { sql: string; params: unknown[] } {
|
|
759
|
+
if (!sql.includes("$")) return { sql, params: [...params] };
|
|
760
|
+
const ordered: unknown[] = [];
|
|
761
|
+
const text = sql.replace(/\$(\d+)/g, (_match, index: string) => {
|
|
762
|
+
ordered.push(params[Number(index) - 1]);
|
|
763
|
+
return "?";
|
|
764
|
+
});
|
|
765
|
+
return { sql: text, params: ordered };
|
|
766
|
+
}
|
|
767
|
+
|
|
575
768
|
async function withRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>, sql: string): Promise<T> {
|
|
576
|
-
if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(
|
|
577
|
-
if (isRlsStampExemptSql(sql)) return fn(
|
|
769
|
+
if (!rls || !RLS_CONTEXT_DRIVERS.has(connection.driverId)) return fn(activeConnection());
|
|
770
|
+
if (isRlsStampExemptSql(sql)) return fn(activeConnection());
|
|
771
|
+
if (sqlTxStorage.getStore()) return fn(activeConnection());
|
|
578
772
|
const run = (): Promise<T> => applyRlsStamp(fn);
|
|
579
773
|
// PGlite is one backend session — concurrent identities must not interleave.
|
|
580
774
|
// Pooled postgres pins each stamp via `transaction()` instead.
|
|
@@ -910,6 +1104,53 @@ export function createSqlStoreHandle(
|
|
|
910
1104
|
};
|
|
911
1105
|
}
|
|
912
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Nested `transaction()` on the pinned connection: `SAVEPOINT`, then
|
|
1109
|
+
* `RELEASE` or `ROLLBACK TO`. Sibling calls share `parent.childTail`.
|
|
1110
|
+
*
|
|
1111
|
+
* @param parent - Frame already on the async store
|
|
1112
|
+
* @param fn - Nested body
|
|
1113
|
+
*/
|
|
1114
|
+
async function runNestedTransaction<T>(
|
|
1115
|
+
parent: SqlTxFrame,
|
|
1116
|
+
fn: (tx: SqlStoreHandle) => Promise<T>,
|
|
1117
|
+
): Promise<T> {
|
|
1118
|
+
return withSqlChildLock(parent, async () => {
|
|
1119
|
+
const depth = parent.depth + 1;
|
|
1120
|
+
const name = quoteIdent(nextSavepointId(parent.connection, depth));
|
|
1121
|
+
const conn = parent.connection;
|
|
1122
|
+
await conn.exec(`SAVEPOINT ${name}`);
|
|
1123
|
+
const child: SqlTxFrame = {
|
|
1124
|
+
connection: conn,
|
|
1125
|
+
depth,
|
|
1126
|
+
childTail: Promise.resolve(),
|
|
1127
|
+
};
|
|
1128
|
+
const parentSignal = currentSignalTransaction();
|
|
1129
|
+
const staging = parentSignal ? openChildSignalScope(parentSignal) : undefined;
|
|
1130
|
+
let released = false;
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await sqlTxStorage.run(child, () => {
|
|
1133
|
+
const body = () => fn(handle as SqlStoreHandle);
|
|
1134
|
+
return staging ? runInSignalTransaction(staging.transaction, body) : body();
|
|
1135
|
+
});
|
|
1136
|
+
await conn.exec(`RELEASE SAVEPOINT ${name}`);
|
|
1137
|
+
released = true;
|
|
1138
|
+
if (staging) await staging.merge();
|
|
1139
|
+
return result;
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
if (!released) {
|
|
1142
|
+
try {
|
|
1143
|
+
await conn.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
1144
|
+
} catch {
|
|
1145
|
+
// The pin may already be aborted; the original error still wins.
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
staging?.discard();
|
|
1149
|
+
throw err;
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
913
1154
|
const handle = {
|
|
914
1155
|
ref,
|
|
915
1156
|
routedRole: options.routedRole,
|
|
@@ -1332,6 +1573,31 @@ export function createSqlStoreHandle(
|
|
|
1332
1573
|
});
|
|
1333
1574
|
},
|
|
1334
1575
|
|
|
1576
|
+
async transaction<T>(fn: (tx: SqlStoreHandle) => Promise<T>): Promise<T> {
|
|
1577
|
+
const current = sqlTxStorage.getStore();
|
|
1578
|
+
if (current) return runNestedTransaction(current, fn);
|
|
1579
|
+
if (!connection.transaction) {
|
|
1580
|
+
throw new Error("fx.store().transaction needs SqlConnection.transaction");
|
|
1581
|
+
}
|
|
1582
|
+
return connection.transaction(async (txConn) =>
|
|
1583
|
+
sqlTxStorage.run({ connection: txConn, depth: 0, childTail: Promise.resolve() }, () =>
|
|
1584
|
+
fn(handle as SqlStoreHandle),
|
|
1585
|
+
),
|
|
1586
|
+
);
|
|
1587
|
+
},
|
|
1588
|
+
|
|
1589
|
+
async run<T extends SqlRow = SqlRow>(builder: {
|
|
1590
|
+
toSQL(): { readonly sql: string; readonly params: readonly unknown[] };
|
|
1591
|
+
}): Promise<T[]> {
|
|
1592
|
+
const compiled = builder.toSQL();
|
|
1593
|
+
const dialect = fromDialect(compiled.sql, compiled.params);
|
|
1594
|
+
if (isSqlDml(dialect.sql)) {
|
|
1595
|
+
await exec(dialect.sql, dialect.params);
|
|
1596
|
+
return [];
|
|
1597
|
+
}
|
|
1598
|
+
return query(dialect.sql, dialect.params) as Promise<T[]>;
|
|
1599
|
+
},
|
|
1600
|
+
|
|
1335
1601
|
async ensureTable(table: TableHandle) {
|
|
1336
1602
|
const cols = Object.values(table.columns);
|
|
1337
1603
|
const pk = resolvePkColumn(table);
|