@junejs/server 0.1.0-dev.7 → 0.1.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.
@@ -2,12 +2,13 @@ import { Channel, ChannelFactory } from "@junejs/core/agent-config";
2
2
  import { Model, Msg, SessionStore, Tool } from "@junejs/core/agent-runtime";
3
3
  import { Resources } from "@junejs/core/resources";
4
4
  //#region src/agent-durable.d.ts
5
- interface SqlStorageCursor<T = Record<string, unknown>> {
5
+ type SqlStorageValue = ArrayBuffer | string | number | null;
6
+ interface SqlStorageCursor<T extends Record<string, SqlStorageValue> = Record<string, SqlStorageValue>> {
6
7
  toArray(): T[];
7
8
  one(): T;
8
9
  }
9
10
  interface SqlStorage {
10
- exec<T = Record<string, unknown>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
11
+ exec<T extends Record<string, SqlStorageValue> = Record<string, SqlStorageValue>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
11
12
  }
12
13
  interface DurableStorage {
13
14
  sql: SqlStorage;
@@ -69,4 +70,4 @@ declare function durableChannelSurface(getNamespace: () => DurableObjectNamespac
69
70
  waitUntil?: (p: Promise<unknown>) => void;
70
71
  }): (req: Request) => Promise<Response | null>;
71
72
  //#endregion
72
- export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, durableAgentSurface, durableChannelSurface, durableFetch };
73
+ export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, SqlStorageValue, durableAgentSurface, durableChannelSurface, durableFetch };
package/dist/app.js CHANGED
@@ -98,6 +98,8 @@ function createApp({ appDir: appDirInput, config = {} }) {
98
98
  return [...own, ...gen].sort();
99
99
  };
100
100
  const resources = memoizeResources(config.resources);
101
+ let servicesCache;
102
+ const services = config.services ? () => (servicesCache ??= { v: config.services.make(process.env) }).v : void 0;
101
103
  config.dataLayer?.install();
102
104
  let pipelinePromise;
103
105
  const getPipeline = () => pipelinePromise ??= buildPipeline();
@@ -143,6 +145,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
143
145
  routeList: routePaths,
144
146
  earlyHints: config.earlyHints,
145
147
  resources,
148
+ services,
146
149
  notFoundComponent,
147
150
  resolve: async (pathname) => {
148
151
  const match = await matchRouteTree(appDir, pathname, { pageConvention: true }) ?? (hasJuneRoutes ? await matchRouteTree(juneRoutesDir, pathname, { pageConvention: true }) : null);
package/dist/build.js CHANGED
@@ -411,6 +411,12 @@ async function juneBuild(appRoot, options = {}) {
411
411
  const dataLayerModule = fullConfig.dataLayer?.module;
412
412
  if (dataLayerModule) imports.push(`import { installDataLayer } from ${JSON.stringify(dataLayerModule)};`);
413
413
  const dataLayerBoot = dataLayerModule ? "\ninstallDataLayer();\n" : "";
414
+ const servicesModule = fullConfig.services?.module;
415
+ if (servicesModule) {
416
+ const spec = /^[./]/.test(servicesModule) ? importPath(genDir, resolve(appRoot, servicesModule)) : servicesModule;
417
+ imports.push(`import { services as __appServices } from ${JSON.stringify(spec)};`);
418
+ }
419
+ const servicesField = servicesModule ? `\n services: __appServices,` : "";
414
420
  const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
415
421
  ${adapterEntry.imports.join("\n")}
416
422
  ${imports.join("\n")}
@@ -430,7 +436,7 @@ ${loadings.join("\n")}
430
436
  },
431
437
  document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
432
438
  agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
433
- earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}
439
+ earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
434
440
  });
435
441
 
436
442
  ${adapterEntry.wrap("pipeline")}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, staticSite, vercel, workers } from "./adapter.js";
2
2
  import { CreateAppOptions, JuneApp, createApp } from "./app.js";
3
3
  import { R2Bucket, localBlob, r2 } from "./blob.js";
4
- import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-B2V6tKfO.js";
4
+ import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
5
5
  import { WorkerManifest, createWorker } from "./worker.js";
6
6
  import { BuildResult, buildManifest, freezeConfig, generateContent, juneBuild, scanRoutes } from "./build.js";
7
7
  import { loadJuneConfig } from "./config-loader.js";
@@ -22,5 +22,6 @@ import { MatchOptions, RouteMatch, RouteTreeMatch, SegmentMatch, listRoutes, mat
22
22
  import { installAsyncContext } from "./instrumentation.js";
23
23
  import { Negotiated, negotiate } from "./negotiate.js";
24
24
  import { blob, currentServices, db, kv } from "@junejs/db";
25
+ import { ServicesConfig, defineServices } from "@junejs/core/config";
25
26
  import { I18nConfig, LOCALE_COOKIE, LocaleConfig, LocaleMatch, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
26
- export { type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
27
+ export { type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type ServicesConfig, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
package/dist/index.js CHANGED
@@ -22,5 +22,6 @@ import { host } from "./host.js";
22
22
  import { startDevServer } from "./dev.js";
23
23
  import { memoryKv, redisKv } from "./kv.js";
24
24
  import { blob, currentServices, db, kv } from "@junejs/db";
25
+ import { defineServices } from "@junejs/core/config";
25
26
  import { LOCALE_COOKIE, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
26
- export { LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
27
+ export { LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
@@ -31,6 +31,7 @@ type PipelineConfig = {
31
31
  routeList: () => Promise<string[]> | string[];
32
32
  resolve: RouteResolver;
33
33
  resources?: () => Promise<Resources> | Resources;
34
+ services?: () => Promise<unknown> | unknown;
34
35
  earlyHints?: string[];
35
36
  htmlCacheControl?: string;
36
37
  notFoundComponent?: React.ComponentType<{
@@ -1,2 +1,2 @@
1
- import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-B2V6tKfO.js";
1
+ import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
2
2
  export { ExtraHandler, LayoutComponent, LoadingComponent, MiddlewareHandler, Pipeline, PipelineConfig, Resolved, ResolvedResource, ResourceHandler, RouteResolver, createPipeline };
package/dist/pipeline.js CHANGED
@@ -228,7 +228,10 @@ function createPipeline(cfg) {
228
228
  }
229
229
  return { async fetch(request) {
230
230
  await ensureScope();
231
- return runInScope({ resources: cfg.resources ? await cfg.resources() : {} }, () => handleRequest(request));
231
+ return runInScope({
232
+ resources: cfg.resources ? await cfg.resources() : {},
233
+ services: cfg.services ? await cfg.services() : void 0
234
+ }, () => handleRequest(request));
232
235
  } };
233
236
  }
234
237
  //#endregion
package/dist/worker.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-B2V6tKfO.js";
1
+ import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-CCp9idQ4.js";
2
2
  import { BrandedRoute } from "@junejs/core/route";
3
3
  import { AgentConfig } from "@junejs/core/config";
4
4
  import React from "react";
@@ -33,6 +33,7 @@ type WorkerManifest = {
33
33
  }>;
34
34
  extra?: ExtraHandler;
35
35
  resources?: (env?: unknown) => Promise<Resources> | Resources;
36
+ services?: (env?: unknown) => unknown;
36
37
  };
37
38
  declare function createWorker(manifest: WorkerManifest): {
38
39
  fetch(request: Request, env?: unknown): Promise<Response>;
package/dist/worker.js CHANGED
@@ -62,6 +62,8 @@ function createWorker(manifest) {
62
62
  const boundaryFor = (key) => manifest.layoutBoundaries?.[key] ?? null;
63
63
  let currentEnv;
64
64
  const provider = manifest.resources;
65
+ const servicesProvider = manifest.services;
66
+ let servicesCache;
65
67
  const agentSurface = manifest.agent.runtime.enabled && manifest.agentName ? (() => {
66
68
  const route = durableAgentSurface(() => currentEnv?.AGENT, {
67
69
  agentName: manifest.agentName,
@@ -80,6 +82,7 @@ function createWorker(manifest) {
80
82
  notFoundComponent: manifest.notFound,
81
83
  extra: manifest.extra,
82
84
  resources: provider ? () => provider(currentEnv) : void 0,
85
+ services: servicesProvider ? () => (servicesCache ??= { v: servicesProvider(currentEnv) }).v : void 0,
83
86
  resolve: async (pathname) => {
84
87
  const staticDef = manifest.routes[pathname];
85
88
  if (staticDef) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.1.0-dev.7",
3
+ "version": "0.1.1",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -125,8 +125,8 @@
125
125
  "build": "tsdown"
126
126
  },
127
127
  "dependencies": {
128
- "@junejs/core": ">=0.1.0-dev.5",
129
- "@junejs/db": ">=0.0.33-dev.2",
128
+ "@junejs/core": ">=0.1.0",
129
+ "@junejs/db": ">=0.0.33",
130
130
  "@momiji-rs/sparkdown": "^0.0.4",
131
131
  "lightningcss": "^1.27.0",
132
132
  "oxc-parser": "^0.137.0",
@@ -37,12 +37,23 @@ import {
37
37
  import { ensureScope, runInScope } from "@junejs/db";
38
38
 
39
39
  // ── minimal structural Cloudflare surface (no @cloudflare/workers-types dep) ──
40
- export interface SqlStorageCursor<T = Record<string, unknown>> {
40
+ // SqlStorage row values are exactly what workerd's SQLite returns. Mirroring
41
+ // @cloudflare/workers-types' `T extends Record<string, SqlStorageValue>` constraint
42
+ // STRUCTURALLY (not importing it) makes `this.ctx.storage` directly assignable to
43
+ // these interfaces, so a DO shell needs no `as unknown as JuneDoState` cast. The
44
+ // constraint (not just a default) is what lets the two `exec` signatures unify:
45
+ // an unconstrained `T` promises `toArray(): T[]` for arbitrary T, which workerd's
46
+ // cursor — only ever `Record<string, SqlStorageValue>` rows — cannot satisfy.
47
+ export type SqlStorageValue = ArrayBuffer | string | number | null;
48
+ export interface SqlStorageCursor<T extends Record<string, SqlStorageValue> = Record<string, SqlStorageValue>> {
41
49
  toArray(): T[];
42
50
  one(): T;
43
51
  }
44
52
  export interface SqlStorage {
45
- exec<T = Record<string, unknown>>(query: string, ...bindings: unknown[]): SqlStorageCursor<T>;
53
+ exec<T extends Record<string, SqlStorageValue> = Record<string, SqlStorageValue>>(
54
+ query: string,
55
+ ...bindings: unknown[]
56
+ ): SqlStorageCursor<T>;
46
57
  }
47
58
  export interface DurableStorage {
48
59
  sql: SqlStorage;
package/src/app.ts CHANGED
@@ -182,6 +182,15 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
182
182
 
183
183
  const resources = memoizeResources(config.resources);
184
184
 
185
+ // Build the app-services bag once from process.env (dev's env; stable across the
186
+ // process) and hand the pipeline a memoized provider — the dev twin of the worker
187
+ // binding services from its per-isolate env. The `{ v }` box memoizes even a
188
+ // null/undefined result. Absent config → no provider → currentServices() undefined.
189
+ let servicesCache: { v: unknown } | undefined;
190
+ const services = config.services
191
+ ? () => (servicesCache ??= { v: config.services!.make(process.env) }).v
192
+ : undefined;
193
+
185
194
  // Boot the opt-in Tier-3 data layer (e.g. `dataLayer: junoDataLayer()`) once:
186
195
  // its install() wires the ambient `db` (Juno registers its SQL tagger). Explicit
187
196
  // and config-declared — no import-time side-effect, and the framework still never
@@ -245,6 +254,7 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
245
254
  routeList: routePaths,
246
255
  earlyHints: config.earlyHints,
247
256
  resources,
257
+ services,
248
258
  notFoundComponent,
249
259
  resolve: async (pathname) => {
250
260
  const match =
package/src/build.ts CHANGED
@@ -706,6 +706,23 @@ export async function juneBuild(
706
706
  }
707
707
  const dataLayerBoot = dataLayerModule ? "\ninstallDataLayer();\n" : "";
708
708
 
709
+ // App services: import the app's `(env) => services` factory (the named `services`
710
+ // export of the declared module) into the entry and hand it to createWorker, which
711
+ // binds it from the worker env per request — the prod twin of app.ts building it from
712
+ // process.env. Like resources, the module (not the config) is imported, so the config's
713
+ // host-only bits never enter the worker graph. Emitted only when declared.
714
+ //
715
+ // The specifier is rebased like a route import: an app-relative path (`./app/services.ts`)
716
+ // is resolved against appRoot then made relative to the entry's dir (.june), since the
717
+ // generated entry lives there; a bare specifier (a shared package, like dataLayer.module)
718
+ // is used verbatim.
719
+ const servicesModule = fullConfig.services?.module;
720
+ if (servicesModule) {
721
+ const spec = /^[./]/.test(servicesModule) ? importPath(genDir, resolve(appRoot, servicesModule)) : servicesModule;
722
+ imports.push(`import { services as __appServices } from ${JSON.stringify(spec)};`);
723
+ }
724
+ const servicesField = servicesModule ? `\n services: __appServices,` : "";
725
+
709
726
  const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
710
727
  ${adapterEntry.imports.join("\n")}
711
728
  ${imports.join("\n")}
@@ -725,7 +742,7 @@ ${loadings.join("\n")}
725
742
  },
726
743
  document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
727
744
  agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
728
- earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}
745
+ earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
729
746
  });
730
747
 
731
748
  ${adapterEntry.wrap("pipeline")}
package/src/index.ts CHANGED
@@ -31,6 +31,9 @@ export { db, kv, blob } from "@junejs/db";
31
31
  // read the isolate's app-defined bag from any tool or loader. The app builds it from
32
32
  // env at the isolate entry and types it here — `currentServices<AppServices>()`.
33
33
  export { currentServices } from "@junejs/db";
34
+ // Declare that bag for the Worker side in june.config.ts (`services: defineServices(...)`)
35
+ // — the twin of the DO's constructor injection, seeded into loaders/views/actions.
36
+ export { defineServices, type ServicesConfig } from "@junejs/core/config";
34
37
  export { loadJuneConfig } from "./config-loader";
35
38
  export {
36
39
  migrate,
package/src/pipeline.ts CHANGED
@@ -121,6 +121,11 @@ export type PipelineConfig = {
121
121
  // Opened data resources (db/kv/blob) injected onto ctx before load(). A
122
122
  // provider so opening is lazy/memoized; absent → no resources on ctx.
123
123
  resources?: () => Promise<Resources> | Resources;
124
+ // The app-defined services bag, built from the isolate's env (config `services`).
125
+ // A provider like `resources` (lazy/memoized); seeded into the request scope so
126
+ // `currentServices()` resolves in loaders/views/actions. Absent → currentServices()
127
+ // is undefined. This is the Worker-side twin of a Durable Object seeding services.
128
+ services?: () => Promise<unknown> | unknown;
124
129
  earlyHints?: string[];
125
130
  htmlCacheControl?: string;
126
131
  notFoundComponent?: React.ComponentType<{ pathname: string }>;
@@ -578,7 +583,8 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
578
583
  // async-context provider on first request (no static node:* import).
579
584
  await ensureScope();
580
585
  const resources = cfg.resources ? await cfg.resources() : {};
581
- return runInScope({ resources }, () => handleRequest(request));
586
+ const services = cfg.services ? await cfg.services() : undefined;
587
+ return runInScope({ resources, services }, () => handleRequest(request));
582
588
  },
583
589
  };
584
590
  }
package/src/worker.ts CHANGED
@@ -59,6 +59,11 @@ export type WorkerManifest = {
59
59
  // bindings come from env, so the generated entry passes an ENV-AWARE provider
60
60
  // (bindWorkerResources) — the worker threads its env through on each fetch.
61
61
  resources?: (env?: unknown) => Promise<Resources> | Resources;
62
+ // App-defined services (config `services`), built from the worker's env. The build
63
+ // imports the app's `(env) => services` factory into the entry and sets it here; the
64
+ // worker threads its env through and createWorker memoizes it per isolate. Seeded
65
+ // into the request scope so `currentServices()` resolves in loaders/views/actions.
66
+ services?: (env?: unknown) => unknown;
62
67
  };
63
68
 
64
69
  type Compiled = { regex: RegExp; names: string[]; def: BrandedRoute; pattern: string };
@@ -133,6 +138,12 @@ export function createWorker(
133
138
  let currentEnv: unknown;
134
139
  const provider = manifest.resources;
135
140
 
141
+ // App services: the build sets a raw `(env) => services` factory here; memoize it per
142
+ // isolate (env is stable per isolate, like the resources provider). The `{ v }` box
143
+ // caches even a null/undefined result. Absent → currentServices() stays undefined.
144
+ const servicesProvider = manifest.services;
145
+ let servicesCache: { v: unknown } | undefined;
146
+
136
147
  // Durable agent (edge): route the chat endpoint to the per-session DO bound at
137
148
  // env.AGENT. Built only when the app has an agent (manifest.agentName) and the
138
149
  // runtime is enabled; the surface is inert (null → fall through) until env.AGENT
@@ -159,6 +170,7 @@ export function createWorker(
159
170
  notFoundComponent: manifest.notFound,
160
171
  extra: manifest.extra,
161
172
  resources: provider ? () => provider(currentEnv) : undefined,
173
+ services: servicesProvider ? () => (servicesCache ??= { v: servicesProvider(currentEnv) }).v : undefined,
162
174
  resolve: async (pathname): Promise<Resolved | ResolvedResource | null> => {
163
175
  const staticDef = manifest.routes[pathname];
164
176
  if (staticDef) {