okengine 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/store.mdx +20 -0
  3. package/site/content/docs/providers/index.mdx +1 -0
  4. package/site/content/docs/recipes/dragonfly.mdx +4 -3
  5. package/site/content/docs/recipes/redis.mdx +4 -5
  6. package/site/content/docs/recipes/valkey.mdx +5 -3
  7. package/site/content/docs/reference/configuration.mdx +3 -1
  8. package/site/content/docs/reference/environment-variables.mdx +5 -5
  9. package/src/cli/load-config.images.test.ts +1 -0
  10. package/src/compiler/extract.test.ts +14 -0
  11. package/src/compiler/extract.ts +1 -0
  12. package/src/console/ui-next/dist/assets/{flows-page-C_Tas1E1.js → flows-page-Dg8CTE29.js} +1 -1
  13. package/src/console/ui-next/dist/assets/{index-DX248G39.js → index-Bp-R7jtM.js} +3 -3
  14. package/src/console/ui-next/dist/assets/{observability-page-BwVS5Jvm.js → observability-page-DAnpEaq1.js} +1 -1
  15. package/src/console/ui-next/dist/assets/{units-page-C5KOI7qG.js → units-page-BtQ0bqMe.js} +1 -1
  16. package/src/console/ui-next/dist/assets/{vault-page-CSOYMHhO.js → vault-page-Ca-MvcmJ.js} +1 -1
  17. package/src/console/ui-next/dist/index.html +1 -1
  18. package/src/docker/helpers.ts +28 -0
  19. package/src/docker/recipes/dragonfly.ts +3 -6
  20. package/src/docker/recipes/redis.ts +2 -6
  21. package/src/docker/recipes/valkey.ts +2 -6
  22. package/src/drivers/types.ts +1 -1
  23. package/src/elements/gate/runtime.ts +3 -3
  24. package/src/elements/store/declare.ts +7 -0
  25. package/src/elements/store/kv-sql.test.ts +86 -0
  26. package/src/elements/store/kv-sql.ts +178 -0
  27. package/src/elements/store/runtime.ts +35 -9
  28. package/src/elements/store.test.ts +2 -0
  29. package/src/kernel/boot-bind/store.test.ts +150 -1
  30. package/src/kernel/boot-bind/store.ts +66 -1
  31. package/src/manifest/diff.test.ts +13 -0
  32. package/src/manifest/diff.ts +15 -0
  33. package/src/manifest/types.ts +5 -0
@@ -34,12 +34,20 @@ import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runt
34
34
  * @param docker - Prefer compose URLs when opening postgres/redis
35
35
  */
36
36
  let filesFsWarned = false;
37
+ let kvCacheShapedWarned = false;
38
+ let kvMemoryDurableWarned = false;
37
39
 
38
40
  /** Test helper — reset the one-shot `fs` multi-instance warn. */
39
41
  export function resetFilesFsWarnForTests(): void {
40
42
  filesFsWarned = false;
41
43
  }
42
44
 
45
+ /** Test helper — reset durable-KV boot warns. */
46
+ export function resetKvDurableWarnsForTests(): void {
47
+ kvCacheShapedWarned = false;
48
+ kvMemoryDurableWarned = false;
49
+ }
50
+
43
51
  /**
44
52
  * Warn once when `drivers.store.files` is `fs` — per-process (or per-pod)
45
53
  * filesystem visibility under horizontal scale.
@@ -54,6 +62,39 @@ function warnFilesFsMultiInstance(): void {
54
62
  );
55
63
  }
56
64
 
65
+ /**
66
+ * Warn once: default KV namespaces are cache-shaped (no AOF) unless
67
+ * `{ durable: true }` persists them in SQL (`oke_kv`).
68
+ */
69
+ function warnKvCacheShaped(names: readonly string[]): void {
70
+ if (kvCacheShapedWarned) return;
71
+ kvCacheShapedWarned = true;
72
+ const listed = names
73
+ .slice(0, 4)
74
+ .map((n) => `"${n}"`)
75
+ .join(", ");
76
+ const extra = names.length > 4 ? ` (and ${String(names.length - 4)} more)` : "";
77
+ emitBootWarn(
78
+ `oke boot: store.kv ${listed}${extra} is cache-shaped — a Redis recreate drops keys ` +
79
+ "(no AOF). Declare `{ durable: true }` on a separate namespace to persist in " +
80
+ "your SQL database (`oke_kv`).",
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Warn once: in-process memory SQL cannot honor `{ durable: true }`.
86
+ */
87
+ function warnKvMemoryDurable(names: readonly string[]): void {
88
+ if (kvMemoryDurableWarned) return;
89
+ kvMemoryDurableWarned = true;
90
+ const listed = names.map((n) => `"${n}"`).join(", ");
91
+ emitBootWarn(
92
+ `oke boot: store.kv ${listed} declared { durable: true } but drivers.store.sql is ` +
93
+ '"memory" — keys are process-local and vanish on restart. Use drivers.store.sql ' +
94
+ "postgres with DATABASE_URL.",
95
+ );
96
+ }
97
+
57
98
  export function bindStore(
58
99
  options: BootOptions,
59
100
  env: ConfigEnv,
@@ -65,9 +106,28 @@ export function bindStore(
65
106
  const filesId = resolveFilesDriverId(options, env, docker);
66
107
  const indexId = resolveIndexDriverId(options, env, docker);
67
108
  if (filesId === "fs") warnFilesFsMultiInstance();
109
+ const kvDecls = (options.stores ?? []).filter(isKvDecl);
110
+ const durableKvDecls = kvDecls.filter((d) => d.durable === true);
111
+ const cacheKvDecls = kvDecls.filter((d) => d.durable !== true);
112
+ if (durableKvDecls.length > 0 && sqlId === "postgres") {
113
+ const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL;
114
+ if (!url) {
115
+ throw new Error(
116
+ docker
117
+ ? "oke boot: durable store.kv needs DATABASE_URL (did `oke dev` write .env.local?)"
118
+ : "oke boot: durable store.kv needs DATABASE_URL",
119
+ );
120
+ }
121
+ }
68
122
  const sqlUrl = sqlUrlFor(sqlId, docker, env);
69
123
  const kvUrl = kvUrlFor(kvId, docker);
70
124
  const filesRoot = filesRootFor(filesId);
125
+ if (kvId === "redis" && env !== "test" && cacheKvDecls.length > 0) {
126
+ warnKvCacheShaped(cacheKvDecls.map((d) => d.name));
127
+ }
128
+ if (sqlId === "memory" && env !== "test" && durableKvDecls.length > 0) {
129
+ warnKvMemoryDurable(durableKvDecls.map((d) => d.name));
130
+ }
71
131
 
72
132
  const sqlBindings: Record<string, { name: string; primary: { url: string } }> = {};
73
133
  const kvBindings: Record<string, { url?: string }> = {};
@@ -81,7 +141,11 @@ export function bindStore(
81
141
  primary: { url: sqlUrl },
82
142
  };
83
143
  } else if (isKvDecl(decl)) {
84
- kvBindings[decl.name] = kvUrl !== undefined ? { url: kvUrl } : {};
144
+ if (decl.durable === true) {
145
+ kvBindings[decl.name] = {};
146
+ } else {
147
+ kvBindings[decl.name] = kvUrl !== undefined ? { url: kvUrl } : {};
148
+ }
85
149
  } else if (isFilesDecl(decl)) {
86
150
  filesBindings[decl.name] = filesRoot !== undefined ? { root: filesRoot } : {};
87
151
  } else if (isIndexDecl(decl)) {
@@ -114,6 +178,7 @@ export function bindStore(
114
178
  index: indexBindings,
115
179
  now,
116
180
  domainDdl,
181
+ sqlUrl,
117
182
  });
118
183
  for (const decl of options.stores ?? []) {
119
184
  store.register?.(decl);
@@ -309,6 +309,19 @@ describe("diffManifest — additional branches", () => {
309
309
  expect(hasCategory(result.changes, "contract-breaking", "facet")).toBe(true);
310
310
  });
311
311
 
312
+ test("store.kv durable on is effect-widening; off is contract-breaking", async () => {
313
+ const before = await loadBase();
314
+ before.stores!.db!.facet = "kv";
315
+ before.stores!.db!.namespaces = ["cache"];
316
+ delete before.stores!.db!.tables;
317
+ const on = clone(before);
318
+ on.stores!.db!.durable = true;
319
+ expect(hasCategory(diffManifest(before, on).changes, "effect-widening", "durable")).toBe(true);
320
+ const off = clone(on);
321
+ off.stores!.db!.durable = false;
322
+ expect(hasCategory(diffManifest(on, off).changes, "contract-breaking", "durable")).toBe(true);
323
+ });
324
+
312
325
  test("clocks, vault, channels, journeys, drivers, i18n, topology, images", async () => {
313
326
  const before = await loadBase();
314
327
  const after = clone(before);
@@ -779,6 +779,21 @@ function diffStore(before: Store, after: Store, path: string, out: ManifestChang
779
779
  diffStringSet(before.namespaces, after.namespaces, `${path}/namespaces`, out);
780
780
  diffStringSet(before.buckets, after.buckets, `${path}/buckets`, out);
781
781
  diffStringSet(before.indexes, after.indexes, `${path}/indexes`, out);
782
+ if (
783
+ before.durable !== after.durable &&
784
+ (before.durable !== undefined || after.durable !== undefined)
785
+ ) {
786
+ out.push(
787
+ change(
788
+ `${path}/durable`,
789
+ after.durable === true ? "effect-widening" : "contract-breaking",
790
+ "changed",
791
+ before.durable,
792
+ after.durable,
793
+ `store durable ${String(before.durable)} → ${String(after.durable)}`,
794
+ ),
795
+ );
796
+ }
782
797
  if (!deepEqual(before.classifications, after.classifications)) {
783
798
  out.push(
784
799
  change(
@@ -234,6 +234,11 @@ export interface Store {
234
234
  buckets?: string[];
235
235
  indexes?: string[];
236
236
  classifications?: Record<string, ClassificationValue>;
237
+ /**
238
+ * KV only — namespace persists in SQL (`oke_kv`), not the cache Redis.
239
+ * Distinct from Flow `durable` (journaled steps).
240
+ */
241
+ durable?: boolean;
237
242
  }
238
243
 
239
244
  /** Named clock / schedule. */