okengine 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.v1.schema.json +21 -2
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +7 -0
- package/site/content/docs/elements/flow.mdx +20 -12
- package/site/content/docs/elements/gate.mdx +108 -3
- package/site/content/docs/elements/signal.mdx +46 -17
- package/site/content/docs/elements/store.mdx +36 -32
- package/site/content/docs/elements/vault.mdx +26 -0
- package/site/content/docs/get-started/project-structure.mdx +4 -4
- package/site/content/docs/reference/cli.md +1 -1
- package/site/content/docs/reference/client.mdx +75 -17
- package/site/content/docs/reference/configuration.mdx +7 -4
- package/site/content/docs/reference/errors.mdx +24 -17
- package/site/content/docs/reference/fx.mdx +39 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +261 -0
- package/src/auth/api-keys.ts +276 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/config.ts +24 -5
- package/src/auth/index.ts +62 -1
- package/src/auth/plugin.ts +6 -1
- package/src/auth/schema.ts +8 -0
- package/src/auth/sessions.ts +18 -0
- package/src/auth/tables.ts +32 -0
- package/src/auth/tenant-config.ts +74 -0
- package/src/auth/tenant-tables.ts +11 -0
- package/src/auth/tenants.test.ts +63 -0
- package/src/auth/tenants.ts +360 -0
- package/src/cli/attach-host-console.test.ts +37 -0
- package/src/cli/attach-host-console.ts +6 -0
- package/src/cli/build.ts +2 -2
- package/src/client/budget.test.ts +1 -1
- package/src/client/create.ts +75 -5
- package/src/client/index.ts +13 -0
- package/src/client/live.test.ts +422 -0
- package/src/client/live.ts +389 -0
- package/src/client/notes-contract.test.ts +41 -0
- package/src/client/types.ts +85 -6
- package/src/client-react/index.ts +85 -1
- package/src/client-react/use-live.test.ts +129 -0
- package/src/compiler/effects-infer.ts +37 -2
- package/src/compiler/extract.test.ts +143 -11
- package/src/compiler/extract.ts +210 -30
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +2 -3
- package/src/compiler/response.ts +41 -11
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +45 -11
- package/src/console/server/console.test.ts +6 -8
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +113 -58
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +10 -30
- package/src/console/server/index.ts +1 -0
- package/src/console/server/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/state.ts +29 -55
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +43 -1
- package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
- package/src/console/ui-next/dist/index.html +11 -9
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +167 -0
- package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
- package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
- package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/journal-postgres.ts +12 -3
- package/src/drivers/pg-rls.ts +26 -2
- package/src/drivers/pg-vault-rls.ts +68 -0
- package/src/drivers/signal-engine.ts +69 -15
- package/src/drivers/signal-live-iter.ts +65 -0
- package/src/drivers/signal-nats.ts +2 -1
- package/src/drivers/signal-postgres.ts +95 -12
- package/src/drivers/signal-redis.ts +2 -1
- package/src/drivers/signal-retention.ts +64 -0
- package/src/drivers/signal-types.ts +35 -5
- package/src/elements/clock/declare.ts +64 -2
- package/src/elements/clock/reconcile.ts +98 -25
- package/src/elements/clock/runtime.ts +13 -2
- package/src/elements/clock.test.ts +28 -0
- package/src/elements/clock.ts +9 -1
- package/src/elements/gate/declare.ts +2 -0
- package/src/elements/gate/permissions.ts +12 -0
- package/src/elements/gate.ts +6 -1
- package/src/elements/signal/declare.ts +44 -10
- package/src/elements/signal/delivery-modes.test.ts +90 -4
- package/src/elements/signal/order-lifecycle.test.ts +20 -4
- package/src/elements/signal/runtime.ts +42 -0
- package/src/elements/signal.test.ts +21 -8
- package/src/elements/signal.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +7 -0
- package/src/elements/store/rls-identity.test.ts +29 -0
- package/src/elements/store/rls-identity.ts +3 -0
- package/src/elements/store/runtime.ts +14 -0
- package/src/elements/store/schema-decl.ts +162 -44
- package/src/elements/store/schema-tenant.ts +41 -0
- package/src/elements/store/sql-rls-isolation.test.ts +39 -0
- package/src/elements/store/sql-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +54 -32
- package/src/elements/store.ts +5 -0
- package/src/elements/vault/builtin-adapter.ts +15 -2
- package/src/elements/vault/declare.ts +8 -0
- package/src/elements/vault/runtime.ts +7 -0
- package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
- package/src/elements/vault/storage.ts +9 -0
- package/src/elements/vault/test-helpers.ts +2 -1
- package/src/i18n/catalogs/ar.ts +19 -0
- package/src/i18n/catalogs/en.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernel/adopt-routes.ts +56 -8
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app-tenant.ts +122 -0
- package/src/kernel/app.ts +196 -57
- package/src/kernel/auth-resolve.ts +13 -5
- package/src/kernel/boot-bind/clock.ts +9 -3
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/clock-durable.ts +8 -0
- package/src/kernel/clock-per-tenant-name.ts +5 -0
- package/src/kernel/clock-reconcile.ts +8 -0
- package/src/kernel/errors-live-resume.ts +15 -0
- package/src/kernel/errors-tenant.ts +29 -0
- package/src/kernel/errors.registry.test.ts +31 -3
- package/src/kernel/errors.ts +43 -3
- package/src/kernel/flow.ts +26 -5
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +203 -0
- package/src/kernel/fx-auth-tenants.test.ts +87 -0
- package/src/kernel/fx-auth-tenants.ts +286 -0
- package/src/kernel/fx-live-stream.ts +149 -0
- package/src/kernel/fx-live.test.ts +157 -0
- package/src/kernel/fx-runtime.ts +15 -0
- package/src/kernel/fx-tenant-store.ts +213 -0
- package/src/kernel/fx.test.ts +108 -1
- package/src/kernel/fx.ts +206 -28
- package/src/kernel/hooks.ts +2 -2
- package/src/kernel/http-resource.ts +9 -18
- package/src/kernel/index.ts +2 -0
- package/src/kernel/journal.ts +12 -0
- package/src/kernel/live-http.test.ts +78 -0
- package/src/kernel/live-http.ts +114 -0
- package/src/kernel/live-resume.test.ts +125 -0
- package/src/kernel/on.ts +51 -0
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline-tenant.ts +49 -0
- package/src/kernel/pipeline.test.ts +1 -1
- package/src/kernel/pipeline.ts +41 -4
- package/src/kernel/resource-mount.test.ts +10 -27
- package/src/kernel/tenant-resolve.test.ts +101 -0
- package/src/kernel/tenant-resolve.ts +124 -0
- package/src/kernel/tenant-roles.test.ts +87 -0
- package/src/kernel/triggers.ts +59 -21
- package/src/manifest/diff.test.ts +11 -2
- package/src/manifest/diff.ts +53 -11
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +0 -1
- package/src/manifest/types.ts +58 -6
- package/src/release/build-lib.ts +14 -1
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +55 -1
- package/src/client/live-gap.test.ts +0 -35
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
- package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
|
@@ -91,6 +91,18 @@ function collectEffects(manifest: Manifest, kind: "reads" | "writes"): string[]
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Whether a Module:Action is an application (user-plane) scope.
|
|
96
|
+
*
|
|
97
|
+
* Console / operator scopes are `console:*` (Access · API-key plane split).
|
|
98
|
+
* Tenant roles may only grant application scopes.
|
|
99
|
+
*
|
|
100
|
+
* @param action - Module:Action pair
|
|
101
|
+
*/
|
|
102
|
+
export function isApplicationScope(action: string): boolean {
|
|
103
|
+
return !action.startsWith("console:");
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
/**
|
|
95
107
|
* Format pairs for `oke gates list` stdout.
|
|
96
108
|
*
|
package/src/elements/gate.ts
CHANGED
|
@@ -63,4 +63,9 @@ export {
|
|
|
63
63
|
type TakeRateOptions,
|
|
64
64
|
} from "./gate/strategies.ts";
|
|
65
65
|
|
|
66
|
-
export {
|
|
66
|
+
export {
|
|
67
|
+
deriveModuleActions,
|
|
68
|
+
flowIdToAction,
|
|
69
|
+
formatGatesList,
|
|
70
|
+
isApplicationScope,
|
|
71
|
+
} from "./gate/permissions.ts";
|
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
import type { SignalDelivery } from "../../manifest/types.ts";
|
|
9
9
|
import { signalRegistry } from "../../kernel/element-registries.ts";
|
|
10
|
+
import { parseDurationMs } from "../clock/duration.ts";
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
export interface
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
/** Live-tape cap. Omit both fields (or omit `retention`) for an unbounded tape. */
|
|
13
|
+
export interface SignalRetention {
|
|
14
|
+
/** Drop events older than this duration (`"7d"`, `"1h"`, `"30s"`, …). */
|
|
15
|
+
readonly maxAge?: string;
|
|
16
|
+
/** Keep only the newest N live events. */
|
|
17
|
+
readonly maxCount?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Shared options for every {@link signal} delivery mode. */
|
|
21
|
+
interface SignalOptionsBase {
|
|
21
22
|
/** Optional human description for Console / docs (falls back to the signal name). */
|
|
22
23
|
readonly description?: string;
|
|
23
24
|
/** Max delivery attempts before dead-letter (once). */
|
|
@@ -33,6 +34,21 @@ export interface SignalOptions {
|
|
|
33
34
|
readonly optional?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Options for {@link signal}.
|
|
39
|
+
*
|
|
40
|
+
* `retention` is live-only — a type error on `once` / `broadcast`.
|
|
41
|
+
*/
|
|
42
|
+
export type SignalOptions =
|
|
43
|
+
| (SignalOptionsBase & {
|
|
44
|
+
readonly delivery: "once" | "broadcast";
|
|
45
|
+
readonly retention?: never;
|
|
46
|
+
})
|
|
47
|
+
| (SignalOptionsBase & {
|
|
48
|
+
readonly delivery: "live";
|
|
49
|
+
readonly retention?: SignalRetention;
|
|
50
|
+
});
|
|
51
|
+
|
|
36
52
|
/**
|
|
37
53
|
* Declared signal handle — usable as `on(signal, flow)`, `fx.emit(signal, …)`,
|
|
38
54
|
* and `fx.deadLetters(signal)`.
|
|
@@ -52,6 +68,8 @@ export interface SignalDecl<T = unknown> {
|
|
|
52
68
|
readonly schema?: unknown;
|
|
53
69
|
/** Optional orphan-emit allowance. */
|
|
54
70
|
readonly optional: boolean;
|
|
71
|
+
/** Live-tape cap (`delivery: "live"` only). Omitted = unbounded. */
|
|
72
|
+
readonly retention?: SignalRetention;
|
|
55
73
|
/** Phantom payload type for typed emits. */
|
|
56
74
|
readonly _payload?: T;
|
|
57
75
|
}
|
|
@@ -91,6 +109,21 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
|
|
|
91
109
|
) {
|
|
92
110
|
throw new TypeError(`signal("${name}"): delivery is mandatory (once | broadcast | live)`);
|
|
93
111
|
}
|
|
112
|
+
const retention = "retention" in options ? options.retention : undefined;
|
|
113
|
+
if (retention !== undefined && options.delivery !== "live") {
|
|
114
|
+
throw new TypeError(`signal("${name}"): retention is only valid with delivery: "live"`);
|
|
115
|
+
}
|
|
116
|
+
if (retention?.maxAge !== undefined && parseDurationMs(retention.maxAge) <= 0) {
|
|
117
|
+
throw new TypeError(
|
|
118
|
+
`signal("${name}"): retention.maxAge must be a duration like "24h" or "30s"`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
retention?.maxCount !== undefined &&
|
|
123
|
+
(!Number.isInteger(retention.maxCount) || retention.maxCount < 1)
|
|
124
|
+
) {
|
|
125
|
+
throw new TypeError(`signal("${name}"): retention.maxCount must be an integer ≥ 1`);
|
|
126
|
+
}
|
|
94
127
|
const decl: SignalDecl<T> = {
|
|
95
128
|
name,
|
|
96
129
|
delivery: options.delivery,
|
|
@@ -99,6 +132,7 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
|
|
|
99
132
|
deadLetter: options.deadLetter ?? true,
|
|
100
133
|
schema: options.schema,
|
|
101
134
|
optional: options.optional ?? false,
|
|
135
|
+
...(options.delivery === "live" && retention !== undefined ? { retention } : {}),
|
|
102
136
|
};
|
|
103
137
|
signalRegistry.push(decl as SignalDecl);
|
|
104
138
|
return decl;
|
|
@@ -20,6 +20,26 @@ import {
|
|
|
20
20
|
type SignalDriver,
|
|
21
21
|
} from "../../drivers/index.ts";
|
|
22
22
|
import { signal, type SignalDecl } from "./declare.ts";
|
|
23
|
+
import type { LiveEvent } from "../../drivers/signal-types.ts";
|
|
24
|
+
|
|
25
|
+
async function takeLivePayloads(iter: AsyncIterable<LiveEvent>, n: number): Promise<unknown[]> {
|
|
26
|
+
return (await takeLiveEvents(iter, n)).map((e) => e.payload);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function takeLiveEvents(iter: AsyncIterable<LiveEvent>, n: number): Promise<LiveEvent[]> {
|
|
30
|
+
const out: LiveEvent[] = [];
|
|
31
|
+
const it = iter[Symbol.asyncIterator]();
|
|
32
|
+
try {
|
|
33
|
+
while (out.length < n) {
|
|
34
|
+
const step = await it.next();
|
|
35
|
+
if (step.done) break;
|
|
36
|
+
out.push(step.value);
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
await it.return?.();
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
23
43
|
|
|
24
44
|
const drivers: Array<{
|
|
25
45
|
label: string;
|
|
@@ -167,13 +187,79 @@ for (const { label, driver, setup } of drivers) {
|
|
|
167
187
|
await bus.emit("seat-feed", { seat: "12C" });
|
|
168
188
|
await bus.drain();
|
|
169
189
|
|
|
170
|
-
const late
|
|
171
|
-
await bus.live("seat-feed", (payload) => {
|
|
172
|
-
late.push(payload);
|
|
173
|
-
});
|
|
190
|
+
const late = await takeLivePayloads(bus.live("seat-feed"), 3);
|
|
174
191
|
|
|
175
192
|
// All retained live messages replay — not the Console recentLive cap of 50.
|
|
176
193
|
expect(late).toEqual([{ seat: "12A" }, { seat: "12B" }, { seat: "12C" }]);
|
|
177
194
|
});
|
|
195
|
+
|
|
196
|
+
test("live: maxCount keeps the newest N", async () => {
|
|
197
|
+
const live = signal("seat-feed", {
|
|
198
|
+
delivery: "live",
|
|
199
|
+
optional: true,
|
|
200
|
+
retention: { maxCount: 2 },
|
|
201
|
+
});
|
|
202
|
+
const bus = await openBus(driver, [live], setup?.() ?? {});
|
|
203
|
+
|
|
204
|
+
for (const seat of ["12A", "12B", "12C", "12D", "12E"]) {
|
|
205
|
+
await bus.emit("seat-feed", { seat });
|
|
206
|
+
}
|
|
207
|
+
await bus.drain();
|
|
208
|
+
|
|
209
|
+
const late = await takeLivePayloads(bus.live("seat-feed"), 2);
|
|
210
|
+
expect(late).toEqual([{ seat: "12D" }, { seat: "12E" }]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("live: maxAge prunes on emit and on live() open", async () => {
|
|
214
|
+
let t = 1_000;
|
|
215
|
+
const live = signal("seat-feed", {
|
|
216
|
+
delivery: "live",
|
|
217
|
+
optional: true,
|
|
218
|
+
retention: { maxAge: "1s" },
|
|
219
|
+
});
|
|
220
|
+
const bus = await openBus(driver, [live], { now: () => t, ...(setup?.() ?? {}) });
|
|
221
|
+
|
|
222
|
+
await bus.emit("seat-feed", { seat: "old" });
|
|
223
|
+
await bus.drain();
|
|
224
|
+
t += 2_000;
|
|
225
|
+
await bus.emit("seat-feed", { seat: "new" });
|
|
226
|
+
await bus.drain();
|
|
227
|
+
|
|
228
|
+
const late = await takeLivePayloads(bus.live("seat-feed"), 1);
|
|
229
|
+
expect(late).toEqual([{ seat: "new" }]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("live: afterId skips already-delivered events", async () => {
|
|
233
|
+
const live = signal("seat-feed", { delivery: "live", optional: true });
|
|
234
|
+
const bus = await openBus(driver, [live], setup?.() ?? {});
|
|
235
|
+
|
|
236
|
+
await bus.emit("seat-feed", { seat: "12A" });
|
|
237
|
+
await bus.emit("seat-feed", { seat: "12B" });
|
|
238
|
+
await bus.emit("seat-feed", { seat: "12C" });
|
|
239
|
+
await bus.drain();
|
|
240
|
+
|
|
241
|
+
const first = await takeLiveEvents(bus.live("seat-feed"), 3);
|
|
242
|
+
expect(first.map((e) => e.payload)).toEqual([
|
|
243
|
+
{ seat: "12A" },
|
|
244
|
+
{ seat: "12B" },
|
|
245
|
+
{ seat: "12C" },
|
|
246
|
+
]);
|
|
247
|
+
const rest = await takeLiveEvents(bus.live("seat-feed", { afterId: first[0]!.id }), 2);
|
|
248
|
+
expect(rest.map((e) => e.payload)).toEqual([{ seat: "12B" }, { seat: "12C" }]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("live: unknown afterId throws OKE1014", async () => {
|
|
252
|
+
const live = signal("seat-feed", { delivery: "live", optional: true });
|
|
253
|
+
const bus = await openBus(driver, [live], setup?.() ?? {});
|
|
254
|
+
await bus.emit("seat-feed", { seat: "12A" });
|
|
255
|
+
await bus.drain();
|
|
256
|
+
|
|
257
|
+
const it = bus.live("seat-feed", { afterId: "missing" })[Symbol.asyncIterator]();
|
|
258
|
+
try {
|
|
259
|
+
await expect(it.next()).rejects.toMatchObject({ code: 1014 });
|
|
260
|
+
} finally {
|
|
261
|
+
await it.return?.();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
178
264
|
});
|
|
179
265
|
}
|
|
@@ -13,8 +13,24 @@
|
|
|
13
13
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
14
14
|
|
|
15
15
|
import { memorySignalDriver, type SignalBus } from "../../drivers/index.ts";
|
|
16
|
+
import type { LiveEvent } from "../../drivers/signal-types.ts";
|
|
16
17
|
import { signal } from "./declare.ts";
|
|
17
18
|
|
|
19
|
+
async function takeLivePayloads(iter: AsyncIterable<LiveEvent>, n: number): Promise<unknown[]> {
|
|
20
|
+
const out: unknown[] = [];
|
|
21
|
+
const it = iter[Symbol.asyncIterator]();
|
|
22
|
+
try {
|
|
23
|
+
while (out.length < n) {
|
|
24
|
+
const step = await it.next();
|
|
25
|
+
if (step.done) break;
|
|
26
|
+
out.push(step.value.payload);
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
await it.return?.();
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
const openBuses: SignalBus[] = [];
|
|
19
35
|
|
|
20
36
|
afterEach(async () => {
|
|
@@ -89,10 +105,10 @@ describe("signal order lifecycle · once + broadcast + live", () => {
|
|
|
89
105
|
expect(notifyHits).toEqual(["ord_42"]);
|
|
90
106
|
|
|
91
107
|
// live: late subscriber replays the full retained status history
|
|
92
|
-
const feed
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
108
|
+
const feed = (await takeLivePayloads(bus.live("order-status"), 3)) as Array<{
|
|
109
|
+
orderId: string;
|
|
110
|
+
status: string;
|
|
111
|
+
}>;
|
|
96
112
|
expect(feed).toEqual([
|
|
97
113
|
{ orderId: "ord_42", status: "placed" },
|
|
98
114
|
{ orderId: "ord_42", status: "fulfilling" },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
DeadLetter,
|
|
8
|
+
LiveEvent,
|
|
8
9
|
SignalBus,
|
|
9
10
|
SignalDriver,
|
|
10
11
|
SignalEmitOptions,
|
|
@@ -59,6 +60,20 @@ export interface SignalRuntime {
|
|
|
59
60
|
* @param name - Signal name
|
|
60
61
|
*/
|
|
61
62
|
deadLetters(name: string): Promise<readonly DeadLetter[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Live feed (auto-starts). Replays history then new events.
|
|
65
|
+
*
|
|
66
|
+
* @param name - Signal name
|
|
67
|
+
* @param opts - Optional resume cursor
|
|
68
|
+
*/
|
|
69
|
+
live(name: string, opts?: { readonly afterId?: string }): AsyncIterable<LiveEvent>;
|
|
70
|
+
/**
|
|
71
|
+
* Validate a live resume cursor (auto-starts). Throws OKE1014 when missing.
|
|
72
|
+
*
|
|
73
|
+
* @param name - Signal name
|
|
74
|
+
* @param afterId - SSE cursor
|
|
75
|
+
*/
|
|
76
|
+
checkLiveResume(name: string, afterId: string): Promise<void>;
|
|
62
77
|
/** Close the bus. */
|
|
63
78
|
close(): Promise<void>;
|
|
64
79
|
}
|
|
@@ -102,6 +117,33 @@ export function createSignalRuntime(options: CreateSignalRuntimeOptions): Signal
|
|
|
102
117
|
const b = await this.start();
|
|
103
118
|
return b.deadLetters(name);
|
|
104
119
|
},
|
|
120
|
+
live(name, opts) {
|
|
121
|
+
return {
|
|
122
|
+
[Symbol.asyncIterator]() {
|
|
123
|
+
let inner: AsyncIterator<LiveEvent> | undefined;
|
|
124
|
+
return {
|
|
125
|
+
async next() {
|
|
126
|
+
if (!inner) {
|
|
127
|
+
const b = await runtime.start();
|
|
128
|
+
inner = b.live(name, opts)[Symbol.asyncIterator]();
|
|
129
|
+
}
|
|
130
|
+
return inner.next();
|
|
131
|
+
},
|
|
132
|
+
async return() {
|
|
133
|
+
if (!inner) {
|
|
134
|
+
const b = await runtime.start();
|
|
135
|
+
inner = b.live(name, opts)[Symbol.asyncIterator]();
|
|
136
|
+
}
|
|
137
|
+
return inner.return?.() ?? { done: true, value: undefined };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
async checkLiveResume(name, afterId) {
|
|
144
|
+
const b = await runtime.start();
|
|
145
|
+
await b.checkLiveResume(name, afterId);
|
|
146
|
+
},
|
|
105
147
|
async close() {
|
|
106
148
|
if (bus) {
|
|
107
149
|
await bus.close();
|
|
@@ -86,6 +86,20 @@ describe("signal declaration", () => {
|
|
|
86
86
|
expect(s.delivery).toBe("once");
|
|
87
87
|
expect(s.name).toBe("order-placed");
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
test("retention is live-only", () => {
|
|
91
|
+
expect(() =>
|
|
92
|
+
// @ts-expect-error retention is live-only
|
|
93
|
+
signal("order-placed", { delivery: "once", retention: { maxCount: 2 } }),
|
|
94
|
+
).toThrow(/retention is only valid/);
|
|
95
|
+
|
|
96
|
+
const live = signal("order-status", {
|
|
97
|
+
delivery: "live",
|
|
98
|
+
optional: true,
|
|
99
|
+
retention: { maxAge: "24h", maxCount: 500 },
|
|
100
|
+
});
|
|
101
|
+
expect(live.retention).toEqual({ maxAge: "24h", maxCount: 500 });
|
|
102
|
+
});
|
|
89
103
|
});
|
|
90
104
|
|
|
91
105
|
for (const { label, driver, setup } of drivers) {
|
|
@@ -184,17 +198,16 @@ for (const { label, driver, setup } of drivers) {
|
|
|
184
198
|
});
|
|
185
199
|
|
|
186
200
|
test("live is client-subscribable", async () => {
|
|
187
|
-
const live = signal("seat-feed", { delivery: "live" });
|
|
201
|
+
const live = signal("seat-feed", { delivery: "live", optional: true });
|
|
188
202
|
const bus = await openBus(driver, [live], setup?.() ?? {});
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
frames.push(payload);
|
|
192
|
-
});
|
|
193
|
-
|
|
203
|
+
const it = bus.live("seat-feed")[Symbol.asyncIterator]();
|
|
204
|
+
const pending = it.next();
|
|
194
205
|
await bus.emit("seat-feed", { seat: "12A" });
|
|
195
206
|
await bus.drain();
|
|
196
|
-
|
|
197
|
-
expect(
|
|
207
|
+
const step = await pending;
|
|
208
|
+
expect(step.done).toBe(false);
|
|
209
|
+
expect(step.value?.payload).toEqual({ seat: "12A" });
|
|
210
|
+
await it.return?.();
|
|
198
211
|
});
|
|
199
212
|
|
|
200
213
|
test("DLQ preserves typed failure reasons per attempt", async () => {
|
package/src/elements/signal.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { signal } from "./signal/declare.ts";
|
|
10
|
-
export type { SignalDecl, SignalOptions } from "./signal/declare.ts";
|
|
10
|
+
export type { SignalDecl, SignalOptions, SignalRetention } from "./signal/declare.ts";
|
|
11
11
|
export type { DeadLetter, SignalFailureReason } from "../drivers/signal-types.ts";
|
|
12
12
|
|
|
13
13
|
export { createSignalRuntime } from "./signal/runtime.ts";
|
|
@@ -63,7 +63,7 @@ export function tier1KeysForReads(
|
|
|
63
63
|
* @param effects - Write flow effects
|
|
64
64
|
*/
|
|
65
65
|
export function resourcesTouchedByWrites(effects: Effects): ResourceRef[] {
|
|
66
|
-
return
|
|
66
|
+
return (effects.writes ?? []).filter((r): r is ResourceRef => isStoreResourceRef(r));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -79,6 +79,11 @@ export interface KvStoreOptions {
|
|
|
79
79
|
* not the cache Redis (`REDIS_URL`). Distinct from Flow `durable`.
|
|
80
80
|
*/
|
|
81
81
|
readonly durable?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Prefix keys with `{tenantId}:` when `gate.auth.tenant` is on.
|
|
84
|
+
* Default `true` then; set `false` for genuinely global namespaces.
|
|
85
|
+
*/
|
|
86
|
+
readonly tenantScoped?: boolean;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/** KV store declaration. */
|
|
@@ -87,6 +92,7 @@ export interface KvStoreDecl extends StoreDeclBase {
|
|
|
87
92
|
readonly ref: `kv:${string}`;
|
|
88
93
|
readonly description?: string;
|
|
89
94
|
readonly durable?: boolean;
|
|
95
|
+
readonly tenantScoped?: boolean;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/** Options for {@link store.files}. */
|
|
@@ -185,6 +191,7 @@ export function kv(name: string, options: KvStoreOptions = {}): KvStoreDecl {
|
|
|
185
191
|
ref: `kv:${name}`,
|
|
186
192
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
187
193
|
...(options.durable === true ? { durable: true } : {}),
|
|
194
|
+
...(options.tenantScoped !== undefined ? { tenantScoped: options.tenantScoped } : {}),
|
|
188
195
|
};
|
|
189
196
|
storeRegistry.push(decl);
|
|
190
197
|
return decl;
|
|
@@ -93,3 +93,32 @@ describe("firstPolicyOrPublic", () => {
|
|
|
93
93
|
expect(firstPolicyOrPublic(["public"])).toBe("public");
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
describe("rlsIdentityFromAuth — tenant GUC", () => {
|
|
98
|
+
test("stamps tenantId only when the field is provided", () => {
|
|
99
|
+
expect(
|
|
100
|
+
rlsIdentityFromAuth({
|
|
101
|
+
userId: "u1",
|
|
102
|
+
scopes: new Set(["member"]),
|
|
103
|
+
gateNames: ["member"],
|
|
104
|
+
tenantId: "acme",
|
|
105
|
+
}),
|
|
106
|
+
).toEqual({
|
|
107
|
+
gate: "member",
|
|
108
|
+
userId: "u1",
|
|
109
|
+
scopes: ["member"],
|
|
110
|
+
tenantId: "acme",
|
|
111
|
+
});
|
|
112
|
+
expect(
|
|
113
|
+
rlsIdentityFromAuth({
|
|
114
|
+
userId: "u1",
|
|
115
|
+
scopes: new Set(["member"]),
|
|
116
|
+
gateNames: ["member"],
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
gate: "member",
|
|
120
|
+
userId: "u1",
|
|
121
|
+
scopes: ["member"],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -66,6 +66,8 @@ export function rlsIdentityFromAuth(input: {
|
|
|
66
66
|
readonly gateNames: readonly string[];
|
|
67
67
|
readonly bypass?: boolean;
|
|
68
68
|
readonly operator?: boolean;
|
|
69
|
+
/** When set (tenancy on), stamp `oke.tenant` — empty string if unresolved. */
|
|
70
|
+
readonly tenantId?: string | null;
|
|
69
71
|
}): RlsIdentity | null {
|
|
70
72
|
if (input.bypass === true || input.operator === true) return null;
|
|
71
73
|
const gate = firstPolicyOrPublic(input.gateNames);
|
|
@@ -75,6 +77,7 @@ export function rlsIdentityFromAuth(input: {
|
|
|
75
77
|
gate,
|
|
76
78
|
userId: input.userId ?? "",
|
|
77
79
|
scopes,
|
|
80
|
+
...(input.tenantId !== undefined ? { tenantId: input.tenantId ?? "" } : {}),
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -239,6 +239,13 @@ export interface StoreRuntime {
|
|
|
239
239
|
* @param ref - `kv:name`
|
|
240
240
|
*/
|
|
241
241
|
kvNamespace(ref: ResourceRef): Promise<KvNamespace>;
|
|
242
|
+
/**
|
|
243
|
+
* Shared primary SQL connection (`sharedSqlConn`).
|
|
244
|
+
*
|
|
245
|
+
* Host auth persist (`oke_api_keys`) and durable KV borrow this same
|
|
246
|
+
* connection — never a second open against `store.sql()` / `DATABASE_URL`.
|
|
247
|
+
*/
|
|
248
|
+
primarySql(): Promise<SqlConnection | undefined>;
|
|
242
249
|
/**
|
|
243
250
|
* Register a declaration (usually done at module load).
|
|
244
251
|
*
|
|
@@ -545,6 +552,13 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
545
552
|
if (!ns) throw new Error(`Unknown kv ref: ${ref}`);
|
|
546
553
|
return ns;
|
|
547
554
|
},
|
|
555
|
+
async primarySql() {
|
|
556
|
+
const sqlDriver = options.drivers.sql;
|
|
557
|
+
if (!sqlDriver) return undefined;
|
|
558
|
+
const firstSql = Object.values(options.sql ?? [])[0];
|
|
559
|
+
const url = options.sqlUrl ?? firstSql?.primary.url;
|
|
560
|
+
return sharedSqlConn(sqlDriver, "primary", { url });
|
|
561
|
+
},
|
|
548
562
|
onWriteEffects(effects) {
|
|
549
563
|
return cache.invalidateFromEffects(effects);
|
|
550
564
|
},
|