@maestro-js/service-registry 1.0.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/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # Service Registry
2
+
3
+ The foundation of the maestro-js provider pattern. Provides a generic service locator with deferred resolution via Proxy, allowing consumers to obtain a reference to a service before it has been registered.
4
+
5
+ ## The Provider Pattern
6
+
7
+ Every major maestro-js package follows the same five-part provider pattern built on `@maestro-js/service-registry`:
8
+
9
+ 1. **`create(config)`** -- Factory that instantiates a service with a driver.
10
+ 2. **`Provider.register(name, service)`** -- Registers the instance in a named registry.
11
+ 3. **`provider(key)`** -- Resolves a named instance (deferred via Proxy, so resolution can happen before registration).
12
+ 4. **Facade** -- The exported const spreads `provider('default')` so methods can be called directly (e.g., `Cache.get(key)` instead of `Cache.provider('default').get(key)`).
13
+ 5. **`drivers`** -- Factory functions for pluggable implementations (e.g., `Cache.drivers.redis(...)`, `Cache.drivers.mockRedis(...)`).
14
+
15
+ Registration order does not matter as long as registration happens before the first method call on a resolved proxy. The Proxy intercepts property access and defers the actual lookup until invocation time.
16
+
17
+ ## API Reference
18
+
19
+ ### `ServiceRegistry.createRegistry(proxy)`
20
+
21
+ Create a typed registry that stores services by name and resolves them through a caller-supplied proxy function.
22
+
23
+ ```ts
24
+ function createRegistry<Name extends string, Item>(
25
+ proxy: (getItem: () => Item) => Item
26
+ ): Registry<Name, Item>
27
+ ```
28
+
29
+ The `proxy` parameter receives a lazy getter (`getItem`) so that the actual service lookup is deferred until the returned reference is used. Returns an object with four methods:
30
+
31
+ - **`register(name, item)`** -- Store a service under the given name.
32
+ - **`resolve(name)`** -- Return a proxied reference to the named service. Throws if the service is not registered at call time (i.e., when a method on the proxy is actually invoked).
33
+ - **`has(name)`** -- Check whether a service has been registered under the given name.
34
+ - **`list()`** -- Return all registered service names.
35
+
36
+ Error behavior on resolve: if a method is called on the proxy before registration, throws `No service registered for "<name>". Available: <registered names>`.
37
+
38
+ ### `ServiceRegistry.proxyFunctionsForObject(getItem)`
39
+
40
+ Default proxy function for services whose members are all functions.
41
+
42
+ ```ts
43
+ function proxyFunctionsForObject<Item extends Record<keyof Item, (...args: any) => unknown>>(
44
+ getItem: () => Item
45
+ ): Item
46
+ ```
47
+
48
+ Returns a `Proxy` that intercepts property access and wraps each property in a function that defers to the real service at call time. The Proxy also implements `has`, `ownKeys`, and `getOwnPropertyDescriptor` traps for full introspection support.
49
+
50
+ Use this for any service whose public interface is entirely methods. Most packages use it directly:
51
+
52
+ ```ts
53
+ const registry = ServiceRegistry.createRegistry<Cache.Provider.Key, Cache.CacheService>(
54
+ ServiceRegistry.proxyFunctionsForObject
55
+ )
56
+ ```
57
+
58
+ ### Custom Proxy Functions
59
+
60
+ When a service has non-function members or needs special delegation logic, supply a custom proxy instead of `proxyFunctionsForObject`. The Events package does this to handle its `create` method (which returns a typed event handle rather than delegating directly):
61
+
62
+ ```ts
63
+ const registry = ServiceRegistry.createRegistry<Events.Provider.Key, Events.EventsService>((getItem) => {
64
+ return new Proxy(
65
+ {},
66
+ {
67
+ get(target, property: keyof Events.EventsService) {
68
+ if (property === 'create') {
69
+ return getCreateEventFn(() => getItem())
70
+ } else {
71
+ return (...args: any) => {
72
+ const item = getItem()
73
+ const func = item[property] as (...args: any) => unknown
74
+ return func(...args)
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ) as Events.EventsService
80
+ })
81
+ ```
82
+
83
+ ## How to Create a New Package Using ServiceRegistry
84
+
85
+ Follow this step-by-step pattern to build a new provider-based package.
86
+
87
+ ### Step 1: Define the service interface
88
+
89
+ Declare a namespace with the service interface and provider types:
90
+
91
+ ```ts
92
+ type KeysWithFallback = keyof MyPkg.Provider.Keys extends never ? { default: unknown } : MyPkg.Provider.Keys
93
+
94
+ export declare namespace MyPkg {
95
+ export interface MyPkgService {
96
+ doSomething(input: string): Promise<string>
97
+ doOther(id: number): Promise<void>
98
+ }
99
+
100
+ namespace Provider {
101
+ export interface MyPkgServiceConfig {
102
+ driver: MyPkgDriver
103
+ }
104
+
105
+ type Key = keyof KeysWithFallback
106
+ export interface Keys {}
107
+ }
108
+ }
109
+ ```
110
+
111
+ The `KeysWithFallback` type ensures `Key` resolves to `'default'` when consumers have not augmented the `Keys` interface.
112
+
113
+ ### Step 2: Write the create factory
114
+
115
+ ```ts
116
+ function create(config: MyPkg.Provider.MyPkgServiceConfig): MyPkg.MyPkgService {
117
+ const driver = config.driver
118
+
119
+ async function doSomething(input: string): Promise<string> {
120
+ return driver.process(input)
121
+ }
122
+
123
+ async function doOther(id: number): Promise<void> {
124
+ await driver.execute(id)
125
+ }
126
+
127
+ return { doSomething, doOther }
128
+ }
129
+ ```
130
+
131
+ ### Step 3: Create the registry and wire up Provider/provider/facade
132
+
133
+ ```ts
134
+ import { ServiceRegistry } from '@maestro-js/service-registry'
135
+
136
+ const registry = ServiceRegistry.createRegistry<MyPkg.Provider.Key, MyPkg.MyPkgService>(
137
+ ServiceRegistry.proxyFunctionsForObject
138
+ )
139
+
140
+ const Provider = {
141
+ create,
142
+ register: registry.register
143
+ }
144
+
145
+ function provider(key: MyPkg.Provider.Key) {
146
+ return registry.resolve(key)
147
+ }
148
+
149
+ const facade = provider('default')
150
+
151
+ export const MyPkg = {
152
+ Provider,
153
+ provider,
154
+ doSomething: facade.doSomething,
155
+ doOther: facade.doOther,
156
+ drivers: {
157
+ myDriver: myDriverFactory
158
+ }
159
+ }
160
+ ```
161
+
162
+ ### Step 4: Consumer registration and usage
163
+
164
+ ```ts
165
+ import { MyPkg } from '@maestro-js/my-pkg'
166
+
167
+ // Registration (typically at app startup)
168
+ MyPkg.Provider.register('default', MyPkg.Provider.create({
169
+ driver: MyPkg.drivers.myDriver({ /* config */ })
170
+ }))
171
+
172
+ // Usage (can import and reference before registration)
173
+ await MyPkg.doSomething('hello')
174
+ ```
175
+
176
+ ## Common Patterns
177
+
178
+ ### Standard usage (Cache, Db)
179
+
180
+ Most packages pass `proxyFunctionsForObject` directly and spread the resolved proxy into the facade:
181
+
182
+ ```ts
183
+ // From packages/cache/src/index.ts
184
+ const registry = ServiceRegistry.createRegistry<Cache.Provider.Key, Cache.CacheService>(
185
+ ServiceRegistry.proxyFunctionsForObject
186
+ )
187
+
188
+ const Provider = {
189
+ create,
190
+ register: registry.register
191
+ }
192
+
193
+ function provider(key: Cache.Provider.Key) {
194
+ const service = registry.resolve(key)
195
+ return {
196
+ has: service.has,
197
+ get: service.get,
198
+ set: service.set,
199
+ del: service.del,
200
+ flush: service.flush,
201
+ remember: service.remember,
202
+ item: service.item,
203
+ lock: service.lock
204
+ }
205
+ }
206
+
207
+ export const Cache = {
208
+ Provider,
209
+ ...provider('default'),
210
+ provider,
211
+ drivers: {
212
+ redis: redisCacheDriver,
213
+ mockRedis: mockRedisCacheDriver
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### Direct resolve (Db, Events)
219
+
220
+ Some packages resolve directly without destructuring in the provider function:
221
+
222
+ ```ts
223
+ // From packages/events/src/index.ts
224
+ function provider(key: Events.Provider.Key) {
225
+ return registry.resolve(key)
226
+ }
227
+
228
+ const facade = provider('default')
229
+
230
+ export const Events = {
231
+ provider,
232
+ Provider,
233
+ addListener: facade.addListener,
234
+ removeListener: facade.removeListener,
235
+ emit: facade.emit,
236
+ create: facade.create,
237
+ removeAllListeners: facade.removeAllListeners,
238
+ drivers: { /* ... */ }
239
+ }
240
+ ```
241
+
242
+ ### Multiple named providers
243
+
244
+ Register additional named instances for multi-tenant or multi-backend scenarios:
245
+
246
+ ```ts
247
+ Db.Provider.register('default', Db.Provider.create({
248
+ driver: Db.drivers.mysql({ host: 'primary-db', database: 'app' })
249
+ }))
250
+
251
+ Db.Provider.register('analytics', Db.Provider.create({
252
+ driver: Db.drivers.mysql({ host: 'analytics-db', database: 'analytics' })
253
+ }))
254
+
255
+ // Use a named provider
256
+ const analytics = Db.provider('analytics')
257
+ await analytics.select('SELECT * FROM events WHERE date > ?', ['2024-01-01'])
258
+ ```
259
+
260
+ ## Cross-Package Integration
261
+
262
+ ServiceRegistry is the foundation layer. The dependency graph flows as:
263
+
264
+ - **ServiceRegistry** -- depended on by Cache, CacheControl, Context, Crypt, Db, Events, Exceptions, Hash, Mail, Metrics, Queue, RecurringJobs, Broadcast
265
+ - No runtime dependencies of its own
266
+ - Packages that do NOT use ServiceRegistry: Log (uses a simpler singleton), Helpers, Sql (pure utilities)
267
+
268
+ When building or debugging any provider-based package, the resolution flow is:
269
+
270
+ 1. Import triggers `ServiceRegistry.createRegistry(...)`, creating the internal `Map`.
271
+ 2. `provider('default')` is called immediately to build the facade, returning a Proxy.
272
+ 3. At app startup, `Provider.register('default', Provider.create({...}))` populates the Map.
273
+ 4. First actual method call on the facade hits the Proxy, which calls `getItem()`, which resolves from the Map.
274
+
275
+ If a method is invoked before registration, step 4 throws with the descriptive error listing available registrations.
276
+
277
+ ## Internal Implementation
278
+
279
+ The registry stores services in a `Map<Name, Item>`. The `resolve` method does not look up the service immediately. Instead it calls the `proxy` function, passing a lazy `getItem` closure. That closure performs the Map lookup (and throws if missing) only when the proxy is actually used.
280
+
281
+ The `proxyFunctionsForObject` proxy creates an empty object wrapped in a `Proxy` with these traps:
282
+
283
+ - **`get`** -- Returns a function that calls `getItem()` at invocation time and delegates to the real method.
284
+ - **`has`** -- Checks property existence on the resolved item.
285
+ - **`ownKeys`** -- Returns keys of the resolved item.
286
+ - **`getOwnPropertyDescriptor`** -- Returns descriptors from the resolved item (all marked `configurable` and `enumerable`).
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Service Registry — a generic service locator with deferred resolution.
3
+ *
4
+ * Services are registered by name and resolved lazily through a proxy, allowing
5
+ * consumers to obtain a reference to a service before it has been registered.
6
+ * Method calls on the proxy are forwarded to the real implementation at invocation
7
+ * time, so registration order doesn't matter as long as it happens before first use.
8
+ */
9
+ interface Registry<Name extends string, Item> {
10
+ register(name: Name, item: Item): void;
11
+ resolve(name: Name): Item;
12
+ has(name: Name): boolean;
13
+ list(): Name[];
14
+ }
15
+ /**
16
+ * Creates a typed registry that stores services by name and resolves them through
17
+ * a caller-supplied proxy function. The proxy receives a lazy getter so that the
18
+ * actual service lookup is deferred until the returned reference is used.
19
+ *
20
+ * @param proxy - A function that wraps a deferred getter into a proxy object.
21
+ * Use `proxyFunctionsForObject` for services whose members are all functions.
22
+ * Supply a custom proxy when you need special handling (e.g. partial delegation).
23
+ */
24
+ declare function createRegistry<Name extends string, Item>(proxy: (getItem: () => Item) => Item): Registry<Name, Item>;
25
+ /**
26
+ * Default proxy function for services whose members are all functions.
27
+ *
28
+ * Returns a Proxy that intercepts property access and wraps each property in a
29
+ * function that defers to the real service at call time. This means `resolve()`
30
+ * can return a usable reference immediately — the actual service only needs to
31
+ * be registered before the first method call.
32
+ */
33
+ declare function proxyFunctionsForObject<Item extends Record<keyof Item, (...args: any) => unknown>>(getItem: () => Item): Item;
34
+ declare const ServiceRegistry: {
35
+ createRegistry: typeof createRegistry;
36
+ proxyFunctionsForObject: typeof proxyFunctionsForObject;
37
+ };
38
+
39
+ export { ServiceRegistry };
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ // src/index.ts
2
+ function createRegistry(proxy) {
3
+ const map = /* @__PURE__ */ new Map();
4
+ function register(name, item) {
5
+ map.set(name, item);
6
+ }
7
+ function resolve(name) {
8
+ return proxy(() => {
9
+ if (!map.has(name)) {
10
+ const available = [...map.keys()].join(", ");
11
+ throw new Error(`No service registered for "${name}". Available: ${available}`);
12
+ }
13
+ return map.get(name);
14
+ });
15
+ }
16
+ function has(name) {
17
+ return map.has(name);
18
+ }
19
+ return { register, resolve, has, list: () => [...map.keys()] };
20
+ }
21
+ function proxyFunctionsForObject(getItem) {
22
+ return new Proxy(
23
+ {},
24
+ {
25
+ get(_, property) {
26
+ return (...args) => {
27
+ const item = getItem();
28
+ const func = item[property];
29
+ return func.apply(item, args);
30
+ };
31
+ },
32
+ has(_, property) {
33
+ return property in getItem();
34
+ },
35
+ ownKeys() {
36
+ return Reflect.ownKeys(getItem());
37
+ },
38
+ getOwnPropertyDescriptor(_, property) {
39
+ return { configurable: true, enumerable: true, value: getItem()[property] };
40
+ }
41
+ }
42
+ );
43
+ }
44
+ var ServiceRegistry = {
45
+ createRegistry,
46
+ proxyFunctionsForObject
47
+ };
48
+ export {
49
+ ServiceRegistry
50
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@maestro-js/service-registry",
3
+ "description": "Use when working with @maestro-js/service-registry. The foundational service locator package that provides deferred resolution via Proxy. Covers createRegistry, proxyFunctionsForObject, register/resolve pattern, custom proxy functions, and the standard Provider pattern used by nearly every other maestro-js package (Cache, Db, Events, Mail, Queue, etc.). Trigger when creating new provider-based packages, debugging service resolution, understanding deferred proxies, or wiring up named service instances.",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "version": "1.0.0-alpha.0",
12
+ "publishConfig": {
13
+ "access": "restricted"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "license": "UNLICENSED",
19
+ "engines": {
20
+ "node": ">=22.18.0"
21
+ },
22
+ "repository": "https://github.com/Marcato-Partners/maestro-js",
23
+ "scripts": {
24
+ "build": "tsup --config ../../tsup.config.ts",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "echo 'No tests yet'",
27
+ "format": "prettier --write src/",
28
+ "lint": "prettier --check src/"
29
+ }
30
+ }