@reactra/service 0.1.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akhil Shastri and the Reactra contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @reactra/service
2
+
3
+ > Reactra service/DI runtime — dependency injection for Reactra apps.
4
+
5
+ **Alpha** — published under the npm dist-tag `alpha`. APIs may change before 1.0.
6
+
7
+ ```bash
8
+ npm install @reactra/service@alpha
9
+ ```
10
+
11
+ Part of [Reactra](https://github.com/akhilshastri/reactra) — a compiler-first,
12
+ React-19-compatible framework. See the [documentation](https://reactra-docs.vercel.app) to get
13
+ started.
14
+
15
+ ## License
16
+
17
+ MIT
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Per-service runtime instance — the object the factory returns. Method
3
+ * names are the keys; values are plain (async or sync) functions. Services
4
+ * are stateless callable units per Service spec §1, so there is no
5
+ * subscribe/notify/snapshot — they expose methods only.
6
+ */
7
+ export type ServiceInstance = Record<string, unknown>;
8
+ /**
9
+ * The shape a compiler-emitted service binding takes: a `{ name, factory }`
10
+ * tuple the user passes to `configureServices`. The factory is invoked
11
+ * eagerly at registration time so service-to-service `inject` lookups
12
+ * resolve in declaration order (the user lists dependencies first in the
13
+ * `configureServices` call).
14
+ */
15
+ export interface ServiceBinding {
16
+ name: string;
17
+ factory: () => ServiceInstance;
18
+ /**
19
+ * Wave 3 §2b — `export service scoped X { ... }`. When `true`, the
20
+ * binding is REGISTERED at `configureServices` time but NOT
21
+ * instantiated. The instance is created on
22
+ * `ServiceRegistry.activateScopedServices()` (typically wired to
23
+ * `RouterRegistry`'s setLocation via `bindScopedServicesToRouter`) and
24
+ * disposed on the next `deactivateScopedServices` call — invoking
25
+ * `instance.dispose()` if the surface includes a `dispose` method.
26
+ * Absent / `false` for app-singleton bindings (the default).
27
+ */
28
+ scoped?: boolean;
29
+ }
30
+ declare class ServiceRegistryImpl {
31
+ private instances;
32
+ private scopedFactories;
33
+ /**
34
+ * Register a service binding. App-singleton (`scoped !== true`) is
35
+ * instantiated immediately so downstream services can resolve
36
+ * dependencies via `ServiceRegistry.get` during their own factory
37
+ * call. Scoped bindings (Wave 3 §2b) store the factory only — they
38
+ * spin up on `activateScopedServices()`.
39
+ */
40
+ register: (binding: ServiceBinding) => void;
41
+ /**
42
+ * Wave 3 §2b — instantiate every scoped service. Idempotent: a
43
+ * second activation without intervening deactivate is a no-op for
44
+ * services that already have a live instance (the user might call
45
+ * this twice on a route re-entry). Called by the router-bound
46
+ * lifecycle hook in `bindScopedServicesToRouter`; safe to call
47
+ * manually in tests.
48
+ */
49
+ activateScopedServices: () => void;
50
+ /**
51
+ * Wave 3 §2b — dispose + drop every scoped service instance. For
52
+ * each scoped instance with a `dispose()` method, invokes it before
53
+ * deletion. Errors thrown by `dispose` are caught + console.error'd
54
+ * so a single misbehaving service can't block the next route's
55
+ * activation. Singleton instances are unaffected.
56
+ */
57
+ deactivateScopedServices: () => void;
58
+ /**
59
+ * Look up a singleton by name. Throws if the service wasn't registered —
60
+ * usually means the user forgot to add it to `configureServices`, or
61
+ * listed it after a consumer in the same array.
62
+ */
63
+ get: <T extends ServiceInstance = ServiceInstance>(name: string) => T;
64
+ /** True if a service is registered. Used by tests + future provide wiring. */
65
+ has: (name: string) => boolean;
66
+ /**
67
+ * HMR hot-replace a binding (Compiler §5.3). Compiler-emitted
68
+ * `import.meta.hot.accept(...)` blocks in service-bearing files call
69
+ * this after the module re-evaluates so the new factory takes effect
70
+ * without a full page reload.
71
+ *
72
+ * Services are stateless callable units (Service spec §1) so the
73
+ * replace is just an instance swap. Consumers that cached a reference
74
+ * via `inject X` at component-body top see the stale singleton until
75
+ * their own component re-renders (component-side Fast Refresh in the
76
+ * consumer file, or the next interaction). Phase-1 trade-off; matches
77
+ * the store-replace one-render-delay characteristic.
78
+ *
79
+ * Idempotent on an unknown name (treated as a fresh register).
80
+ */
81
+ replace: (binding: ServiceBinding) => void;
82
+ }
83
+ /**
84
+ * The process-wide service registry singleton. The compiler emits imports
85
+ * of this and `ServiceRegistry.get("X")` at every `inject X` site. User
86
+ * code typically doesn't touch it directly except through `configureServices`.
87
+ */
88
+ export declare const ServiceRegistry: ServiceRegistryImpl;
89
+ /**
90
+ * Bootstrap call — invoked from `src/main.tsx` per Runtime v0 §5. Registers
91
+ * every service binding the app cares about. Order matters: a service must
92
+ * appear in this array after the services it injects, because each
93
+ * factory call resolves `inject` references eagerly via `ServiceRegistry.get`.
94
+ */
95
+ export declare const configureServices: (opts: {
96
+ services: ServiceBinding[];
97
+ }) => void;
98
+ /**
99
+ * Compiler-emitted factories call this to build a `ServiceInstance`. The
100
+ * `build` callback returns the surface object — `{ methodName: fn, ... }`.
101
+ * This helper exists for symmetry with `createStoreInstance` and to leave a
102
+ * future hook for Strategy B context-wrapping; today it is a pass-through.
103
+ *
104
+ * Shape used by emitted code (see Pass 9 codegen):
105
+ *
106
+ * createServiceInstance(() => {
107
+ * const authService = ServiceRegistry.get("authService")
108
+ * const get = async (id) => { ... authService.getToken() ... }
109
+ * return { get }
110
+ * })
111
+ */
112
+ export declare const createServiceInstance: (build: () => ServiceInstance) => ServiceInstance;
113
+ /**
114
+ * The per-subtree map of service overrides. Read by {@link useService};
115
+ * written by the compiler-emitted `<ServiceContext.Provider>` wrap that
116
+ * Pass 9 emits around a component's view when the component has any
117
+ * `provide X with Y` declarations.
118
+ *
119
+ * Plain readonly object instead of just `ReadonlyMap` so future fields
120
+ * (e.g. a debug label) can be added without a breaking shape change.
121
+ */
122
+ export interface ServiceOverrideMap {
123
+ readonly map: ReadonlyMap<string, unknown>;
124
+ }
125
+ /**
126
+ * The empty map used as the default `ServiceContext` value (no overrides
127
+ * — every `useService(name)` falls through to `ServiceRegistry.get`).
128
+ * Exported so tests can compare against it and the codegen can short-
129
+ * circuit composing an empty additions object.
130
+ */
131
+ export declare const EMPTY_OVERRIDES: ServiceOverrideMap;
132
+ /**
133
+ * React context carrying the service-override map for the current subtree.
134
+ * Initial value is `EMPTY_OVERRIDES` so a component that doesn't sit under
135
+ * any `<ServiceContext.Provider>` still reads via the singleton-fallback
136
+ * path. The context is shared across the whole app — a SINGLE context,
137
+ * not one-per-service, so the runtime stays simple at the cost of a tiny
138
+ * per-render lookup hit (a single `Map.get`).
139
+ */
140
+ export declare const ServiceContext: import("react").Context<ServiceOverrideMap>;
141
+ /**
142
+ * Pure-logic resolver — used by tests AND by {@link useService}. Looks
143
+ * `name` up in `overrides`; on miss, falls through to `ServiceRegistry`'s
144
+ * singleton. Exported so the inject/provide tests can exercise the
145
+ * fallback chain without standing up a React renderer.
146
+ */
147
+ export declare const resolveServiceFromOverrides: <T extends ServiceInstance = ServiceInstance>(name: string, overrides: ServiceOverrideMap) => T;
148
+ /**
149
+ * Component-side service read. The compiler emits this in place of
150
+ * `ServiceRegistry.get` for `inject service X` in component bodies, so
151
+ * the call site honours any ancestor `provide X with Y` override.
152
+ *
153
+ * Reads the nearest enclosing `ServiceContext` via `React.use` (React 19),
154
+ * then delegates to {@link resolveServiceFromOverrides}. Service-to-service
155
+ * injections (inside a service body, not a component) continue to use
156
+ * `ServiceRegistry.get` directly — there's no React render context
157
+ * available there to read.
158
+ */
159
+ export declare const useService: <T extends ServiceInstance = ServiceInstance>(name: string) => T;
160
+ /**
161
+ * Pure helper — layer `additions` on top of `parent`'s map, with the
162
+ * additions WINNING (child `provide` overrides ancestor `provide` for the
163
+ * same name). The compiler wraps the call in `useMemo` so the provider
164
+ * value is stable across re-renders unless the parent map or additions
165
+ * change identity. Pass-9 emits one entry per `provide X with Y` in the
166
+ * component body — `additions = { X: ServiceRegistry.get("Y") }`.
167
+ *
168
+ * Returns a NEW map; never mutates the input. Passing an empty
169
+ * `additions` returns the parent's map identity (avoids spurious context
170
+ * value churn when a component declared `provide` for no service after
171
+ * all — e.g. after a refactor).
172
+ */
173
+ export declare const composeOverrides: (parent: ServiceOverrideMap, additions: Readonly<Record<string, unknown>>) => ServiceOverrideMap;
174
+ /**
175
+ * Minimal duck-type for `RouterRegistry` — the only method this helper
176
+ * cares about is `subscribe`. Avoids a compile-time dependency on
177
+ * `@reactra/router` (which would invert today's import graph).
178
+ */
179
+ export interface RouterLike {
180
+ subscribe(onChange: () => void): () => void;
181
+ /**
182
+ * Present on the real `RouterRegistry` (Middleware spec v2 §3): the slot
183
+ * `bindScopedServicesToRouter` installs the middleware `ctx.service`
184
+ * resolver into. Optional so test doubles that only exercise the scoped
185
+ * lifecycle stay valid.
186
+ */
187
+ __setMiddlewareServiceResolver?(resolver: (name: string) => unknown): void;
188
+ }
189
+ /**
190
+ * Wire scoped services to the router's route-change lifecycle. On the
191
+ * first invocation, activates every registered scoped service (the app
192
+ * just landed on its initial route). On every subsequent route change,
193
+ * deactivates the previous route's instances + activates fresh ones for
194
+ * the new route.
195
+ *
196
+ * Returns an unsubscribe function — call it on app teardown (rarely
197
+ * needed in browser, useful in tests).
198
+ *
199
+ * KNOWN LIMITATION (Stage 3 of Service Strategy B): the router's
200
+ * `subscribe` listeners fire AFTER `setLocation`'s onEnter has already
201
+ * called the new route's stores. So a store's `onEnter` reading a scoped
202
+ * service still sees the PREVIOUS route's instance until the next
203
+ * microtask. Components (which read via useService → React.use →
204
+ * re-render) see the new instance on the very next render. Spec-faithful
205
+ * pre-onEnter scope activation needs a router-side hook the runtime
206
+ * doesn't expose yet — filed as a follow-up.
207
+ *
208
+ * // main.tsx
209
+ * import { RouterRegistry } from "@reactra/router"
210
+ * import { bindScopedServicesToRouter } from "@reactra/service"
211
+ * ...
212
+ * configureRouter({ ... })
213
+ * bindScopedServicesToRouter(RouterRegistry)
214
+ */
215
+ export declare const bindScopedServicesToRouter: (router: RouterLike) => (() => void);
216
+ export {};
217
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+BA;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,eAAe,CAAA;IAC9B;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,cAAM,mBAAmB;IACvB,OAAO,CAAC,SAAS,CAAqC;IAMtD,OAAO,CAAC,eAAe,CAA2C;IAElE;;;;;;OAMG;IACH,QAAQ,GAAI,SAAS,cAAc,KAAG,IAAI,CAgBzC;IAED;;;;;;;OAOG;IACH,sBAAsB,QAAO,IAAI,CAKhC;IAED;;;;;;OAMG;IACH,wBAAwB,QAAO,IAAI,CAmBlC;IAED;;;;OAIG;IACH,GAAG,GAAI,CAAC,SAAS,eAAe,GAAG,eAAe,EAAE,MAAM,MAAM,KAAG,CAAC,CAQnE;IAED,8EAA8E;IAC9E,GAAG,GAAI,MAAM,MAAM,KAAG,OAAO,CAA4B;IAEzD;;;;;;;;;;;;;;OAcG;IACH,OAAO,GAAI,SAAS,cAAc,KAAG,IAAI,CAExC;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,qBAA4B,CAAA;AAExD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM;IAAE,QAAQ,EAAE,cAAc,EAAE,CAAA;CAAE,KAAG,IAExE,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,MAAM,eAAe,KAAG,eAA0B,CAAA;AAM/F;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,kBAAuC,CAAA;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,6CAAqD,CAAA;AAEhF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GAAI,CAAC,SAAS,eAAe,GAAG,eAAe,EACrF,MAAM,MAAM,EACZ,WAAW,kBAAkB,KAC5B,CAIF,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,eAAe,GAAG,eAAe,EACpE,MAAM,MAAM,KACX,CAA8D,CAAA;AAEjE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,kBAAkB,EAC1B,WAAW,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAC3C,kBAMF,CAAA;AAID;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,8BAA8B,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAA;CAC3E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,UAAU,KAAG,CAAC,MAAM,IAAI,CAgB1E,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,272 @@
1
+ // @reactra/service — Strategy A singletons + Strategy B `provide` runtime
2
+ //
3
+ // Owner: reactra-service-spec.md (Spec 2.0 / Discussion v2)
4
+ //
5
+ // What's shipped:
6
+ // ServiceRegistry: register / get / has / replace (HMR)
7
+ // configureServices({ services })
8
+ // createServiceInstance — helper consumed by compiler-emitted factories
9
+ //
10
+ // Strategy B runtime (Wave 3 §2b — this commit):
11
+ // ServiceContext — React context carrying the override map for the
12
+ // current subtree.
13
+ // useService<T>(name) — read a service from the nearest enclosing
14
+ // override map; falls back to ServiceRegistry's
15
+ // singleton when not overridden. The compiler
16
+ // emits this in place of `ServiceRegistry.get`
17
+ // for component-side `inject service X` once the
18
+ // Pass-9 changes (next commit) land.
19
+ // composeOverrides — pure helper Pass-9 wraps in `useMemo` to build
20
+ // the value the component's <ServiceContext.Provider>
21
+ // passes down. Layers `additions` on top of
22
+ // `parent` so a child `provide` overrides the
23
+ // ancestor for the same name.
24
+ //
25
+ // Still deferred to subsequent commits:
26
+ // Pass-1.1 `provide X with Y` preprocessor + AST + Pass 5c + Pass 9 emit
27
+ // `service scoped` route-co-located lifecycle (depends on router glue)
28
+ // `service server` server-action emission (React 19 "use server")
29
+ // AbortSignal auto-injection from resources (Service spec §8.4)
30
+ // Dead-code elimination of unused methods (Pass 6b)
31
+ class ServiceRegistryImpl {
32
+ instances = new Map();
33
+ // Wave 3 §2b — scoped service factories registered but NOT instantiated
34
+ // at `configureServices` time. `activateScopedServices()` instantiates
35
+ // them on route enter; `deactivateScopedServices()` disposes them on
36
+ // exit. A separate map keeps the singleton/scoped registries clean —
37
+ // the `get` path still consults `instances` first either way.
38
+ scopedFactories = new Map();
39
+ /**
40
+ * Register a service binding. App-singleton (`scoped !== true`) is
41
+ * instantiated immediately so downstream services can resolve
42
+ * dependencies via `ServiceRegistry.get` during their own factory
43
+ * call. Scoped bindings (Wave 3 §2b) store the factory only — they
44
+ * spin up on `activateScopedServices()`.
45
+ */
46
+ register = (binding) => {
47
+ if (binding.scoped === true) {
48
+ if (this.scopedFactories.has(binding.name) || this.instances.has(binding.name)) {
49
+ throw new Error(`[reactra:service] SVC001: service "${binding.name}" is already registered`);
50
+ }
51
+ this.scopedFactories.set(binding.name, binding.factory);
52
+ return;
53
+ }
54
+ if (this.instances.has(binding.name)) {
55
+ throw new Error(`[reactra:service] SVC001: service "${binding.name}" is already registered`);
56
+ }
57
+ this.instances.set(binding.name, binding.factory());
58
+ };
59
+ /**
60
+ * Wave 3 §2b — instantiate every scoped service. Idempotent: a
61
+ * second activation without intervening deactivate is a no-op for
62
+ * services that already have a live instance (the user might call
63
+ * this twice on a route re-entry). Called by the router-bound
64
+ * lifecycle hook in `bindScopedServicesToRouter`; safe to call
65
+ * manually in tests.
66
+ */
67
+ activateScopedServices = () => {
68
+ for (const [name, factory] of this.scopedFactories) {
69
+ if (this.instances.has(name))
70
+ continue;
71
+ this.instances.set(name, factory());
72
+ }
73
+ };
74
+ /**
75
+ * Wave 3 §2b — dispose + drop every scoped service instance. For
76
+ * each scoped instance with a `dispose()` method, invokes it before
77
+ * deletion. Errors thrown by `dispose` are caught + console.error'd
78
+ * so a single misbehaving service can't block the next route's
79
+ * activation. Singleton instances are unaffected.
80
+ */
81
+ deactivateScopedServices = () => {
82
+ for (const name of this.scopedFactories.keys()) {
83
+ const inst = this.instances.get(name);
84
+ if (!inst)
85
+ continue;
86
+ const disposer = inst.dispose;
87
+ if (typeof disposer === "function") {
88
+ try {
89
+ ;
90
+ disposer.call(inst);
91
+ }
92
+ catch (err) {
93
+ if (typeof console !== "undefined") {
94
+ console.error(`[reactra:service] scoped service "${name}".dispose threw — continuing teardown`, err);
95
+ }
96
+ }
97
+ }
98
+ this.instances.delete(name);
99
+ }
100
+ };
101
+ /**
102
+ * Look up a singleton by name. Throws if the service wasn't registered —
103
+ * usually means the user forgot to add it to `configureServices`, or
104
+ * listed it after a consumer in the same array.
105
+ */
106
+ get = (name) => {
107
+ const inst = this.instances.get(name);
108
+ if (!inst) {
109
+ throw new Error(`[reactra:service] service "${name}" was used before configureServices() registered it`);
110
+ }
111
+ return inst;
112
+ };
113
+ /** True if a service is registered. Used by tests + future provide wiring. */
114
+ has = (name) => this.instances.has(name);
115
+ /**
116
+ * HMR hot-replace a binding (Compiler §5.3). Compiler-emitted
117
+ * `import.meta.hot.accept(...)` blocks in service-bearing files call
118
+ * this after the module re-evaluates so the new factory takes effect
119
+ * without a full page reload.
120
+ *
121
+ * Services are stateless callable units (Service spec §1) so the
122
+ * replace is just an instance swap. Consumers that cached a reference
123
+ * via `inject X` at component-body top see the stale singleton until
124
+ * their own component re-renders (component-side Fast Refresh in the
125
+ * consumer file, or the next interaction). Phase-1 trade-off; matches
126
+ * the store-replace one-render-delay characteristic.
127
+ *
128
+ * Idempotent on an unknown name (treated as a fresh register).
129
+ */
130
+ replace = (binding) => {
131
+ this.instances.set(binding.name, binding.factory());
132
+ };
133
+ }
134
+ /**
135
+ * The process-wide service registry singleton. The compiler emits imports
136
+ * of this and `ServiceRegistry.get("X")` at every `inject X` site. User
137
+ * code typically doesn't touch it directly except through `configureServices`.
138
+ */
139
+ export const ServiceRegistry = new ServiceRegistryImpl();
140
+ /**
141
+ * Bootstrap call — invoked from `src/main.tsx` per Runtime v0 §5. Registers
142
+ * every service binding the app cares about. Order matters: a service must
143
+ * appear in this array after the services it injects, because each
144
+ * factory call resolves `inject` references eagerly via `ServiceRegistry.get`.
145
+ */
146
+ export const configureServices = (opts) => {
147
+ for (const binding of opts.services)
148
+ ServiceRegistry.register(binding);
149
+ };
150
+ /**
151
+ * Compiler-emitted factories call this to build a `ServiceInstance`. The
152
+ * `build` callback returns the surface object — `{ methodName: fn, ... }`.
153
+ * This helper exists for symmetry with `createStoreInstance` and to leave a
154
+ * future hook for Strategy B context-wrapping; today it is a pass-through.
155
+ *
156
+ * Shape used by emitted code (see Pass 9 codegen):
157
+ *
158
+ * createServiceInstance(() => {
159
+ * const authService = ServiceRegistry.get("authService")
160
+ * const get = async (id) => { ... authService.getToken() ... }
161
+ * return { get }
162
+ * })
163
+ */
164
+ export const createServiceInstance = (build) => build();
165
+ // ─── Strategy B — `provide` runtime ────────────────────────────────────────
166
+ import { createContext, use } from "react";
167
+ /**
168
+ * The empty map used as the default `ServiceContext` value (no overrides
169
+ * — every `useService(name)` falls through to `ServiceRegistry.get`).
170
+ * Exported so tests can compare against it and the codegen can short-
171
+ * circuit composing an empty additions object.
172
+ */
173
+ export const EMPTY_OVERRIDES = { map: new Map() };
174
+ /**
175
+ * React context carrying the service-override map for the current subtree.
176
+ * Initial value is `EMPTY_OVERRIDES` so a component that doesn't sit under
177
+ * any `<ServiceContext.Provider>` still reads via the singleton-fallback
178
+ * path. The context is shared across the whole app — a SINGLE context,
179
+ * not one-per-service, so the runtime stays simple at the cost of a tiny
180
+ * per-render lookup hit (a single `Map.get`).
181
+ */
182
+ export const ServiceContext = createContext(EMPTY_OVERRIDES);
183
+ /**
184
+ * Pure-logic resolver — used by tests AND by {@link useService}. Looks
185
+ * `name` up in `overrides`; on miss, falls through to `ServiceRegistry`'s
186
+ * singleton. Exported so the inject/provide tests can exercise the
187
+ * fallback chain without standing up a React renderer.
188
+ */
189
+ export const resolveServiceFromOverrides = (name, overrides) => {
190
+ const override = overrides.map.get(name);
191
+ if (override !== undefined)
192
+ return override;
193
+ return ServiceRegistry.get(name);
194
+ };
195
+ /**
196
+ * Component-side service read. The compiler emits this in place of
197
+ * `ServiceRegistry.get` for `inject service X` in component bodies, so
198
+ * the call site honours any ancestor `provide X with Y` override.
199
+ *
200
+ * Reads the nearest enclosing `ServiceContext` via `React.use` (React 19),
201
+ * then delegates to {@link resolveServiceFromOverrides}. Service-to-service
202
+ * injections (inside a service body, not a component) continue to use
203
+ * `ServiceRegistry.get` directly — there's no React render context
204
+ * available there to read.
205
+ */
206
+ export const useService = (name) => resolveServiceFromOverrides(name, use(ServiceContext));
207
+ /**
208
+ * Pure helper — layer `additions` on top of `parent`'s map, with the
209
+ * additions WINNING (child `provide` overrides ancestor `provide` for the
210
+ * same name). The compiler wraps the call in `useMemo` so the provider
211
+ * value is stable across re-renders unless the parent map or additions
212
+ * change identity. Pass-9 emits one entry per `provide X with Y` in the
213
+ * component body — `additions = { X: ServiceRegistry.get("Y") }`.
214
+ *
215
+ * Returns a NEW map; never mutates the input. Passing an empty
216
+ * `additions` returns the parent's map identity (avoids spurious context
217
+ * value churn when a component declared `provide` for no service after
218
+ * all — e.g. after a refactor).
219
+ */
220
+ export const composeOverrides = (parent, additions) => {
221
+ const keys = Object.keys(additions);
222
+ if (keys.length === 0)
223
+ return parent;
224
+ const merged = new Map(parent.map);
225
+ for (const k of keys)
226
+ merged.set(k, additions[k]);
227
+ return { map: merged };
228
+ };
229
+ /**
230
+ * Wire scoped services to the router's route-change lifecycle. On the
231
+ * first invocation, activates every registered scoped service (the app
232
+ * just landed on its initial route). On every subsequent route change,
233
+ * deactivates the previous route's instances + activates fresh ones for
234
+ * the new route.
235
+ *
236
+ * Returns an unsubscribe function — call it on app teardown (rarely
237
+ * needed in browser, useful in tests).
238
+ *
239
+ * KNOWN LIMITATION (Stage 3 of Service Strategy B): the router's
240
+ * `subscribe` listeners fire AFTER `setLocation`'s onEnter has already
241
+ * called the new route's stores. So a store's `onEnter` reading a scoped
242
+ * service still sees the PREVIOUS route's instance until the next
243
+ * microtask. Components (which read via useService → React.use →
244
+ * re-render) see the new instance on the very next render. Spec-faithful
245
+ * pre-onEnter scope activation needs a router-side hook the runtime
246
+ * doesn't expose yet — filed as a follow-up.
247
+ *
248
+ * // main.tsx
249
+ * import { RouterRegistry } from "@reactra/router"
250
+ * import { bindScopedServicesToRouter } from "@reactra/service"
251
+ * ...
252
+ * configureRouter({ ... })
253
+ * bindScopedServicesToRouter(RouterRegistry)
254
+ */
255
+ export const bindScopedServicesToRouter = (router) => {
256
+ // Service-aware middleware (Middleware spec v2 §3): the same explicit
257
+ // bootstrap line also installs the `ctx.service` resolver — app singletons
258
+ // + currently-activated scoped instances, via the registry's own lookup
259
+ // (its SVC diagnostics propagate unchanged).
260
+ router.__setMiddlewareServiceResolver?.((name) => ServiceRegistry.get(name));
261
+ let activated = false;
262
+ return router.subscribe(() => {
263
+ if (!activated) {
264
+ ServiceRegistry.activateScopedServices();
265
+ activated = true;
266
+ return;
267
+ }
268
+ ServiceRegistry.deactivateScopedServices();
269
+ ServiceRegistry.activateScopedServices();
270
+ });
271
+ };
272
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@reactra/service",
3
+ "version": "0.1.0-alpha.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "peerDependencies": {
13
+ "react": "^19.2.0"
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "tag": "alpha",
22
+ "provenance": false
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/akhilshastri/reactra.git",
27
+ "directory": "packages/service"
28
+ },
29
+ "homepage": "https://reactra-docs.vercel.app",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=22.18"
33
+ },
34
+ "sideEffects": false,
35
+ "description": "Reactra service/DI runtime — dependency injection for Reactra apps."
36
+ }