@luckystack/server 0.1.9 → 0.2.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.
@@ -1,206 +1,206 @@
1
- # Runtime Maps Loader
2
-
3
- > Deep specs. Bron: `packages/server/src/runtimeMapsLoader.ts`. Bijgewerkt: 2026-05-20.
4
-
5
- ## Overview
6
-
7
- A `RuntimeMapsProvider` (defined in `@luckystack/core`) is the runtime source of truth for which `apis`, `syncs`, and `functions` exist in the running process. Without one, every `/api/*` and `/sync/*` request silently resolves to `notFound`.
8
-
9
- `@luckystack/server` ships two helpers so consumers no longer need to hand-roll their own `server/prod/runtimeMaps.ts`:
10
-
11
- - `createProdRuntimeMapsProvider(options)` — builds and returns the provider without registering.
12
- - `registerProdRuntimeMapsProvider(options)` — convenience wrapper: builds AND calls `registerRuntimeMapsProvider(provider)`. Most consumers want this.
13
-
14
- In production, the provider loads generated maps via the consumer-supplied `loadGenerated(preset)` callback. In dev it transparently delegates to `@luckystack/devkit`'s in-memory discovery (`devApis` / `devSyncs` / `devFunctions`), which is populated by the devkit watchers.
15
-
16
- Why does the consumer supply `loadGenerated`? Dynamic-import path resolution in ESM is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. Passing a function that calls `import(\`./prod/generatedApis.${preset}\`)` lets the consumer-side module own resolution.
17
-
18
- Preset list resolution (in `resolvePresets`):
19
-
20
- 1. `options.preset` if it is a non-empty string -> `[preset]`.
21
- 2. `options.preset` if it is a non-empty array -> dedup via `Array.from(new Set(...))`.
22
- 3. `getParsedBundles()` from `argv.ts` when non-empty.
23
- 4. Final fallback: `['default']`.
24
-
25
- Multiple presets are loaded in parallel, normalized to `{ apisObject, syncObject, functionsObject }`, then shallow-merged into a single view. Key collisions across presets throw at boot — services must own exactly one preset (see `docs/ARCHITECTURE_PACKAGING.md` §10).
26
-
27
- ## API Reference
28
-
29
- ### `createProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
30
-
31
- **Signature:**
32
-
33
- ```typescript
34
- export interface ProdRuntimeMapsLoaderOptions {
35
- loadGenerated: (preset: string) => Promise<unknown>;
36
- preset?: string | string[];
37
- }
38
-
39
- export const createProdRuntimeMapsProvider = (
40
- options: ProdRuntimeMapsLoaderOptions,
41
- ): RuntimeMapsProvider;
42
- ```
43
-
44
- **Parameters:**
45
-
46
- | Field | Type | Default | Purpose |
47
- | --- | --- | --- | --- |
48
- | `loadGenerated` | `(preset: string) => Promise<unknown>` | required | Dynamic-import callback that resolves the generated maps module for a given preset. The resolved module must have shape `{ apis, syncs, functions }` (what `scripts/generateServerRequests.ts` emits). Called once per preset per process lifetime; the result is cached. |
49
- | `preset` | `string \| string[]` | argv -> `['default']` | Override for the preset list. String -> single-entry array; array -> deduplicated. Skips the argv lookup. |
50
-
51
- **Returns:** a `RuntimeMapsProvider`:
52
-
53
- ```typescript
54
- interface RuntimeMapsProvider {
55
- getRuntimeApiMaps: () => Promise<{ apisObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
56
- getRuntimeSyncMaps: () => Promise<{ syncObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
57
- }
58
- ```
59
-
60
- **Behavior:**
61
-
62
- - Builds two getters (`getRuntimeApiMaps`, `getRuntimeSyncMaps`) that switch on `process.env.NODE_ENV === 'production'`:
63
- - **Production:** lazy-loads + caches the prod maps via `loadProdRuntimeMaps`. Subsequent calls await the same `prodMapsPromise`.
64
- - **Dev:** lazy-imports `@luckystack/devkit` and returns `{ devApis, devSyncs, devFunctions }`. Devkit is excluded from production bundles so this dynamic import is never reached when `NODE_ENV === 'production'`.
65
- - `loadProdRuntimeMaps`:
66
- 1. Resolve presets via `resolvePresets(options)`.
67
- 2. Concurrently `loadGenerated(preset)` for each preset; failures resolve to `null` (caught with `.catch(() => null)`).
68
- 3. For each loaded module, run `normalizeGeneratedModule` (defends against missing or malformed export shape: anything other than an object becomes `{}`).
69
- 4. Merge `apis`, `syncs`, `functions` into the combined view with `mergeInto`, tracking origin per key in three `Map<string, string>`s.
70
- 5. Skipped presets emit `console.warn('[luckystack:runtimeMaps] preset "<name>" failed to load — skipping. ...')`.
71
- 6. If no presets loaded successfully, emit `console.warn('[luckystack:runtimeMaps] no presets resolved (tried: ...). Every api/sync request will return notFound until at least one generated module loads.')` and return empty maps.
72
- - The dev branch caches the devkit module promise so repeated calls share one resolved module.
73
-
74
- **Errors / Edge cases:**
75
-
76
- - Key collision across presets throws synchronously inside `loadProdRuntimeMaps`:
77
-
78
- ```
79
- [luckystack:runtimeMaps] <kind> key collision: "<key>" present in both preset "<previous>" and preset "<current>". Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md §10).
80
- ```
81
-
82
- - Failed `import()` (e.g. preset module not built) is swallowed and logged as a warning; the provider continues with whatever did load.
83
- - A preset module whose default export is not an object normalizes to `{}` for all three maps — no throw, just empty.
84
- - The dev branch dynamic-imports `'@luckystack/devkit'` even when only one of the two getters is invoked; the module promise is cached.
85
-
86
- **Example:**
87
-
88
- ```typescript
89
- import { createProdRuntimeMapsProvider, registerRuntimeMapsProvider } from '@luckystack/server';
90
-
91
- const provider = createProdRuntimeMapsProvider({
92
- loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
93
- preset: 'billing',
94
- });
95
- registerRuntimeMapsProvider(provider);
96
- ```
97
-
98
- ---
99
-
100
- ### `registerProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
101
-
102
- **Signature:**
103
-
104
- ```typescript
105
- export const registerProdRuntimeMapsProvider = (
106
- options: ProdRuntimeMapsLoaderOptions,
107
- ): RuntimeMapsProvider;
108
- ```
109
-
110
- **Parameters:** identical to `createProdRuntimeMapsProvider`.
111
-
112
- **Returns:** the same `RuntimeMapsProvider` instance that was registered.
113
-
114
- **Behavior:**
115
-
116
- - Internally:
117
- ```typescript
118
- const provider = createProdRuntimeMapsProvider(options);
119
- registerRuntimeMapsProvider(provider);
120
- return provider;
121
- ```
122
- - Calling twice replaces the active provider (last-write-wins) — `registerRuntimeMapsProvider` is the registry function.
123
-
124
- **Errors / Edge cases:** same as `createProdRuntimeMapsProvider`. `registerRuntimeMapsProvider` itself does not throw on duplicate calls.
125
-
126
- **Example — direct use:**
127
-
128
- ```typescript
129
- import { registerProdRuntimeMapsProvider } from '@luckystack/server';
130
-
131
- registerProdRuntimeMapsProvider({
132
- loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
133
- });
134
- ```
135
-
136
- **Example — via `createLuckyStackServer.loadGeneratedMaps`:** when the option is supplied, `createLuckyStackServer` calls `registerProdRuntimeMapsProvider` for you before `verifyBootstrap` runs.
137
-
138
- ```typescript
139
- import { createLuckyStackServer } from '@luckystack/server';
140
-
141
- const server = await createLuckyStackServer({
142
- serveFile,
143
- serveFavicon,
144
- loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
145
- runtimeMapsPreset: ['billing', 'vehicles'],
146
- });
147
- await server.listen();
148
- ```
149
-
150
- ## Expected module shape
151
-
152
- `loadGenerated(preset)` must resolve to a module that has these named exports:
153
-
154
- ```typescript
155
- export const apis: Record<string, ApiRouteEntry>;
156
- export const syncs: Record<string, SyncRouteEntry>;
157
- export const functions: Record<string, FunctionEntry>;
158
- ```
159
-
160
- This matches what `scripts/generateServerRequests.ts` emits. Missing exports default to `{}` — useful when a preset has no syncs or functions.
161
-
162
- ## Merge semantics
163
-
164
- `mergeInto(target, source, kind, fromPreset, keyOrigin)`:
165
-
166
- - Iterate every key in `source`.
167
- - If `keyOrigin` already records a different preset for that key -> throw the collision error.
168
- - Otherwise record the origin and shallow-assign `target[key] = source[key]`.
169
-
170
- Three independent origin maps are used so an api named `users/get` and a sync named `users/get` do not collide.
171
-
172
- ## Failure modes
173
-
174
- | Situation | Behavior |
175
- | --- | --- |
176
- | `loadGenerated(preset)` rejects | Warn (`preset "<name>" failed to load — skipping`); continue with other presets. |
177
- | All presets failed | Warn (`no presets resolved (tried: ...)`); return empty maps; every request returns `notFound`. |
178
- | Preset module has wrong shape | Normalize to `{}`. Same effect: requests for those routes return `notFound`. |
179
- | Key collision across presets | Throw at boot — services must own exactly one preset. |
180
- | Provider is never registered at all | `verifyBootstrap` hard-fails in production, loud-warns in dev. |
181
-
182
- ## Dev vs prod branch
183
-
184
- | Environment | Source of maps |
185
- | --- | --- |
186
- | `NODE_ENV === 'production'` | `loadGenerated(preset)` for each resolved preset; cached `prodMapsPromise`. |
187
- | Anything else | Dynamic import of `'@luckystack/devkit'` -> `{ devApis, devSyncs, devFunctions }`. Devkit watchers keep these maps live. |
188
-
189
- The dynamic import to `'@luckystack/devkit'` is intentionally written as `import('@luckystack/devkit')` and not statically referenced, so bundlers tree-shake it out of production builds.
190
-
191
- ## Config keys
192
-
193
- | Source | Key | Effect |
194
- | --- | --- | --- |
195
- | env | `NODE_ENV` | Switches dev (devkit) vs prod (`loadGenerated`) branch. |
196
- | argv | `<bundles>` | Preset list when `options.preset` is omitted. |
197
- | options | `options.preset` | Overrides argv-derived preset list. |
198
- | options | `options.loadGenerated` | Required prod loader callback. |
199
-
200
- ## Related
201
-
202
- - Function INDEX: `packages/server/CLAUDE.md`
203
- - Argv parsing: `packages/server/docs/argv-parsing.md`
204
- - Create server: `packages/server/docs/create-server.md`
205
- - Architecture: `docs/ARCHITECTURE_PACKAGING.md` (§10 preset bundles + multi-service builds)
206
- - README: `packages/server/README.md`
1
+ # Runtime Maps Loader
2
+
3
+ > Deep specs. Bron: `packages/server/src/runtimeMapsLoader.ts`. Bijgewerkt: 2026-05-20.
4
+
5
+ ## Overview
6
+
7
+ A `RuntimeMapsProvider` (defined in `@luckystack/core`) is the runtime source of truth for which `apis`, `syncs`, and `functions` exist in the running process. Without one, every `/api/*` and `/sync/*` request silently resolves to `notFound`.
8
+
9
+ `@luckystack/server` ships two helpers so consumers no longer need to hand-roll their own `server/prod/runtimeMaps.ts`:
10
+
11
+ - `createProdRuntimeMapsProvider(options)` — builds and returns the provider without registering.
12
+ - `registerProdRuntimeMapsProvider(options)` — convenience wrapper: builds AND calls `registerRuntimeMapsProvider(provider)`. Most consumers want this.
13
+
14
+ In production, the provider loads generated maps via the consumer-supplied `loadGenerated(preset)` callback. In dev it transparently delegates to `@luckystack/devkit`'s in-memory discovery (`devApis` / `devSyncs` / `devFunctions`), which is populated by the devkit watchers.
15
+
16
+ Why does the consumer supply `loadGenerated`? Dynamic-import path resolution in ESM is module-scoped — the framework cannot resolve a relative path on the consumer's behalf. Passing a function that calls `import(\`./prod/generatedApis.${preset}\`)` lets the consumer-side module own resolution.
17
+
18
+ Preset list resolution (in `resolvePresets`):
19
+
20
+ 1. `options.preset` if it is a non-empty string -> `[preset]`.
21
+ 2. `options.preset` if it is a non-empty array -> dedup via `Array.from(new Set(...))`.
22
+ 3. `getParsedBundles()` from `argv.ts` when non-empty.
23
+ 4. Final fallback: `['default']`.
24
+
25
+ Multiple presets are loaded in parallel, normalized to `{ apisObject, syncObject, functionsObject }`, then shallow-merged into a single view. Key collisions across presets throw at boot — services must own exactly one preset (see `docs/ARCHITECTURE_PACKAGING.md` §10).
26
+
27
+ ## API Reference
28
+
29
+ ### `createProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
30
+
31
+ **Signature:**
32
+
33
+ ```typescript
34
+ export interface ProdRuntimeMapsLoaderOptions {
35
+ loadGenerated: (preset: string) => Promise<unknown>;
36
+ preset?: string | string[];
37
+ }
38
+
39
+ export const createProdRuntimeMapsProvider = (
40
+ options: ProdRuntimeMapsLoaderOptions,
41
+ ): RuntimeMapsProvider;
42
+ ```
43
+
44
+ **Parameters:**
45
+
46
+ | Field | Type | Default | Purpose |
47
+ | --- | --- | --- | --- |
48
+ | `loadGenerated` | `(preset: string) => Promise<unknown>` | required | Dynamic-import callback that resolves the generated maps module for a given preset. The resolved module must have shape `{ apis, syncs, functions }` (what `scripts/generateServerRequests.ts` emits). Called once per preset per process lifetime; the result is cached. |
49
+ | `preset` | `string \| string[]` | argv -> `['default']` | Override for the preset list. String -> single-entry array; array -> deduplicated. Skips the argv lookup. |
50
+
51
+ **Returns:** a `RuntimeMapsProvider`:
52
+
53
+ ```typescript
54
+ interface RuntimeMapsProvider {
55
+ getRuntimeApiMaps: () => Promise<{ apisObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
56
+ getRuntimeSyncMaps: () => Promise<{ syncObject: Record<string, unknown>; functionsObject: Record<string, unknown> }>;
57
+ }
58
+ ```
59
+
60
+ **Behavior:**
61
+
62
+ - Builds two getters (`getRuntimeApiMaps`, `getRuntimeSyncMaps`) that switch on `process.env.NODE_ENV === 'production'`:
63
+ - **Production:** lazy-loads + caches the prod maps via `loadProdRuntimeMaps`. Subsequent calls await the same `prodMapsPromise`.
64
+ - **Dev:** lazy-imports `@luckystack/devkit` and returns `{ devApis, devSyncs, devFunctions }`. Devkit is excluded from production bundles so this dynamic import is never reached when `NODE_ENV === 'production'`.
65
+ - `loadProdRuntimeMaps`:
66
+ 1. Resolve presets via `resolvePresets(options)`.
67
+ 2. Concurrently `loadGenerated(preset)` for each preset; failures resolve to `null` (caught with `.catch(() => null)`).
68
+ 3. For each loaded module, run `normalizeGeneratedModule` (defends against missing or malformed export shape: anything other than an object becomes `{}`).
69
+ 4. Merge `apis`, `syncs`, `functions` into the combined view with `mergeInto`, tracking origin per key in three `Map<string, string>`s.
70
+ 5. Skipped presets emit `console.warn('[luckystack:runtimeMaps] preset "<name>" failed to load — skipping. ...')`.
71
+ 6. If no presets loaded successfully, emit `console.warn('[luckystack:runtimeMaps] no presets resolved (tried: ...). Every api/sync request will return notFound until at least one generated module loads.')` and return empty maps.
72
+ - The dev branch caches the devkit module promise so repeated calls share one resolved module.
73
+
74
+ **Errors / Edge cases:**
75
+
76
+ - Key collision across presets throws synchronously inside `loadProdRuntimeMaps`:
77
+
78
+ ```
79
+ [luckystack:runtimeMaps] <kind> key collision: "<key>" present in both preset "<previous>" and preset "<current>". Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md §10).
80
+ ```
81
+
82
+ - Failed `import()` (e.g. preset module not built) is swallowed and logged as a warning; the provider continues with whatever did load.
83
+ - A preset module whose default export is not an object normalizes to `{}` for all three maps — no throw, just empty.
84
+ - The dev branch dynamic-imports `'@luckystack/devkit'` even when only one of the two getters is invoked; the module promise is cached.
85
+
86
+ **Example:**
87
+
88
+ ```typescript
89
+ import { createProdRuntimeMapsProvider, registerRuntimeMapsProvider } from '@luckystack/server';
90
+
91
+ const provider = createProdRuntimeMapsProvider({
92
+ loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
93
+ preset: 'billing',
94
+ });
95
+ registerRuntimeMapsProvider(provider);
96
+ ```
97
+
98
+ ---
99
+
100
+ ### `registerProdRuntimeMapsProvider(options: ProdRuntimeMapsLoaderOptions): RuntimeMapsProvider`
101
+
102
+ **Signature:**
103
+
104
+ ```typescript
105
+ export const registerProdRuntimeMapsProvider = (
106
+ options: ProdRuntimeMapsLoaderOptions,
107
+ ): RuntimeMapsProvider;
108
+ ```
109
+
110
+ **Parameters:** identical to `createProdRuntimeMapsProvider`.
111
+
112
+ **Returns:** the same `RuntimeMapsProvider` instance that was registered.
113
+
114
+ **Behavior:**
115
+
116
+ - Internally:
117
+ ```typescript
118
+ const provider = createProdRuntimeMapsProvider(options);
119
+ registerRuntimeMapsProvider(provider);
120
+ return provider;
121
+ ```
122
+ - Calling twice replaces the active provider (last-write-wins) — `registerRuntimeMapsProvider` is the registry function.
123
+
124
+ **Errors / Edge cases:** same as `createProdRuntimeMapsProvider`. `registerRuntimeMapsProvider` itself does not throw on duplicate calls.
125
+
126
+ **Example — direct use:**
127
+
128
+ ```typescript
129
+ import { registerProdRuntimeMapsProvider } from '@luckystack/server';
130
+
131
+ registerProdRuntimeMapsProvider({
132
+ loadGenerated: (preset) => import(`./prod/generatedApis.${preset}`),
133
+ });
134
+ ```
135
+
136
+ **Example — via `createLuckyStackServer.loadGeneratedMaps`:** when the option is supplied, `createLuckyStackServer` calls `registerProdRuntimeMapsProvider` for you before `verifyBootstrap` runs.
137
+
138
+ ```typescript
139
+ import { createLuckyStackServer } from '@luckystack/server';
140
+
141
+ const server = await createLuckyStackServer({
142
+ serveFile,
143
+ serveFavicon,
144
+ loadGeneratedMaps: (preset) => import(`./prod/generatedApis.${preset}`),
145
+ runtimeMapsPreset: ['billing', 'vehicles'],
146
+ });
147
+ await server.listen();
148
+ ```
149
+
150
+ ## Expected module shape
151
+
152
+ `loadGenerated(preset)` must resolve to a module that has these named exports:
153
+
154
+ ```typescript
155
+ export const apis: Record<string, ApiRouteEntry>;
156
+ export const syncs: Record<string, SyncRouteEntry>;
157
+ export const functions: Record<string, FunctionEntry>;
158
+ ```
159
+
160
+ This matches what `scripts/generateServerRequests.ts` emits. Missing exports default to `{}` — useful when a preset has no syncs or functions.
161
+
162
+ ## Merge semantics
163
+
164
+ `mergeInto(target, source, kind, fromPreset, keyOrigin)`:
165
+
166
+ - Iterate every key in `source`.
167
+ - If `keyOrigin` already records a different preset for that key -> throw the collision error.
168
+ - Otherwise record the origin and shallow-assign `target[key] = source[key]`.
169
+
170
+ Three independent origin maps are used so an api named `users/get` and a sync named `users/get` do not collide.
171
+
172
+ ## Failure modes
173
+
174
+ | Situation | Behavior |
175
+ | --- | --- |
176
+ | `loadGenerated(preset)` rejects | Warn (`preset "<name>" failed to load — skipping`); continue with other presets. |
177
+ | All presets failed | Warn (`no presets resolved (tried: ...)`); return empty maps; every request returns `notFound`. |
178
+ | Preset module has wrong shape | Normalize to `{}`. Same effect: requests for those routes return `notFound`. |
179
+ | Key collision across presets | Throw at boot — services must own exactly one preset. |
180
+ | Provider is never registered at all | `verifyBootstrap` hard-fails in production, loud-warns in dev. |
181
+
182
+ ## Dev vs prod branch
183
+
184
+ | Environment | Source of maps |
185
+ | --- | --- |
186
+ | `NODE_ENV === 'production'` | `loadGenerated(preset)` for each resolved preset; cached `prodMapsPromise`. |
187
+ | Anything else | Dynamic import of `'@luckystack/devkit'` -> `{ devApis, devSyncs, devFunctions }`. Devkit watchers keep these maps live. |
188
+
189
+ The dynamic import to `'@luckystack/devkit'` is intentionally written as `import('@luckystack/devkit')` and not statically referenced, so bundlers tree-shake it out of production builds.
190
+
191
+ ## Config keys
192
+
193
+ | Source | Key | Effect |
194
+ | --- | --- | --- |
195
+ | env | `NODE_ENV` | Switches dev (devkit) vs prod (`loadGenerated`) branch. |
196
+ | argv | `<bundles>` | Preset list when `options.preset` is omitted. |
197
+ | options | `options.preset` | Overrides argv-derived preset list. |
198
+ | options | `options.loadGenerated` | Required prod loader callback. |
199
+
200
+ ## Related
201
+
202
+ - Function INDEX: `packages/server/CLAUDE.md`
203
+ - Argv parsing: `packages/server/docs/argv-parsing.md`
204
+ - Create server: `packages/server/docs/create-server.md`
205
+ - Architecture: `docs/ARCHITECTURE_PACKAGING.md` (§10 preset bundles + multi-service builds)
206
+ - README: `packages/server/README.md`