@junejs/db 0.0.9

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 ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@junejs/db",
3
+ "version": "0.0.9",
4
+ "description": "June — the agent-native React framework. Ambient data resources (db/kv/blob).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://june.build",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "test": "bun test",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@junejs/core": "0.0.9"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/junebuild/june"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // @junejs/db — the ambient data resources. Decoupled from ctx (which is identity
2
+ // only): `import { db } from "@junejs/db"` and use it in any loader, view, model,
3
+ // or action. The host (@junejs/server) opens the resources and runs each request
4
+ // in the scope these read; this package is the worker-safe seam they cross.
5
+
6
+ export { db, kv, blob, runInScope, ensureScope, type RequestScope } from "./scope";
package/src/scope.ts ADDED
@@ -0,0 +1,93 @@
1
+ // The request scope — how `db` / `kv` / `blob` reach code WITHOUT riding on ctx.
2
+ //
3
+ // ctx is IDENTITY (who is calling: user, session, url, params) — what
4
+ // authorization needs. Resources are CAPABILITY (what tools exist). Mixing them
5
+ // onto one object forced every model/repo/helper to thread ctx just to touch the
6
+ // db (the Express `req.db` anti-pattern). Instead the host pipeline runs each
7
+ // request inside a scope holding the opened resources, and `db`/`kv`/`blob` are
8
+ // ambient accessors that read it — so domain code never sees the request object:
9
+ //
10
+ // import { db } from "@junejs/db";
11
+ // const getUser = (id) => db.get("select * from users where id = ?", [id]);
12
+ //
13
+ // This package is the ambient data seam. It is EDGE-SAFE (no static `node:*`):
14
+ // the async context is AsyncLocalStorage, loaded LAZILY through a non-literal
15
+ // specifier so no bundler resolves a static `node:*` import — workerd registers
16
+ // worker chunks raw and a static node: import breaks module registration
17
+ // (rebuild-plan reminders #1, #4). workerd provides node:async_hooks at runtime
18
+ // via nodejs_compat; dev gets it from Bun/Node. The store propagates across
19
+ // awaits AND to async work spawned inside runInScope, so a streamed loader still
20
+ // sees the db after fetch returns.
21
+
22
+ import type { Resources, JuneDb, JuneKv, JuneBlob } from "@junejs/core/resources";
23
+
24
+ export type RequestScope = { resources: Resources };
25
+
26
+ // The minimal slice of AsyncLocalStorage we use — kept structural so this module
27
+ // never statically names the runtime.
28
+ type AsyncContext<T> = { getStore(): T | undefined; run<R>(store: T, fn: () => R): R };
29
+
30
+ let als: AsyncContext<RequestScope> | null = null;
31
+ let ensuring: Promise<void> | null = null;
32
+
33
+ // Load the async-context provider once (idempotent). Awaited by the pipeline
34
+ // before the first runInScope. Hosts without async_hooks leave `als` null — then
35
+ // runInScope is a pass-through and ambient resources throw the guidance below.
36
+ export async function ensureScope(): Promise<void> {
37
+ if (als) return;
38
+ ensuring ??= (async () => {
39
+ try {
40
+ const specifier = "node:async_hooks";
41
+ const mod = (await import(specifier)) as {
42
+ AsyncLocalStorage: new () => AsyncContext<RequestScope>;
43
+ };
44
+ als = new mod.AsyncLocalStorage();
45
+ } catch {
46
+ /* no async_hooks on this host — ambient resources stay unavailable */
47
+ }
48
+ })();
49
+ await ensuring;
50
+ }
51
+
52
+ // Run `fn` (the whole request) with the scope active. The opened resources are
53
+ // captured at call time; the store survives this returning, so a streaming
54
+ // response that renders later still resolves the same handles.
55
+ export function runInScope<T>(scope: RequestScope, fn: () => T): T {
56
+ return als ? als.run(scope, fn) : fn();
57
+ }
58
+
59
+ function pick<K extends keyof Resources>(name: K): NonNullable<Resources[K]> {
60
+ const store = als?.getStore();
61
+ if (!store) {
62
+ throw new Error(
63
+ `June: \`${name}\` was used outside a request scope. Resources are only ` +
64
+ `available while a request is being handled (a route loader/view or an action).`,
65
+ );
66
+ }
67
+ const resource = store.resources[name];
68
+ if (!resource) {
69
+ throw new Error(
70
+ `June: no \`${name}\` resource is declared. Add \`resources: { ${name}: … }\` ` +
71
+ `to june.config.ts (e.g. \`db: sqlite()\`).`,
72
+ );
73
+ }
74
+ return resource as NonNullable<Resources[K]>;
75
+ }
76
+
77
+ // Forward every property access to the CURRENT request's resource handle. Methods
78
+ // are bound so `this` stays the real handle. Accessing one with no scope, or with
79
+ // the resource undeclared, throws the guidance above instead of a vague TypeError.
80
+ function ambient<T extends object>(name: keyof Resources): T {
81
+ return new Proxy({} as T, {
82
+ get(_t, prop) {
83
+ const resource = pick(name) as unknown as Record<string | symbol, unknown>;
84
+ const value = resource[prop];
85
+ return typeof value === "function" ? (value as (...a: unknown[]) => unknown).bind(resource) : value;
86
+ },
87
+ });
88
+ }
89
+
90
+ // The ambient resources. `import { db } from "@junejs/db"` anywhere.
91
+ export const db: JuneDb = ambient<JuneDb>("db");
92
+ export const kv: JuneKv = ambient<JuneKv>("kv");
93
+ export const blob: JuneBlob = ambient<JuneBlob>("blob");