ripple-di 1.0.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 +21 -0
- package/README.md +507 -0
- package/dist/index.d.mts +379 -0
- package/dist/index.mjs +948 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilya Semenov
|
|
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,507 @@
|
|
|
1
|
+
# Ripple DI
|
|
2
|
+
|
|
3
|
+
Scoped dependency injection for TypeScript without container lookups in application code.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
Define a database from configuration and use it as a normal imported function:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineDependency } from "ripple-di"
|
|
11
|
+
|
|
12
|
+
export const useConfig = defineDependency(() => ({
|
|
13
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
export const useDb = defineDependency(
|
|
17
|
+
() => createDb(useConfig().databaseUrl),
|
|
18
|
+
{ dispose: db => db.close() },
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export async function loadUsers() {
|
|
22
|
+
return useDb().query("select * from users")
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Now replace only the configuration for one operation:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { provide, withOverrides } from "ripple-di"
|
|
30
|
+
|
|
31
|
+
const users = await withOverrides(
|
|
32
|
+
[provide(useConfig, { databaseUrl: testDatabaseUrl })],
|
|
33
|
+
() => loadUsers(),
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`useDb` reads `useConfig()`, so Ripple DI builds a separate database for the test configuration and loads the users through it.
|
|
38
|
+
The production database and everything unrelated keep the instances they already had, and the temporary database is closed when the callback finishes.
|
|
39
|
+
|
|
40
|
+
This is the ripple: override one input, and only the values built from it change.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install ripple-di
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Ripple DI needs `node:async_hooks`, so it runs on Node.js 18 or newer, Bun, and Deno with Node compatibility, but not in browsers.
|
|
49
|
+
The package is published as ESM.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Define each shared input and service once, next to the code that owns it.
|
|
54
|
+
Factories are lazy: they run the first time something reads the value.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// config.ts
|
|
58
|
+
import { defineDependency } from "ripple-di"
|
|
59
|
+
|
|
60
|
+
export const useConfig = defineDependency(() => ({
|
|
61
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
62
|
+
}))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// db.ts
|
|
67
|
+
import { defineDependency } from "ripple-di"
|
|
68
|
+
|
|
69
|
+
import { useConfig } from "./config"
|
|
70
|
+
|
|
71
|
+
export const useDb = defineDependency(
|
|
72
|
+
() => createDb(useConfig().databaseUrl),
|
|
73
|
+
{ dispose: db => db.close() },
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Anything that needs the database imports it and calls it.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { useDb } from "./db"
|
|
81
|
+
|
|
82
|
+
export async function loadUsers() {
|
|
83
|
+
return useDb().query("select * from users")
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The return type of `useDb()` is inferred from `createDb`.
|
|
88
|
+
|
|
89
|
+
## Which function do I call?
|
|
90
|
+
|
|
91
|
+
| You want to | Call
|
|
92
|
+
| --------------------------------------------------------- | ---------------------------
|
|
93
|
+
| Define an input, derived value, or service | `defineDependency`
|
|
94
|
+
| Supply the real values when the application starts | `install`
|
|
95
|
+
| Replace values for one callback | `withOverrides`
|
|
96
|
+
| Name an override of one dependency you write often | `createValueOverride`
|
|
97
|
+
| Replace the same values for many separate calls | `createOverrideRunner`
|
|
98
|
+
| Replace values for a lifetime you manage yourself | `createScope`
|
|
99
|
+
| Supply a value, or a factory, to `install` or an override | `provide`, `provideFactory`
|
|
100
|
+
| Shut everything down | `dispose`
|
|
101
|
+
|
|
102
|
+
## Define dependencies
|
|
103
|
+
|
|
104
|
+
Call `defineDependency<T>()` without a factory when a value must come from the application, request, or task boundary.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineDependency, provide, withOverrides } from "ripple-di"
|
|
108
|
+
|
|
109
|
+
const useTenant = defineDependency<Tenant>()
|
|
110
|
+
|
|
111
|
+
await withOverrides(
|
|
112
|
+
[provide(useTenant, tenant)],
|
|
113
|
+
() => processRequest(),
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Reading it without a provider throws `MissingProviderError`.
|
|
118
|
+
Pass a factory when the dependency has a built-in value.
|
|
119
|
+
The factory runs lazily, and calls to other dependencies inside it are tracked.
|
|
120
|
+
A function is always taken as that factory, so a dependency whose own value is a function needs `defineDependency(() => handler)`.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const useClock = defineDependency(() => systemClock)
|
|
124
|
+
const usePublicUrl = defineDependency(() => createPublicUrl(useConfig()))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The result is cached and recreated wherever one of the dependencies it read is overridden.
|
|
128
|
+
Add a `dispose` callback when values owned by Ripple DI need cleanup, as shown by `useDb` in the quick start.
|
|
129
|
+
|
|
130
|
+
## Override dependencies
|
|
131
|
+
|
|
132
|
+
`withOverrides` runs a callback with temporary values and cleans up whatever it created for that callback.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { provide, withOverrides } from "ripple-di"
|
|
136
|
+
|
|
137
|
+
const users = await withOverrides(
|
|
138
|
+
[
|
|
139
|
+
provide(useConfig, {
|
|
140
|
+
databaseUrl: "postgres://localhost/test",
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
() => loadUsers(),
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Inside the callback `useConfig()` returns the test configuration, and `useDb()` returns a database built from it.
|
|
148
|
+
The production database is untouched.
|
|
149
|
+
|
|
150
|
+
Override as many dependencies as you need in one call.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
await withOverrides(
|
|
154
|
+
[
|
|
155
|
+
provide(useConfig, testConfig),
|
|
156
|
+
provide(useClock, fixedClock),
|
|
157
|
+
],
|
|
158
|
+
() => generateReport(),
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For one dependency, you can omit the array:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await withOverrides(
|
|
166
|
+
provide(useConfig, testConfig),
|
|
167
|
+
() => loadUsers(),
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Overrides survive `await` and stay isolated between parallel callbacks.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const [leftUsers, rightUsers] = await Promise.all([
|
|
175
|
+
withOverrides([provide(useConfig, leftConfig)], () => loadUsers()),
|
|
176
|
+
withOverrides([provide(useConfig, rightConfig)], () => loadUsers()),
|
|
177
|
+
])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Name an override you write repeatedly
|
|
181
|
+
|
|
182
|
+
When the same dependency is supplied the same way all over the application, `createValueOverride` turns that into one named helper.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { createValueOverride } from "ripple-di"
|
|
186
|
+
|
|
187
|
+
const withOpenAiClient = createValueOverride(useOpenAiClient)
|
|
188
|
+
|
|
189
|
+
await withOpenAiClient(client, () => createTextEmbedding(text))
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Each call supplies the value it is given to one callback, exactly like writing `withOverrides` with a single `provide` by hand.
|
|
193
|
+
Pass the ownership options once, and every call cleans its own value up.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const withConnection = createValueOverride(useConnection, { dispose: true })
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Wire the application at startup
|
|
200
|
+
|
|
201
|
+
Some dependencies get their real value only when the application starts.
|
|
202
|
+
`install` supplies them once for the whole process.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// dependencies.ts
|
|
206
|
+
import { defineDependency } from "ripple-di"
|
|
207
|
+
|
|
208
|
+
export const useWebConfig = defineDependency<WebConfig>()
|
|
209
|
+
export const useTenantResolver = defineDependency<TenantResolver>()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// startup.ts
|
|
214
|
+
import { install, provide } from "ripple-di"
|
|
215
|
+
|
|
216
|
+
import { useTenantResolver, useWebConfig } from "./dependencies"
|
|
217
|
+
|
|
218
|
+
const installation = install([
|
|
219
|
+
provide(useWebConfig, config),
|
|
220
|
+
provide(useTenantResolver, resolveTenant),
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await runApplication()
|
|
225
|
+
} finally {
|
|
226
|
+
await installation.close()
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Installed providers are the fallback everywhere: request handlers, background jobs, tests, and any other code running outside a scope.
|
|
231
|
+
Nothing is resolved eagerly, and a scoped override still wins over an installed provider.
|
|
232
|
+
Closing the installation removes its providers and cleans up the scopes and owned values created beneath it.
|
|
233
|
+
|
|
234
|
+
- Installing while another installation or any scope is still open throws `InstallationConflictError`, whose message says what is still open.
|
|
235
|
+
Await `installation.close()` before installing a replacement.
|
|
236
|
+
- Providing the same dependency twice in one installation is rejected, exactly like in `withOverrides`.
|
|
237
|
+
- Installing late is allowed: it applies to the reads that come after it, and closing it brings the earlier values back.
|
|
238
|
+
- Every worker thread and every process wires its own installation.
|
|
239
|
+
- In tests, install once for the whole process and use `withOverrides` per test.
|
|
240
|
+
|
|
241
|
+
### Collect the providers of several modules
|
|
242
|
+
|
|
243
|
+
One runtime has one installation, so independent modules cannot each install their own providers.
|
|
244
|
+
A module exports the provisions it owns, and the composition root installs them together.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// core/providers.ts
|
|
248
|
+
export function getCoreProvisions() {
|
|
249
|
+
return [
|
|
250
|
+
provide(useDbConfig, dbConfig),
|
|
251
|
+
provide(useEmailTransport, emailTransport),
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// platform/providers.ts
|
|
258
|
+
export function getPlatformProvisions() {
|
|
259
|
+
return [
|
|
260
|
+
provide(useAgentChatConfig, agentChatConfig),
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// startup.ts
|
|
267
|
+
const installation = install([
|
|
268
|
+
...getCoreProvisions(),
|
|
269
|
+
...getPlatformProvisions(),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
onShutdown(() => installation.close())
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- Export a function that builds the provisions rather than a ready-made array, so each installation gets provisions of its own.
|
|
276
|
+
A provision that hands over ownership belongs to a single installation and cannot be reused by the next one.
|
|
277
|
+
- The composition root that installs the provisions is also the one that closes the installation, from whichever shutdown hook the application already has.
|
|
278
|
+
- Tests have the same single composition root: one preload installs the provisions of every layer the suite needs.
|
|
279
|
+
Separate preloads installing on their own would fail on the second `install` instead of adding their providers.
|
|
280
|
+
|
|
281
|
+
## Application shutdown
|
|
282
|
+
|
|
283
|
+
Call `dispose()` when the application shuts down.
|
|
284
|
+
It closes every scope and cleans up every owned value still held by the module-level API, including an active installation.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { dispose } from "ripple-di"
|
|
288
|
+
|
|
289
|
+
await dispose()
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Values created inside `withOverrides` never wait for shutdown; they are cleaned up when their own callback finishes.
|
|
293
|
+
|
|
294
|
+
`dispose()` is final: afterwards the runtime cannot resolve dependencies, create scopes, or install providers, and those calls throw `ScopeClosedError`.
|
|
295
|
+
Do not use it to reset state between tests — use `withOverrides` for that, or create a separate runtime for each lifecycle.
|
|
296
|
+
|
|
297
|
+
## Advanced usage
|
|
298
|
+
|
|
299
|
+
Everything below is optional.
|
|
300
|
+
|
|
301
|
+
### Choose how to provide a value
|
|
302
|
+
|
|
303
|
+
Each form of `provide` answers one question: who cleans the value up?
|
|
304
|
+
The disposer configured by `defineDependency` describes how to clean up an owned value; it does not make a plain provided value owned.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// You own the value. Ripple DI uses it in the scope and never disposes it.
|
|
308
|
+
provide(useConfig, testConfig)
|
|
309
|
+
|
|
310
|
+
// The scope creates the value on first read, owns it, and cleans it up with
|
|
311
|
+
// the dispose callback from the dependency definition.
|
|
312
|
+
provideFactory(useDb, () => createFakeDb())
|
|
313
|
+
|
|
314
|
+
// You hand an existing value over to the scope, cleaned up the same way.
|
|
315
|
+
provide(useDb, fakeDb, { dispose: true })
|
|
316
|
+
|
|
317
|
+
// Same handover, with cleanup you specify instead.
|
|
318
|
+
provide(useDb, fakeDb, { dispose: db => db.closeImmediately() })
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A function passed to `provide` stays an ordinary function value; use `provideFactory` when it should build the value instead.
|
|
322
|
+
The dependencies an override factory reads are tracked like the ones read by the factory it replaces.
|
|
323
|
+
`dispose: true` reuses the disposer from `defineDependency`, so it throws right away when the dependency declares none.
|
|
324
|
+
|
|
325
|
+
A provision that hands over ownership belongs to a single scope or installation.
|
|
326
|
+
Using it a second time throws `OwnedProvisionReuseError`, so create a separate value for each owner.
|
|
327
|
+
|
|
328
|
+
A dependency can define a shared cleanup rule even when it has no built-in factory:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const useQueue = defineDependency<Queue>({
|
|
332
|
+
dispose: queue => queue.close(),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const installation = install([
|
|
336
|
+
provideFactory(useQueue, () => createQueue(queueUrl)),
|
|
337
|
+
])
|
|
338
|
+
|
|
339
|
+
// Later, at shutdown:
|
|
340
|
+
await installation.close()
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The installed factory is lazy, and its queue client is closed with the installation.
|
|
344
|
+
|
|
345
|
+
### Manage a scope explicitly
|
|
346
|
+
|
|
347
|
+
Use `withOverrides` when one callback covers the whole lifetime.
|
|
348
|
+
Use `createScope` when several operations share the same overrides and close at a boundary you manage yourself.
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { createScope, provide } from "ripple-di"
|
|
352
|
+
|
|
353
|
+
const scope = createScope([
|
|
354
|
+
provide(useConfig, tenantConfig),
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
await scope.run(() => processTenant())
|
|
359
|
+
} finally {
|
|
360
|
+
await scope.close()
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- `scope.run` makes the scope current for a callback without closing it.
|
|
365
|
+
- `scope.resolve(useDb)` reads a dependency from that scope rather than the current one.
|
|
366
|
+
- `scope.createScope` and `scope.withOverrides` create children of that scope instead of the current one.
|
|
367
|
+
- A child of the scope that `withOverrides` created must be closed before the callback returns, otherwise Ripple DI closes it and throws `LeakedChildScopeError`.
|
|
368
|
+
- `scope.close()` closes the scope and everything below it, while `scope.retire()` waits for child scopes to finish first.
|
|
369
|
+
- Closing disposes what the scope itself created; reused application values stay open.
|
|
370
|
+
- Cleanup continues past a failing disposer and reports every failure in one `AggregateError`.
|
|
371
|
+
|
|
372
|
+
### Reuse one set of overrides
|
|
373
|
+
|
|
374
|
+
A long-lived object such as an API caller or a job worker is created once, but every call through it has to run with the same overrides in a scope of its own.
|
|
375
|
+
`createOverrideRunner` prepares those overrides once and applies them to each call separately.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { createOverrideRunner, provide } from "ripple-di"
|
|
379
|
+
|
|
380
|
+
const jobOverrides = createOverrideRunner(() => [
|
|
381
|
+
provide(useJobContext, createJobContext(), { dispose: true }),
|
|
382
|
+
])
|
|
383
|
+
|
|
384
|
+
const worker = createWorker(job => jobOverrides.run(() => handleJob(job)))
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Every `run` call creates a scope, applies the overrides to it, and closes it when the callback finishes, exactly like `withOverrides`.
|
|
388
|
+
Concurrent calls stay isolated, and everything a call created is gone once it returns, while application values built outside the call keep their identity.
|
|
389
|
+
|
|
390
|
+
The overrides come from a function because it runs again for every call.
|
|
391
|
+
A value it hands over with `dispose` therefore belongs to the call that created it rather than to the runner, and returning the same handover provision to a second call throws `OwnedProvisionReuseError`.
|
|
392
|
+
|
|
393
|
+
`wrap` turns a function into one that applies the overrides itself, so the worker above can take the handler directly:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
const worker = createWorker(jobOverrides.wrap(handleJob))
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The wrapped function keeps the arguments and the receiver it is called with, and returns a promise for its result.
|
|
400
|
+
It prepares nothing in advance: every call of it is one `run` call with a temporary scope of its own.
|
|
401
|
+
That also suits a file of independent test cases that share one set of overrides.
|
|
402
|
+
|
|
403
|
+
`extend` returns a runner with one more layer of overrides and leaves the runner it extends unchanged:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
const tenantOverrides = jobOverrides.extend(() => [
|
|
407
|
+
provide(useTenantResolver, tenantResolver),
|
|
408
|
+
])
|
|
409
|
+
|
|
410
|
+
await tenantOverrides.run(() => handleRequest())
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
A call through `tenantOverrides` applies both layers, and the added layer wins wherever the two provide the same dependency.
|
|
414
|
+
|
|
415
|
+
Use `createScope` instead when several operations share one set of values that stay alive until you close the scope.
|
|
416
|
+
A runner never keeps a scope open between calls: it is for repeated independent calls, not for one long-lived scope.
|
|
417
|
+
|
|
418
|
+
### Multiple runtimes
|
|
419
|
+
|
|
420
|
+
Most applications never need this.
|
|
421
|
+
The module-level functions all work on one built-in runtime.
|
|
422
|
+
Create your own only when a single process has to host several independent graphs, each with its own definitions, cached values, and shutdown lifecycle.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import { createRuntime } from "ripple-di"
|
|
426
|
+
|
|
427
|
+
function createApplication(databaseUrl: string) {
|
|
428
|
+
const runtime = createRuntime()
|
|
429
|
+
|
|
430
|
+
const useConfig = runtime.defineDependency(() => ({ databaseUrl }))
|
|
431
|
+
const useDb = runtime.defineDependency(
|
|
432
|
+
() => createDb(useConfig().databaseUrl),
|
|
433
|
+
{ dispose: db => db.close() },
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return { runtime, useDb }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const production = createApplication(productionDatabaseUrl)
|
|
440
|
+
const preview = createApplication(previewDatabaseUrl)
|
|
441
|
+
|
|
442
|
+
const productionUsers = await production.useDb().query("select * from users")
|
|
443
|
+
const previewUsers = await preview.useDb().query("select * from users")
|
|
444
|
+
|
|
445
|
+
await Promise.all([
|
|
446
|
+
production.runtime.dispose(),
|
|
447
|
+
preview.runtime.dispose(),
|
|
448
|
+
])
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
A dependency belongs to the runtime that defined it and cannot be read or overridden in another one.
|
|
452
|
+
Pass `name` to `createRuntime` to see that name in error messages.
|
|
453
|
+
Every runtime has the same methods, and each has a module-level counterpart that targets the built-in runtime:
|
|
454
|
+
|
|
455
|
+
- `defineDependency`
|
|
456
|
+
- `install`
|
|
457
|
+
- `resolve`
|
|
458
|
+
- `createScope`
|
|
459
|
+
- `withOverrides`
|
|
460
|
+
- `createValueOverride`
|
|
461
|
+
- `createOverrideRunner`
|
|
462
|
+
- `dispose`
|
|
463
|
+
|
|
464
|
+
In that list, `resolve` is the explicit form of reading a value: `runtime.resolve(useDb)` returns the same thing as `useDb()`.
|
|
465
|
+
Everything shown earlier is that same API applied to the built-in runtime, so an ordinary application never creates or passes a runtime around.
|
|
466
|
+
|
|
467
|
+
## Diagnostics
|
|
468
|
+
|
|
469
|
+
During resolution and lifecycle management, Ripple DI reports mistakes with specific errors: `MissingProviderError` for a dependency with no provider, `DependencyCycleError` for a cycle, and `AsyncFactoryError` for a factory that returned a native promise without `asValue`.
|
|
470
|
+
Errors thrown by your own factory arrive wrapped in `FactoryError` with the original cause.
|
|
471
|
+
A failed factory is not cached, so the next read tries again.
|
|
472
|
+
|
|
473
|
+
These errors name the dependencies involved.
|
|
474
|
+
Pass `name` to give one a readable label instead of a generated one:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
const useConfig = defineDependency(loadConfig, { name: "config" })
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The name only affects messages.
|
|
481
|
+
|
|
482
|
+
## Limits
|
|
483
|
+
|
|
484
|
+
- Factories are synchronous; disposers may be asynchronous.
|
|
485
|
+
A factory returns the value itself, so a value that implements `then`, such as a query builder or another awaitable client, is stored as it is.
|
|
486
|
+
A factory is rejected only when it returns a native `Promise` object, because dependency reads made after an `await` are not tracked.
|
|
487
|
+
When the promise is the value, wrap the result in `asValue`: `defineDependency(() => asValue(loadToken()))` defines a `Promise<Token>` dependency created once and awaited by its readers.
|
|
488
|
+
- A `withOverrides` callback that returns an awaitable value has it awaited, exactly like any promise returned from a callback, so returning a query builder runs its query.
|
|
489
|
+
Use the value inside the callback, and use `createScope` when it has to outlive the callback.
|
|
490
|
+
Returning it wrapped in an object avoids the await but hands back a value whose scope is already closed, together with everything that scope owned.
|
|
491
|
+
- A factory can read dependencies, but cannot create, enter, close, or retire scopes and installations in its own runtime; misuse throws `FactoryScopeOperationError`.
|
|
492
|
+
- A disposer, and any async work it starts, cannot read dependencies or manage scopes and installations in the runtime being closed; misuse throws `DisposerContextError`.
|
|
493
|
+
Put everything cleanup needs into the dependency value itself.
|
|
494
|
+
- A factory reads from its own scope only; `scope.resolve` on a different scope throws `CrossScopeResolutionError`.
|
|
495
|
+
- Only the dependency calls made while a factory runs are tracked.
|
|
496
|
+
Reads from `process.env`, `Date.now()`, or another async context are not.
|
|
497
|
+
- An override factory cannot read the previous value of the dependency it replaces; define a base dependency and a decorated one instead.
|
|
498
|
+
- A factory-created value is scoped only by the dependencies its factory reads.
|
|
499
|
+
A value whose factory reads none stays shared even when it is first requested inside a scope.
|
|
500
|
+
- Read dependencies where you use them.
|
|
501
|
+
A module-level `const db = useDb()` freezes one scope's value forever.
|
|
502
|
+
- Two copies of Ripple DI, separately installed or bundled, each run their own graph and cannot be combined.
|
|
503
|
+
Do not pass dependencies or provisions between them, and do not call one copy's dependency inside a factory owned by the other.
|
|
504
|
+
|
|
505
|
+
## License
|
|
506
|
+
|
|
507
|
+
MIT
|