@@ -2406,7 +2406,11 @@ export default class Serve extends Command {
24062406 // The remote driver self-registers on import; import it dynamically so it
24072407 // works in BOTH config-boot and compiled-artifact mode. Open-core ships
24082408 // only the in-memory driver — remote drivers (e.g. redis) come from the EE
2409- // distribution; if absent we fall back to the in-memory cluster.
2409+ // distribution. An absent driver does NOT fall back to the in-memory
2410+ // cluster: `clusterConfig` still names it and `defineCluster()` raises
2411+ // its documented error (cluster.mdx §8.1). The only documented downgrade
2412+ // here is a multi-node GATE DENIAL, below. (#13330 — this sentence said
2413+ // the opposite for as long as the silent catch below agreed with it.)
24102414 let clusterConfig : { driver : string ; url ?: string } | undefined ;
24112415 // The gate's verdict, held for the operator-facing telemetry emitted near
24122416 // the end of boot (#12667). The gate is consulted exactly once per
@@ -2432,9 +2436,15 @@ export default class Serve extends Command {
24322436 // '@objectstack/service-cluster'` and took the whole boot down — while
24332437 // app-side code loaded the very same package fine.
24342438 const __clusterPkg : string = '@objectstack/service-cluster' ;
2435- const { checkMultiNodeAllowed } = ( await importFromHost ( __clusterPkg ) ) as {
2439+ // The whole namespace, not just the gate: the DRIVER REGISTRY read
2440+ // further down has to come from this same module instance, because
2441+ // that is the instance `defineCluster()` consults (#13330).
2442+ const __clusterModule = ( await importFromHost ( __clusterPkg ) ) as {
24362443 checkMultiNodeAllowed : ( requested ?: number ) => MultiNodeGateVerdict ;
2444+ /** Optional: an app on a pre-#13330 `service-cluster` does not have it. */
2445+ listClusterDrivers ?: ( ) => string [ ] ;
24372446 } ;
2447+ const { checkMultiNodeAllowed } = __clusterModule ;
24382448 // Ask the gate about the topology the operator actually DECLARED.
24392449 // Calling zero-arg leaves `requested` undefined, which a cap-aware gate
24402450 // has nothing to clamp against — so the licensed-overflow verdict was
@@ -2467,12 +2477,97 @@ export default class Serve extends Command {
24672477 const __capAdvisory = formatMultiNodeCapAdvisory ( __gate ) ;
24682478 if ( __capAdvisory ) console . warn ( __capAdvisory ) ;
24692479 // Same host-anchored resolution as the gate above — the shipped
2470- // drivers (`-redis`, `-postgres`, …) are app-declared too. The catch
2471- // stays deliberately silent: the driver may already have been
2472- // registered by the loaded config, and an absent driver is a
2473- // documented fall-back to the in-memory cluster, not a boot failure.
2474- try { await importFromHost ( `@objectstack/service-cluster-${ __clusterDriver } ` ) ; }
2475- catch { /* may already be registered by the loaded config */ }
2480+ // drivers (`-redis`, `-postgres`, …) are app-declared too.
2481+ //
2482+ // ── Why this is no longer a silent catch (#13330) ────────────────
2483+ //
2484+ // A driver package's entire contract is a load-time SIDE EFFECT:
2485+ // `registerClusterDriver('<driver>', …)` into the module-scope
2486+ // registry of `@objectstack/service-cluster`, which `defineCluster()`
2487+ // reads two statements below. Whether that side effect landed is a
2488+ // fact about THIS process, so it is read here rather than assumed.
2489+ //
2490+ // It used to be assumed. The catch was silent on two stated grounds —
2491+ // "may already be registered by the loaded config" and "an absent
2492+ // driver is a documented fall-back to the in-memory cluster" — and a
2493+ // single EE boot measured both wrong at once:
2494+ //
2495+ // • the load SUCCEEDED and the registration was invisible. The
2496+ // declared leg of `importFromHost` resolved with CommonJS
2497+ // semantics, so the driver ran as its `.cjs` build and registered
2498+ // into a SECOND instance of the registry, while the ESM Runtime
2499+ // read the first. Fixed at the seam (`@objectstack/types/node`);
2500+ // this reading is what makes any residual split audible instead
2501+ // of arriving as "not registered" one line later.
2502+ // • an absent driver falls back to nothing HERE — `clusterConfig`
2503+ // below names the driver either way, so `defineCluster()` raises
2504+ // its documented error (cluster.mdx §8.1). That is left exactly
2505+ // as it is: downgrading to in-memory instead would boot a silent
2506+ // single node for an operator who explicitly asked for a remote
2507+ // driver, and on the multi-replica deployments this matters for,
2508+ // the ADR-0010 split-brain guard throws on that downgrade anyway.
2509+ // What changes is only that the reason is no longer swallowed.
2510+ //
2511+ // Nothing below throws: every branch is a diagnosis printed ahead of
2512+ // behaviour that is unchanged.
2513+ let __driverLoadError : unknown ;
2514+ try {
2515+ await importFromHost ( `@objectstack/service-cluster-${ __clusterDriver } ` ) ;
2516+ } catch ( err ) {
2517+ __driverLoadError = err ;
2518+ }
2519+ // `undefined` ⇒ the app's `@objectstack/service-cluster` predates
2520+ // `listClusterDrivers`, so the registry cannot be read from here.
2521+ // That is NOT MEASURED — it is not "registered" and not "missing",
2522+ // and no branch below claims either.
2523+ const __registeredDrivers =
2524+ typeof __clusterModule . listClusterDrivers === 'function'
2525+ ? __clusterModule . listClusterDrivers ( )
2526+ : undefined ;
2527+ const __driverVisible =
2528+ __registeredDrivers === undefined
2529+ ? undefined
2530+ : __registeredDrivers . indexOf ( __clusterDriver ) >= 0 ;
2531+ if ( __driverVisible !== true ) {
2532+ if ( __driverLoadError !== undefined ) {
2533+ // Resolution failures carry a kind and are already worded for an
2534+ // operator by `createHostImporter`; anything else RESOLVED and
2535+ // then crashed while evaluating. Swallowing the second is how a
2536+ // driver with a broken dependency reported as "not registered",
2537+ // sending operators to look for a package already installed.
2538+ const __kind = hostImportFailureKind ( __driverLoadError ) ;
2539+ if ( __kind !== undefined ) {
2540+ console . warn (
2541+ `[cluster] driver "${ __clusterDriver } " was requested but could not be ` +
2542+ `loaded (${ __kind } ):\n${
2543+ __driverLoadError instanceof Error
2544+ ? __driverLoadError . message
2545+ : String ( __driverLoadError )
2546+ } `,
2547+ ) ;
2548+ } else {
2549+ console . warn (
2550+ `[cluster] driver "${ __clusterDriver } " resolved but threw while loading — ` +
2551+ `this is the driver package's own failure, not a missing package:` ,
2552+ __driverLoadError ,
2553+ ) ;
2554+ }
2555+ } else if ( __driverVisible === false ) {
2556+ // Loaded cleanly and still not in the registry: two live
2557+ // instances of `@objectstack/service-cluster` in one process,
2558+ // which is a PHYSICAL-copy split no resolver condition can merge.
2559+ console . warn (
2560+ `[cluster] driver "${ __clusterDriver } " loaded but did not register: ` +
2561+ `@objectstack/service-cluster-${ __clusterDriver } evaluated without error, yet the ` +
2562+ `registry this boot reads holds [${ __registeredDrivers ?. join ( ', ' ) || 'nothing' } ]. ` +
2563+ `Two instances of @objectstack/service-cluster are live in this process and the ` +
2564+ `driver registered into the other one — look for two physical copies (a version ` +
2565+ `skew between the app and the framework, or a bundled one). Importing ` +
2566+ `"@objectstack/service-cluster-${ __clusterDriver } " from objectstack.config.ts ` +
2567+ `registers into the instance the Runtime reads.` ,
2568+ ) ;
2569+ }
2570+ }
24762571 clusterConfig = { driver : __clusterDriver , url : process . env . OS_REDIS_URL } ;
24772572 }
24782573 }
0 commit comments