okengine 0.21.0 → 0.21.1
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 +2 -7
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/client/create.ts +13 -26
- package/src/client/live.ts +44 -101
- package/src/client/sse.ts +20 -66
- package/src/client/stream.ts +25 -67
- package/src/client/transport.ts +28 -73
- package/src/client/wire.ts +119 -0
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/interpret.ts +45 -0
- package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Bgt9bq2r.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CSKumwS2.js → agent-disclosure-B43CXZZR.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-92uM5MO7.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-DtGVPtcs.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-DN7l6zmC.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-CwoV56jn.js → duration-tone-BmIR9FV8.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-BMHs-IzK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-BlAEVgNW.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-BDf7OAHv.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-Cul17AcV.js → index-BKpaes3n.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-oY9vdYBk.js → observability-page-WnVLI-0j.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-B3GLNVfF.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-DMbnAe3f.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CL0D9dOq.js → store-page-KvFDingJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-1PlT19ft.js → units-page-C4NdNuxP.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-CBYl0LW_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/elements/store/sql-session.ts +20 -4
- package/src/kernel/app.ts +9 -2
- package/src/kernel/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +23 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors.ts +90 -138
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.ts +37 -329
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/release/build-lib.ts +9 -0
package/src/kernel/fx.ts
CHANGED
|
@@ -9,10 +9,16 @@
|
|
|
9
9
|
* for deterministic tests (§7.6).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
isJsonResult,
|
|
14
|
+
isJsonStreamResult,
|
|
15
|
+
jsonResultBrand,
|
|
16
|
+
type JsonResult,
|
|
17
|
+
type JsonStreamResult,
|
|
18
|
+
} from "./json-result.ts";
|
|
12
19
|
import type { Effects, ResourceRef, SignalResourceRef } from "../manifest/types.ts";
|
|
13
20
|
import { isMcpToolRef } from "../manifest/mcp-ref.ts";
|
|
14
21
|
import type { QueryPageSpec } from "./list-page.ts";
|
|
15
|
-
import { schemaTableName, sqlTableRef } from "../manifest/sql-resource.ts";
|
|
16
22
|
import type {
|
|
17
23
|
FilesStoreDecl,
|
|
18
24
|
FilesStoreFxHandle,
|
|
@@ -20,8 +26,6 @@ import type {
|
|
|
20
26
|
IndexStoreFxHandle,
|
|
21
27
|
KvStoreDecl,
|
|
22
28
|
KvStoreFxHandle,
|
|
23
|
-
SelectFromBuilder,
|
|
24
|
-
SelectOrderBuilder,
|
|
25
29
|
SqlStoreDecl,
|
|
26
30
|
StoreDecl,
|
|
27
31
|
StoreHandle,
|
|
@@ -30,7 +34,6 @@ import type {
|
|
|
30
34
|
} from "../elements/store.ts";
|
|
31
35
|
import { rlsIdentityFromAuth } from "../elements/store.ts";
|
|
32
36
|
import type { RlsIdentity } from "../drivers/pg-rls.ts";
|
|
33
|
-
import type { SqlRow } from "../drivers/types.ts";
|
|
34
37
|
import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
|
|
35
38
|
import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
36
39
|
import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
|
|
@@ -60,14 +63,7 @@ import {
|
|
|
60
63
|
} from "./dry-run.ts";
|
|
61
64
|
import type { FailFn } from "./errors.ts";
|
|
62
65
|
import { currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
63
|
-
import {
|
|
64
|
-
fxAll,
|
|
65
|
-
fxRace,
|
|
66
|
-
fxRetry,
|
|
67
|
-
fxUsing,
|
|
68
|
-
type FxRetryOptions,
|
|
69
|
-
type FxThunk,
|
|
70
|
-
} from "./concurrency.ts";
|
|
66
|
+
import type { FxRetryOptions, FxThunk } from "./concurrency.ts";
|
|
71
67
|
import { maskRedactedDeep, Redacted } from "./redacted.ts";
|
|
72
68
|
import type { JournalSession, JournalStepOptions } from "./journal.ts";
|
|
73
69
|
|
|
@@ -90,8 +86,23 @@ function loadFail(): FailFn {
|
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
/** Lazy runs/window helpers — kept off the cold `oke` static graph. */
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
function loadRunsWindow(): typeof import("../runs/window.ts") {
|
|
90
|
+
const stem = ["win", "dow"].join("");
|
|
91
|
+
try {
|
|
92
|
+
return lazyRequire(`${import.meta.dir}/../runs`, stem);
|
|
93
|
+
} catch {
|
|
94
|
+
return lazyRequire(import.meta.dir, stem);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Driver-backed SQL query builder — off the edge profile until `fx.store` opens SQL. */
|
|
99
|
+
function loadFxSql(): typeof import("./fx-sql-handle.ts") {
|
|
100
|
+
return lazyRequire(import.meta.dir, ["fx", "sql", "handle"].join("-"));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `fx.all` / `race` / `retry` / `using` — off the edge profile until called. */
|
|
104
|
+
function loadConcurrency(): typeof import("./concurrency.ts") {
|
|
105
|
+
return lazyRequire(import.meta.dir, ["concur", "rency"].join(""));
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
/**
|
|
@@ -390,29 +401,8 @@ export interface FxSearchOptions {
|
|
|
390
401
|
readonly topK?: number;
|
|
391
402
|
}
|
|
392
403
|
|
|
393
|
-
|
|
394
|
-
export
|
|
395
|
-
|
|
396
|
-
/** Carrier from {@link FxJson} — status + body read by the response encoder. */
|
|
397
|
-
export interface JsonResult<T = unknown> {
|
|
398
|
-
readonly [jsonResultBrand]: true;
|
|
399
|
-
readonly status: number;
|
|
400
|
-
readonly value?: T;
|
|
401
|
-
readonly meta?: Record<string, unknown>;
|
|
402
|
-
readonly kind?: undefined;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** SSE carrier from {@link FxJson.stream} / {@link Fx.live}. */
|
|
406
|
-
export interface JsonStreamResult {
|
|
407
|
-
readonly [jsonResultBrand]: true;
|
|
408
|
-
readonly kind: "stream";
|
|
409
|
-
readonly status: 200;
|
|
410
|
-
readonly chunks: AsyncIterable<unknown>;
|
|
411
|
-
/** Awaited before the 200 SSE body; throws OKE1210 on a missing resume cursor. */
|
|
412
|
-
ready?: () => Promise<void>;
|
|
413
|
-
/** Set by the kernel to commit journal / Runs after the stream settles. */
|
|
414
|
-
finalize?: () => Promise<void>;
|
|
415
|
-
}
|
|
404
|
+
export { isJsonResult, isJsonStreamResult, jsonResultBrand };
|
|
405
|
+
export type { JsonResult, JsonStreamResult };
|
|
416
406
|
|
|
417
407
|
const sseFrameBrand: unique symbol = Symbol.for("oke.sse.frame");
|
|
418
408
|
|
|
@@ -442,26 +432,6 @@ export function isSseFrame(value: unknown): value is SseFrame {
|
|
|
442
432
|
return typeof value === "object" && value !== null && (value as SseFrame)[sseFrameBrand] === true;
|
|
443
433
|
}
|
|
444
434
|
|
|
445
|
-
/** True when `value` is an {@link FxJson} JSON-envelope carrier. */
|
|
446
|
-
export function isJsonResult(value: unknown): value is JsonResult {
|
|
447
|
-
return (
|
|
448
|
-
typeof value === "object" &&
|
|
449
|
-
value !== null &&
|
|
450
|
-
(value as JsonResult)[jsonResultBrand] === true &&
|
|
451
|
-
(value as JsonStreamResult).kind !== "stream"
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/** True when `value` is an SSE stream carrier from {@link FxJson.stream}. */
|
|
456
|
-
export function isJsonStreamResult(value: unknown): value is JsonStreamResult {
|
|
457
|
-
return (
|
|
458
|
-
typeof value === "object" &&
|
|
459
|
-
value !== null &&
|
|
460
|
-
(value as JsonStreamResult)[jsonResultBrand] === true &&
|
|
461
|
-
(value as JsonStreamResult).kind === "stream"
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
435
|
/**
|
|
466
436
|
* Paginated JSON page — `data` is Flow `out`; `meta` is the HTTP pager.
|
|
467
437
|
*
|
|
@@ -1293,271 +1263,6 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1293
1263
|
};
|
|
1294
1264
|
}
|
|
1295
1265
|
|
|
1296
|
-
/**
|
|
1297
|
-
* Lazy, capability-gated proxy over a driver-backed {@link SqlStoreHandle}.
|
|
1298
|
-
*
|
|
1299
|
-
* @param decl - Store declaration
|
|
1300
|
-
* @param open - Opens (and caches) the runtime handle
|
|
1301
|
-
*/
|
|
1302
|
-
function gatedSqlHandle(decl: StoreDecl, open: () => Promise<SqlStoreHandle>): SqlStoreHandle {
|
|
1303
|
-
const ref = decl.ref as `sql:${string}`;
|
|
1304
|
-
let cached: SqlStoreHandle | undefined;
|
|
1305
|
-
const ensure = async (): Promise<SqlStoreHandle> => {
|
|
1306
|
-
if (!cached) cached = await open();
|
|
1307
|
-
return cached;
|
|
1308
|
-
};
|
|
1309
|
-
/** Driver-backed SQL has no dry-run transaction — refuse writes. */
|
|
1310
|
-
const refuseDryRunWrite = (): void => {
|
|
1311
|
-
if (isDryRun()) {
|
|
1312
|
-
throw new DryRunWriteIsolationError(
|
|
1313
|
-
`Driver-backed store "${ref}" cannot isolate writes during dry-run; dry-run refused rather than risk a double-write.`,
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
/**
|
|
1318
|
-
* Gate a table-scoped SQL operation. Prefers the precise `sql:<table>`
|
|
1319
|
-
* ref (matches what the compiler's AST inference derives from the same
|
|
1320
|
-
* call site — {@link "../manifest/sql-resource.ts"}); falls back to the
|
|
1321
|
-
* store-level ref when the table ref isn't declared — every flow that
|
|
1322
|
-
* hand-declared the older `effects: { writes: ["sql:<store>"] }`
|
|
1323
|
-
* convention (every existing template, `upsert-app.test.ts`, …) must
|
|
1324
|
-
* keep working unchanged. Ledger / journal record whichever ref the
|
|
1325
|
-
* capability check actually matched, not always the coarser one.
|
|
1326
|
-
*
|
|
1327
|
-
* @param kind - Effect kind
|
|
1328
|
-
* @param table - Table argument passed to a `SqlStoreHandle` method
|
|
1329
|
-
* @param body - Work to run under the gate
|
|
1330
|
-
*/
|
|
1331
|
-
const gatedTable = <T>(
|
|
1332
|
-
kind: Parameters<CapabilityToken["assert"]>[0],
|
|
1333
|
-
table: unknown,
|
|
1334
|
-
body: () => T | Promise<T>,
|
|
1335
|
-
): Promise<T> => {
|
|
1336
|
-
const externalOf = (): EffectExternal | undefined => cached?.external;
|
|
1337
|
-
const name = schemaTableName(table);
|
|
1338
|
-
if (name !== undefined) {
|
|
1339
|
-
const perTable = sqlTableRef(name);
|
|
1340
|
-
if (perTable !== ref && capability.allows(kind, perTable)) {
|
|
1341
|
-
return gated(kind, perTable, body, externalOf);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
return gated(kind, ref, body, externalOf);
|
|
1345
|
-
};
|
|
1346
|
-
|
|
1347
|
-
return {
|
|
1348
|
-
ref,
|
|
1349
|
-
get routedRole() {
|
|
1350
|
-
return cached?.routedRole ?? "primary";
|
|
1351
|
-
},
|
|
1352
|
-
get driverId() {
|
|
1353
|
-
return cached?.driverId ?? "memory";
|
|
1354
|
-
},
|
|
1355
|
-
select: ((columns?: unknown) => {
|
|
1356
|
-
return {
|
|
1357
|
-
from(table: unknown) {
|
|
1358
|
-
const run = (plan: {
|
|
1359
|
-
where?: unknown;
|
|
1360
|
-
orders?: readonly unknown[];
|
|
1361
|
-
limit?: number;
|
|
1362
|
-
offset?: number;
|
|
1363
|
-
}): Promise<SqlRow[]> =>
|
|
1364
|
-
gatedTable("read", table, async () => {
|
|
1365
|
-
const h = await ensure();
|
|
1366
|
-
const from = h.select(columns).from(table) as SelectFromBuilder;
|
|
1367
|
-
const filtered = plan.where === undefined ? from : from.where(plan.where);
|
|
1368
|
-
const ordered =
|
|
1369
|
-
plan.orders === undefined ? filtered : filtered.orderBy(...plan.orders);
|
|
1370
|
-
if (plan.offset !== undefined) return ordered.offset(plan.offset);
|
|
1371
|
-
return plan.limit === undefined ? ordered : ordered.limit(plan.limit);
|
|
1372
|
-
});
|
|
1373
|
-
|
|
1374
|
-
const tail = (plan: {
|
|
1375
|
-
where?: unknown;
|
|
1376
|
-
orders?: readonly unknown[];
|
|
1377
|
-
}): SelectOrderBuilder => ({
|
|
1378
|
-
limit(n) {
|
|
1379
|
-
return run({ ...plan, limit: n });
|
|
1380
|
-
},
|
|
1381
|
-
offset(n) {
|
|
1382
|
-
return run({ ...plan, offset: n });
|
|
1383
|
-
},
|
|
1384
|
-
then(onfulfilled, onrejected) {
|
|
1385
|
-
return run(plan).then(onfulfilled, onrejected);
|
|
1386
|
-
},
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
return {
|
|
1390
|
-
where(where: unknown) {
|
|
1391
|
-
return {
|
|
1392
|
-
...tail({ where }),
|
|
1393
|
-
orderBy: (...orders: readonly unknown[]) => tail({ where, orders }),
|
|
1394
|
-
};
|
|
1395
|
-
},
|
|
1396
|
-
orderBy: (...orders: readonly unknown[]) => tail({ orders }),
|
|
1397
|
-
limit(n: number) {
|
|
1398
|
-
return run({ limit: n });
|
|
1399
|
-
},
|
|
1400
|
-
offset(n: number) {
|
|
1401
|
-
return run({ offset: n });
|
|
1402
|
-
},
|
|
1403
|
-
then(
|
|
1404
|
-
onfulfilled: (value: SqlRow[]) => unknown,
|
|
1405
|
-
onrejected?: (reason: unknown) => unknown,
|
|
1406
|
-
) {
|
|
1407
|
-
return run({}).then(onfulfilled, onrejected);
|
|
1408
|
-
},
|
|
1409
|
-
};
|
|
1410
|
-
},
|
|
1411
|
-
};
|
|
1412
|
-
}) as SqlStoreHandle["select"],
|
|
1413
|
-
insert(table) {
|
|
1414
|
-
return {
|
|
1415
|
-
values(row) {
|
|
1416
|
-
const runExecute = () =>
|
|
1417
|
-
gatedTable("write", table, async () => {
|
|
1418
|
-
refuseDryRunWrite();
|
|
1419
|
-
const h = await ensure();
|
|
1420
|
-
await h.insert(table).values(row).execute();
|
|
1421
|
-
});
|
|
1422
|
-
return {
|
|
1423
|
-
returning() {
|
|
1424
|
-
return gatedTable("write", table, async () => {
|
|
1425
|
-
refuseDryRunWrite();
|
|
1426
|
-
const h = await ensure();
|
|
1427
|
-
return h.insert(table).values(row).returning();
|
|
1428
|
-
});
|
|
1429
|
-
},
|
|
1430
|
-
execute: runExecute,
|
|
1431
|
-
then(onfulfilled, onrejected) {
|
|
1432
|
-
return runExecute().then(onfulfilled, onrejected);
|
|
1433
|
-
},
|
|
1434
|
-
};
|
|
1435
|
-
},
|
|
1436
|
-
};
|
|
1437
|
-
},
|
|
1438
|
-
update(table) {
|
|
1439
|
-
return {
|
|
1440
|
-
set(row) {
|
|
1441
|
-
return {
|
|
1442
|
-
where(where) {
|
|
1443
|
-
return gatedTable("write", table, async () => {
|
|
1444
|
-
refuseDryRunWrite();
|
|
1445
|
-
const h = await ensure();
|
|
1446
|
-
return h.update(table).set(row).where(where);
|
|
1447
|
-
});
|
|
1448
|
-
},
|
|
1449
|
-
};
|
|
1450
|
-
},
|
|
1451
|
-
};
|
|
1452
|
-
},
|
|
1453
|
-
findById(table, id) {
|
|
1454
|
-
return gatedTable("read", table, async () => {
|
|
1455
|
-
const h = await ensure();
|
|
1456
|
-
return h.findById(table, id);
|
|
1457
|
-
});
|
|
1458
|
-
},
|
|
1459
|
-
delete(table: Parameters<SqlStoreHandle["delete"]>[0], id?: string) {
|
|
1460
|
-
if (id !== undefined) {
|
|
1461
|
-
return gatedTable("write", table, async () => {
|
|
1462
|
-
refuseDryRunWrite();
|
|
1463
|
-
const h = await ensure();
|
|
1464
|
-
return h.delete(table, id);
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
where(where: unknown) {
|
|
1469
|
-
return gatedTable("write", table, async () => {
|
|
1470
|
-
refuseDryRunWrite();
|
|
1471
|
-
const h = await ensure();
|
|
1472
|
-
return h.delete(table).where(where);
|
|
1473
|
-
});
|
|
1474
|
-
},
|
|
1475
|
-
};
|
|
1476
|
-
},
|
|
1477
|
-
exists(table, idOrWhere) {
|
|
1478
|
-
return gatedTable("read", table, async () => {
|
|
1479
|
-
const h = await ensure();
|
|
1480
|
-
return h.exists(table, idOrWhere);
|
|
1481
|
-
});
|
|
1482
|
-
},
|
|
1483
|
-
upsert(table, matchOn, values, upsertOptions) {
|
|
1484
|
-
return gatedTable("write", table, async () => {
|
|
1485
|
-
refuseDryRunWrite();
|
|
1486
|
-
const h = await ensure();
|
|
1487
|
-
return h.upsert(table, matchOn, values, upsertOptions);
|
|
1488
|
-
});
|
|
1489
|
-
},
|
|
1490
|
-
increment(table, id, column, by) {
|
|
1491
|
-
return gatedTable("write", table, async () => {
|
|
1492
|
-
refuseDryRunWrite();
|
|
1493
|
-
const h = await ensure();
|
|
1494
|
-
return h.increment(table, id, column, by);
|
|
1495
|
-
});
|
|
1496
|
-
},
|
|
1497
|
-
raw(sql, params) {
|
|
1498
|
-
return gated("read", ref, async () => {
|
|
1499
|
-
const h = await ensure();
|
|
1500
|
-
return h.raw(sql, params);
|
|
1501
|
-
});
|
|
1502
|
-
},
|
|
1503
|
-
count(table, where) {
|
|
1504
|
-
return gatedTable("read", table, async () => {
|
|
1505
|
-
const h = await ensure();
|
|
1506
|
-
return h.count(table, where);
|
|
1507
|
-
});
|
|
1508
|
-
},
|
|
1509
|
-
page(table, pageOptions) {
|
|
1510
|
-
return gatedTable("read", table, async () => {
|
|
1511
|
-
const h = await ensure();
|
|
1512
|
-
return h.page(table, pageOptions);
|
|
1513
|
-
});
|
|
1514
|
-
},
|
|
1515
|
-
search(table, searchOptions) {
|
|
1516
|
-
return gatedTable("read", table, async () => {
|
|
1517
|
-
const h = await ensure();
|
|
1518
|
-
const result = await h.search(table, searchOptions);
|
|
1519
|
-
// Optional rerank via fx.ask — only when explicitly requested.
|
|
1520
|
-
if (
|
|
1521
|
-
searchOptions.rerank &&
|
|
1522
|
-
typeof searchOptions.rerank === "object" &&
|
|
1523
|
-
searchOptions.rerank.model
|
|
1524
|
-
) {
|
|
1525
|
-
const model = searchOptions.rerank.model;
|
|
1526
|
-
const pk = "id";
|
|
1527
|
-
const docs = result.data.map((row) => ({
|
|
1528
|
-
id: String(row[pk] ?? ""),
|
|
1529
|
-
text: Object.values(row)
|
|
1530
|
-
.filter((v) => typeof v === "string")
|
|
1531
|
-
.join("\n"),
|
|
1532
|
-
score: 0,
|
|
1533
|
-
}));
|
|
1534
|
-
const out = (await fx.ask(model, {
|
|
1535
|
-
query: searchOptions.query,
|
|
1536
|
-
docs,
|
|
1537
|
-
})) as { rankedIds?: string[] };
|
|
1538
|
-
if (out.rankedIds && out.rankedIds.length > 0) {
|
|
1539
|
-
const byId = new Map(result.data.map((r) => [String(r[pk] ?? ""), r]));
|
|
1540
|
-
return {
|
|
1541
|
-
...result,
|
|
1542
|
-
data: out.rankedIds
|
|
1543
|
-
.map((id) => byId.get(id))
|
|
1544
|
-
.filter((r): r is NonNullable<typeof r> => r !== undefined),
|
|
1545
|
-
};
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
return result;
|
|
1549
|
-
});
|
|
1550
|
-
},
|
|
1551
|
-
ensureTable(table) {
|
|
1552
|
-
return gatedTable("write", table, async () => {
|
|
1553
|
-
refuseDryRunWrite();
|
|
1554
|
-
const h = await ensure();
|
|
1555
|
-
return h.ensureTable(table);
|
|
1556
|
-
});
|
|
1557
|
-
},
|
|
1558
|
-
} as SqlStoreHandle;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
1266
|
function loadFxTenantStore(): {
|
|
1562
1267
|
kv: (
|
|
1563
1268
|
mode: "in" | "out",
|
|
@@ -1617,9 +1322,12 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1617
1322
|
};
|
|
1618
1323
|
|
|
1619
1324
|
if (decl.facet === "sql") {
|
|
1620
|
-
return
|
|
1621
|
-
|
|
1622
|
-
|
|
1325
|
+
return loadFxSql().createGatedSqlHandle({
|
|
1326
|
+
decl,
|
|
1327
|
+
open: async () => (await open()) as SqlStoreHandle,
|
|
1328
|
+
gated,
|
|
1329
|
+
capability,
|
|
1330
|
+
ask: (model, input) => fx.ask(model, input),
|
|
1623
1331
|
});
|
|
1624
1332
|
}
|
|
1625
1333
|
|
|
@@ -2292,16 +2000,16 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
2292
2000
|
return rlsInvokeContext().rls;
|
|
2293
2001
|
},
|
|
2294
2002
|
all(thunks) {
|
|
2295
|
-
return fxAll(thunks);
|
|
2003
|
+
return loadConcurrency().fxAll(thunks);
|
|
2296
2004
|
},
|
|
2297
2005
|
race(thunks) {
|
|
2298
|
-
return fxRace(thunks);
|
|
2006
|
+
return loadConcurrency().fxRace(thunks);
|
|
2299
2007
|
},
|
|
2300
2008
|
retry(fn, opts) {
|
|
2301
|
-
return fxRetry(fn, opts);
|
|
2009
|
+
return loadConcurrency().fxRetry(fn, opts);
|
|
2302
2010
|
},
|
|
2303
2011
|
using(acquire, release, use) {
|
|
2304
|
-
return fxUsing(acquire, release, use);
|
|
2012
|
+
return loadConcurrency().fxUsing(acquire, release, use);
|
|
2305
2013
|
},
|
|
2306
2014
|
};
|
|
2307
2015
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fx.json` carriers — brand and guards, with no `fx` / Store graph.
|
|
3
|
+
*
|
|
4
|
+
* `project-out` and the HTTP encoder need these on the cold-start path.
|
|
5
|
+
* Importing them from `fx.ts` would evaluate the Store barrel (Zod, hybrid
|
|
6
|
+
* search) before the server listens.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Brand for {@link JsonResult} (kept internal — flows never construct it). */
|
|
10
|
+
export const jsonResultBrand: unique symbol = Symbol.for("oke.json");
|
|
11
|
+
|
|
12
|
+
/** Carrier from `fx.json` — status + body read by the response encoder. */
|
|
13
|
+
export interface JsonResult<T = unknown> {
|
|
14
|
+
readonly [jsonResultBrand]: true;
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly value?: T;
|
|
17
|
+
readonly meta?: Record<string, unknown>;
|
|
18
|
+
readonly kind?: undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** SSE carrier from `fx.json.stream` / `fx.live`. */
|
|
22
|
+
export interface JsonStreamResult {
|
|
23
|
+
readonly [jsonResultBrand]: true;
|
|
24
|
+
readonly kind: "stream";
|
|
25
|
+
readonly status: 200;
|
|
26
|
+
readonly chunks: AsyncIterable<unknown>;
|
|
27
|
+
/** Awaited before the 200 SSE body; throws OKE1210 on a missing resume cursor. */
|
|
28
|
+
ready?: () => Promise<void>;
|
|
29
|
+
/** Set by the kernel to commit journal / Runs after the stream settles. */
|
|
30
|
+
finalize?: () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True when `value` is an `fx.json` envelope carrier.
|
|
35
|
+
*
|
|
36
|
+
* @param value - Unknown handler output
|
|
37
|
+
*/
|
|
38
|
+
export function isJsonResult(value: unknown): value is JsonResult {
|
|
39
|
+
return (
|
|
40
|
+
typeof value === "object" &&
|
|
41
|
+
value !== null &&
|
|
42
|
+
(value as JsonResult)[jsonResultBrand] === true &&
|
|
43
|
+
(value as JsonStreamResult).kind !== "stream"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when `value` is an SSE stream carrier from `fx.json.stream`.
|
|
49
|
+
*
|
|
50
|
+
* @param value - Unknown handler output
|
|
51
|
+
*/
|
|
52
|
+
export function isJsonStreamResult(value: unknown): value is JsonStreamResult {
|
|
53
|
+
return (
|
|
54
|
+
typeof value === "object" &&
|
|
55
|
+
value !== null &&
|
|
56
|
+
(value as JsonStreamResult)[jsonResultBrand] === true &&
|
|
57
|
+
(value as JsonStreamResult).kind === "stream"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import type { SchemaInput } from "../validation/standard-schema.ts";
|
|
14
14
|
import { validate } from "../validation/standard-schema.ts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
isJsonResult,
|
|
17
|
+
isJsonStreamResult,
|
|
18
|
+
jsonResultBrand,
|
|
19
|
+
type JsonResult,
|
|
20
|
+
} from "./json-result.ts";
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Project `output` through `schema` when the exposure declared `out`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `okid({ … })` options path — sortable, prefix, and alphabet toggles.
|
|
3
|
+
*
|
|
4
|
+
* Kept off the kernel edge profile. `okid()` and `okid(length)` never load it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
assertLength,
|
|
9
|
+
OKID_ALPHABET,
|
|
10
|
+
OKID_DEFAULT_LENGTH,
|
|
11
|
+
OKID_LOOKALIKE_CHARS,
|
|
12
|
+
OKID_MAX_PREFIX_LENGTH,
|
|
13
|
+
OKID_MIN_LENGTH,
|
|
14
|
+
OKID_SORTABLE_ALPHABET,
|
|
15
|
+
OKID_SORTABLE_MIN_LENGTH,
|
|
16
|
+
type OkidOptions,
|
|
17
|
+
} from "./okid-shared.ts";
|
|
18
|
+
|
|
19
|
+
/** Character groups addressable through {@link OkidOptions} toggles. */
|
|
20
|
+
const GROUPS = {
|
|
21
|
+
numbers: "0123456789",
|
|
22
|
+
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
|
23
|
+
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
24
|
+
symbols: "-_",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/** Resolved alphabet + encoding metadata for one options combination. */
|
|
28
|
+
interface ResolvedAlphabet {
|
|
29
|
+
readonly chars: string;
|
|
30
|
+
readonly size: number;
|
|
31
|
+
/** Bitmask covering `size` values (`size` is always a power of two here). */
|
|
32
|
+
readonly mask: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Memoized resolutions keyed by the toggle bitmask (32 combinations max). */
|
|
36
|
+
const ALPHABET_CACHE = new Map<number, ResolvedAlphabet>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a toggle combination to an alphabet and rejection-sampling mask.
|
|
40
|
+
*
|
|
41
|
+
* @param numbers - Include `0-9`
|
|
42
|
+
* @param lowercase - Include `a-z`
|
|
43
|
+
* @param uppercase - Include `A-Z`
|
|
44
|
+
* @param symbols - Include `-` and `_`
|
|
45
|
+
* @param lookAlikes - Include visually confusable characters
|
|
46
|
+
*/
|
|
47
|
+
function resolveAlphabet(
|
|
48
|
+
numbers: boolean,
|
|
49
|
+
lowercase: boolean,
|
|
50
|
+
uppercase: boolean,
|
|
51
|
+
symbols: boolean,
|
|
52
|
+
lookAlikes: boolean,
|
|
53
|
+
): ResolvedAlphabet {
|
|
54
|
+
const key =
|
|
55
|
+
(numbers ? 1 : 0) |
|
|
56
|
+
(lowercase ? 2 : 0) |
|
|
57
|
+
(uppercase ? 4 : 0) |
|
|
58
|
+
(symbols ? 8 : 0) |
|
|
59
|
+
(lookAlikes ? 0 : 16);
|
|
60
|
+
const cached = ALPHABET_CACHE.get(key);
|
|
61
|
+
if (cached) return cached;
|
|
62
|
+
|
|
63
|
+
let chars = "";
|
|
64
|
+
if (numbers) chars += GROUPS.numbers;
|
|
65
|
+
if (lowercase) chars += GROUPS.lowercase;
|
|
66
|
+
if (uppercase) chars += GROUPS.uppercase;
|
|
67
|
+
if (symbols) chars += GROUPS.symbols;
|
|
68
|
+
if (!chars) {
|
|
69
|
+
throw new RangeError("okid: alphabet is empty — enable at least one character group");
|
|
70
|
+
}
|
|
71
|
+
if (!lookAlikes) {
|
|
72
|
+
chars = [...chars].filter((c) => !OKID_LOOKALIKE_CHARS.includes(c)).join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Round up to a power of two for mask-based rejection sampling: bytes below
|
|
76
|
+
// `size` map uniformly, bytes above are discarded and re-drawn — unbiased at
|
|
77
|
+
// every alphabet size, unlike naive modulo.
|
|
78
|
+
const rawSize = chars.length;
|
|
79
|
+
const size = 1 << Math.ceil(Math.log2(rawSize));
|
|
80
|
+
const resolved: ResolvedAlphabet = { chars, size: rawSize, mask: size - 1 };
|
|
81
|
+
ALPHABET_CACHE.set(key, resolved);
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Encode one random byte stream into `length` characters of `alphabet`.
|
|
87
|
+
*
|
|
88
|
+
* @param alphabet - Resolved charset
|
|
89
|
+
* @param length - Output length
|
|
90
|
+
*/
|
|
91
|
+
function encodeAlphabet(alphabet: ResolvedAlphabet, length: number): string {
|
|
92
|
+
const { chars, size, mask } = alphabet;
|
|
93
|
+
const bytes = new Uint8Array(length + Math.ceil(length >> 2));
|
|
94
|
+
crypto.getRandomValues(bytes.subarray(0, length));
|
|
95
|
+
let out = "";
|
|
96
|
+
let i = 0;
|
|
97
|
+
while (out.length < length && i < bytes.length) {
|
|
98
|
+
const byte = bytes[i++]!;
|
|
99
|
+
if ((byte & mask) < size) out += chars[byte & mask];
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pack epoch-ms into exactly 8 codepoint-ordered characters (48 bits).
|
|
106
|
+
*
|
|
107
|
+
* @param nowMs - Epoch milliseconds
|
|
108
|
+
*/
|
|
109
|
+
function encodeTimestamp(nowMs: number): string {
|
|
110
|
+
let t = nowMs % 2 ** 48;
|
|
111
|
+
let out = "";
|
|
112
|
+
for (let i = 0; i < 8; i++) {
|
|
113
|
+
out = OKID_SORTABLE_ALPHABET[t & 63]! + out;
|
|
114
|
+
t = Math.floor(t / 64);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Assert a semantic prefix is within bounds and stays on the URL-safe alphabet.
|
|
121
|
+
*
|
|
122
|
+
* @param prefix - Caller-supplied label
|
|
123
|
+
*/
|
|
124
|
+
function assertPrefix(prefix: string): void {
|
|
125
|
+
if (prefix.length > OKID_MAX_PREFIX_LENGTH) {
|
|
126
|
+
throw new RangeError(
|
|
127
|
+
`okid: prefix length ${prefix.length} exceeds max ${OKID_MAX_PREFIX_LENGTH}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
for (const char of prefix) {
|
|
131
|
+
if (!OKID_ALPHABET.includes(char)) {
|
|
132
|
+
throw new RangeError(
|
|
133
|
+
`okid: prefix contains invalid character ${JSON.stringify(char)} — use characters from OKID_ALPHABET`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate an id from an options object.
|
|
141
|
+
*
|
|
142
|
+
* @param options - Length, prefix, sortable, and alphabet toggles
|
|
143
|
+
*/
|
|
144
|
+
export function okidWithOptions(options: OkidOptions): string {
|
|
145
|
+
const {
|
|
146
|
+
length = OKID_DEFAULT_LENGTH,
|
|
147
|
+
prefix = "",
|
|
148
|
+
sortable = false,
|
|
149
|
+
lowercase = true,
|
|
150
|
+
uppercase = true,
|
|
151
|
+
numbers = true,
|
|
152
|
+
symbols = true,
|
|
153
|
+
lookAlikes = true,
|
|
154
|
+
} = options;
|
|
155
|
+
|
|
156
|
+
if (prefix) assertPrefix(prefix);
|
|
157
|
+
|
|
158
|
+
let body: string;
|
|
159
|
+
if (sortable) {
|
|
160
|
+
assertLength(length, OKID_SORTABLE_MIN_LENGTH, "length");
|
|
161
|
+
// Time ordering requires lexicographic encoding, which requires the full
|
|
162
|
+
// codepoint-ordered alphabet — partial subsets cannot preserve both the
|
|
163
|
+
// caller's charset choice AND cross-ms ordering, so toggles are ignored.
|
|
164
|
+
const alphabet = resolveAlphabet(true, true, true, true, true);
|
|
165
|
+
body = encodeTimestamp(Date.now()) + encodeAlphabet(alphabet, length - 8);
|
|
166
|
+
} else {
|
|
167
|
+
assertLength(length, OKID_MIN_LENGTH, "length");
|
|
168
|
+
body = encodeAlphabet(
|
|
169
|
+
resolveAlphabet(numbers, lowercase, uppercase, symbols, lookAlikes),
|
|
170
|
+
length,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return prefix ? prefix + body : body;
|
|
175
|
+
}
|