@junejs/db 0.0.13 → 0.0.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/db",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "June — the agent-native React framework. Ambient data resources (db/kv/blob).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "typecheck": "tsc --noEmit"
17
17
  },
18
18
  "dependencies": {
19
- "@junejs/core": "0.0.13"
19
+ "@junejs/core": "0.0.15"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
package/src/index.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  // or action. The host (@junejs/server) opens the resources and runs each request
4
4
  // in the scope these read; this package is the worker-safe seam they cross.
5
5
 
6
- export { db, kv, blob, runInScope, ensureScope, type RequestScope } from "./scope";
6
+ export { db, kv, blob, runInScope, ensureScope, requestLocal, registerSqlTagger, type RequestScope } from "./scope";
package/src/scope.ts CHANGED
@@ -21,7 +21,11 @@
21
21
 
22
22
  import type { Resources, JuneDb, JuneKv, JuneBlob } from "@junejs/core/resources";
23
23
 
24
- export type RequestScope = { resources: Resources };
24
+ // `locals` is generic per-request state for layers built ON TOP of the resources
25
+ // (e.g. Juno's batch-loader registry). This package never reads it — it just
26
+ // carries it per request so such state is STRUCTURALLY request-scoped and can't be
27
+ // stashed on a long-lived handle. Lazily created by requestLocal().
28
+ export type RequestScope = { resources: Resources; locals?: Map<symbol, unknown> };
25
29
 
26
30
  // The minimal slice of AsyncLocalStorage we use — kept structural so this module
27
31
  // never statically names the runtime.
@@ -56,6 +60,24 @@ export function runInScope<T>(scope: RequestScope, fn: () => T): T {
56
60
  return als ? als.run(scope, fn) : fn();
57
61
  }
58
62
 
63
+ // Per-request local state for a layer built on top of the resources, keyed by a
64
+ // symbol that layer owns. Created once per request and discarded with the scope —
65
+ // so the state is structurally request-scoped and unstashable, the property a
66
+ // long-lived handle (e.g. a module-scope `juno(db)`) can't give. Throws outside a
67
+ // scope, like the ambient resources.
68
+ export function requestLocal<T>(key: symbol, make: () => T): T {
69
+ const store = als?.getStore();
70
+ if (!store) {
71
+ throw new Error(
72
+ "June: request-scoped state was used outside a request scope. It is only " +
73
+ "available while a request is being handled (a route loader/view or an action).",
74
+ );
75
+ }
76
+ const locals = (store.locals ??= new Map<symbol, unknown>());
77
+ if (!locals.has(key)) locals.set(key, make());
78
+ return locals.get(key) as T;
79
+ }
80
+
59
81
  function pick<K extends keyof Resources>(name: K): NonNullable<Resources[K]> {
60
82
  const store = als?.getStore();
61
83
  if (!store) {
@@ -87,7 +109,31 @@ function ambient<T extends object>(name: keyof Resources): T {
87
109
  });
88
110
  }
89
111
 
90
- // The ambient resources. `import { db } from "@junejs/db"` anywhere.
91
- export const db: JuneDb = ambient<JuneDb>("db");
112
+ // A Tier-3 layer (Juno) registers a tagger so the ambient `db` records the tables
113
+ // each RAW query touches — what lets a raw read inside cache() be auto-invalidated
114
+ // instead of going silently stale. @junejs/db stays generic: it only CALLS the
115
+ // registered fn, never imports it, so the dependency direction stays inward
116
+ // (juno → db, never db → juno). Absent (no Juno) → `db` is raw, untagged.
117
+ let sqlTagger: ((sql: string) => void) | null = null;
118
+ export function registerSqlTagger(tag: (sql: string) => void): void {
119
+ sqlTagger = tag;
120
+ }
121
+
122
+ // The ambient resources. `import { db } from "@junejs/db"` anywhere. `db` is the
123
+ // ONE canonical handle (the framework re-exports it); it auto-tags raw query/get/
124
+ // run when a tagger is registered, so installing Juno upgrades it in place — no
125
+ // second `db` to choose between.
126
+ export const db: JuneDb = new Proxy({} as JuneDb, {
127
+ get(_t, prop) {
128
+ const handle = pick("db") as unknown as Record<string | symbol, unknown>;
129
+ const value = handle[prop];
130
+ if (typeof value !== "function") return value;
131
+ const fn = (value as (...a: unknown[]) => unknown).bind(handle);
132
+ if (sqlTagger && (prop === "query" || prop === "get" || prop === "run")) {
133
+ return (sql: string, params?: unknown[]) => (sqlTagger!(sql), fn(sql, params));
134
+ }
135
+ return fn;
136
+ },
137
+ });
92
138
  export const kv: JuneKv = ambient<JuneKv>("kv");
93
139
  export const blob: JuneBlob = ambient<JuneBlob>("blob");