Automatic Server-Side Action Service Injection
Automatic Server-Side Action Service Injection
Written for AI agents. See Log Methodology Note below for details.
Background
Jay Stack server actions (design log 63) provide a type-safe RPC mechanism for client-server communication. Actions defined with makeJayAction/makeJayQuery can declare service dependencies via .withServices().
When an action is called from client-side code, the compiler transforms the call to an HTTP request, and the action router on the server resolves services before executing the handler.
However, when an action is called from server-side code (e.g., in a render phase), services are NOT automatically injected. The action callable passes an empty array:
// jay-action-builder.ts line 186
(input: I): Promise<O> => handler(input, ...([] as unknown as Services)),
Problem
Backend code calling actions directly fails because services aren't injected:
// In renderSlowlyChanging - FAILS
const result = await queryItems({ collectionId, limit: 20 });
// ❌ WixDataService is undefined because services = []
Questions and Answers
Q: Why can't fullstack-component import resolveServices from stack-server-runtime?
- A: Circular dependency.
stack-server-runtimedepends onfullstack-componentfor types.
- A: Circular dependency.
Q: Can we detect at runtime if we're on server vs client?
- A: Yes, but the service resolver function still needs to be available somehow.
Q: Should we transform server-side action calls at build time?
- A: Could work, but adds complexity to the compiler and requires AST analysis of all call sites.
Q: What about using a global resolver?
- A: This is the cleanest approach -
fullstack-componentchecks for a global resolver,stack-server-runtimeregisters it at startup.
- A: This is the cleanest approach -
Design: Global Service Resolver
Mechanism
- Define resolver interface in
fullstack-component:
// jay-action-builder.ts
type ServiceResolver = (markers: any[]) => any[];
declare global {
var __JAY_SERVICE_RESOLVER__: ServiceResolver | undefined;
}
- Register resolver in
stack-server-runtimeat startup:
// services.ts
import { resolveServices } from './services';
globalThis.__JAY_SERVICE_RESOLVER__ = resolveServices;
- Action callable uses resolver if available:
// jay-action-builder.ts - updated callable
(input: I): Promise<O> => {
const resolver = globalThis.__JAY_SERVICE_RESOLVER__;
const services = resolver ? resolver(serviceMarkers) : [];
return handler(input, ...(services as Services));
};
Flow Diagram
Benefits
- Zero changes needed for action calls -
await action(input)just works on server - No circular dependencies - resolver registered at runtime, not imported
- Fail-safe - if resolver not registered, falls back to empty services (shouldn't happen in normal operation)
Trade-offs
- Global state - Uses
globalThis, but this is already common in Node.js server patterns - Runtime check on every call - Negligible overhead (one property lookup)
- Implicit behavior - Service injection happens automatically, matching client-side behavior
Implementation Plan
Phase 1: Core Changes
fullstack-component/lib/jay-action-builder.ts- Add
ServiceResolvertype and global declaration - Update action callable to use global resolver if available
- Keep service markers on the action object for resolver access
- Add
stack-server-runtime/lib/services.ts- Register
resolveServicesonglobalThis.__JAY_SERVICE_RESOLVER__at module load
- Register
Phase 2: Remove runAction
stack-server-runtime/lib/action-registry.ts- Remove
runActionexport
- Remove
Fix packages using runAction:
wix/packages/wix-data- remove runAction imports/callswix/packages/wix-stores- remove runAction imports/callswix/packages/wix-stores-v1- remove runAction imports/calls
Phase 3: Testing
- Add test that verifies server-side action calls work without explicit service resolution
- Verify client-side transforms still work (no change expected)
- Test that resolver registration happens before any action calls
Examples
Before (Current)
// collection-list.ts
import { runAction } from '@jay-framework/stack-server-runtime';
import { queryItems } from '../actions/data-actions';
async function renderSlowlyChanging(props, wixData) {
// Must use runAction for service injection
const result = await runAction(queryItems, {
collectionId,
limit: PAGE_SIZE,
});
}
After (With Enhancement)
// collection-list.ts
import { queryItems } from '../actions/data-actions';
async function renderSlowlyChanging(props, wixData) {
// Just works - services automatically injected on server
const result = await queryItems({
collectionId,
limit: PAGE_SIZE,
});
}
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.