vla 0.1.1 โ 0.1.2
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 +127 -66
- package/dist/index.d.mts +254 -85
- package/dist/index.mjs +263 -74
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vla
|
|
2
|
+
|
|
3
|
+
_A smooth dutch dessert that goes with everything._
|
|
2
4
|
|
|
3
5
|
A TypeScript data layer kernel for backend- and fullstack apps. Compatible with whatever framework or library you're using.
|
|
4
6
|
|
|
@@ -13,22 +15,29 @@ A TypeScript data layer kernel for backend- and fullstack apps. Compatible with
|
|
|
13
15
|
- Tree shakeable
|
|
14
16
|
- Memoziation for Repos
|
|
15
17
|
- request-based context with AsyncLocalStorage
|
|
16
|
-
-
|
|
18
|
+
- first-class context injection
|
|
19
|
+
- ๐๏ธ auto tracing
|
|
20
|
+
- ๐๏ธ input validation
|
|
21
|
+
- ๐๏ธ error handling
|
|
17
22
|
- ...
|
|
18
23
|
|
|
19
24
|
## Why?
|
|
20
25
|
|
|
21
|
-
Many fullstack frameworks lack structure and conventions on the backend side (data layer), but they have lots of good structure and conventions on the frontend side (presentation layer). They are still great frameworks and they all have their own strengths. This is where
|
|
26
|
+
Many fullstack frameworks lack structure and conventions on the backend side (data layer), but they have lots of good structure and conventions on the frontend side (presentation layer). They are still great frameworks and they all have their own strengths. This is where Vla comes in. It aims to fill in the missing gap in the data layer, allowing you to write well-structured maintainable, scalable and testable code.
|
|
22
27
|
|
|
23
28
|
## Usage
|
|
24
29
|
|
|
25
30
|
```ts
|
|
26
|
-
import {
|
|
31
|
+
import { Kernel, Vla } from "vla"
|
|
27
32
|
|
|
28
|
-
// Users
|
|
29
|
-
|
|
33
|
+
// Create a Users module.
|
|
34
|
+
// Modules are optional and helpful for larger apps.
|
|
35
|
+
// Smaller apps might only need a single module.
|
|
36
|
+
// You can use the provided `Vla.Action`, `Vla.Service` etc as shortcuts
|
|
37
|
+
// instead of creating your own module
|
|
38
|
+
const Users = Vla.createModule("Users")
|
|
30
39
|
|
|
31
|
-
class
|
|
40
|
+
class ShowUserSettings extends Users.Action {
|
|
32
41
|
users = this.inject(UserService)
|
|
33
42
|
|
|
34
43
|
async handle(userId: string) {
|
|
@@ -40,10 +49,10 @@ class ShowUserSettingsAction extends UserModule.Action {
|
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
class UserService extends
|
|
52
|
+
class UserService extends Users.Service {
|
|
44
53
|
repo = this.inject(UserRepo)
|
|
45
54
|
billing = this.inject(BillingFacade)
|
|
46
|
-
|
|
55
|
+
session = this.inject(SessionFacade)
|
|
47
56
|
|
|
48
57
|
async getSettings(userId: string) {
|
|
49
58
|
await canViewProfile(userId)
|
|
@@ -57,31 +66,45 @@ class UserService extends UserModule.Service {
|
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
private canViewProfile(userId: string) {
|
|
69
|
+
private async canViewProfile(userId: string) {
|
|
61
70
|
const isSameUser = this.ctx.currentUser.id !== userId
|
|
62
71
|
if (!isSameUser) throw new Forbidden()
|
|
63
72
|
|
|
64
73
|
// repo method calls are memoized
|
|
65
74
|
const profile = await this.repo.findById(userId)
|
|
75
|
+
const currentUser = await this.session.currentUser()
|
|
66
76
|
const isTeamAdmin =
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
currentUser.role === "admin" &&
|
|
78
|
+
currentUser.teamId === profile.teamId
|
|
69
79
|
|
|
70
80
|
if (!isTeamAdmin) throw new Forbidden()
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
class
|
|
84
|
+
class SessionFacade extends Users.Service {
|
|
85
|
+
ctx = this.inject(ReqContext)
|
|
86
|
+
repo = this.inject(UserRepo)
|
|
87
|
+
|
|
88
|
+
async currentUser() {
|
|
89
|
+
const currentUserId = cookies.userId
|
|
90
|
+
const user = await this.repo.findById(currentUserId)
|
|
91
|
+
return user
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class UserRepo extends Users.Repo {
|
|
96
|
+
db = this.inject(Database)
|
|
97
|
+
|
|
75
98
|
findById = this.memo((id: string) => {
|
|
76
99
|
// this method is memoized per request.
|
|
77
100
|
// memoized methods can be called like any normal method, but
|
|
78
101
|
// if it's called multiple times with the same args, it's only
|
|
79
102
|
// executed once and the result is cached
|
|
80
|
-
return db.users.find({ id })
|
|
103
|
+
return this.db.users.find({ id })
|
|
81
104
|
})
|
|
82
105
|
|
|
83
106
|
async create(data: UserValues) {
|
|
84
|
-
const createdUser = await db.users.create({ data })
|
|
107
|
+
const createdUser = await this.db.users.create({ data })
|
|
85
108
|
|
|
86
109
|
this.findById.prime(createUser.id).value(createdUser)
|
|
87
110
|
// memoized methods support multiple utilities:
|
|
@@ -95,8 +118,21 @@ class UserRepo extends UserModule.Repo {
|
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
class Database extends Vla.Resource {
|
|
122
|
+
static override unwrap = "db"
|
|
123
|
+
// Unwraps a property when injecting the resource:
|
|
124
|
+
// When another class injects the database with `this.inject(Database)`,
|
|
125
|
+
// the `unwrap` will cause that it doesn't return the instance of the
|
|
126
|
+
// Database class, but the `db` property of it.
|
|
127
|
+
// This prevents that you have to write stuff like `this.db.db.find()`
|
|
128
|
+
|
|
129
|
+
// `Resource` classes are singletons,
|
|
130
|
+
// so the client this will only be initialized once
|
|
131
|
+
db = new DbClient()
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
// Billing
|
|
99
|
-
const
|
|
135
|
+
const Billing = Vla.createModule("Billing")
|
|
100
136
|
|
|
101
137
|
class BillingFacade extends Billing.Facade {
|
|
102
138
|
repo = this.inject(BillingRepo)
|
|
@@ -113,10 +149,16 @@ class BillingRepo extends BillingModule.Repo {
|
|
|
113
149
|
}
|
|
114
150
|
}
|
|
115
151
|
|
|
152
|
+
// Supports injecting context
|
|
153
|
+
const ReqContext = Vla.createContext<{ cookies: Record<string, unknown> }>()
|
|
154
|
+
|
|
116
155
|
const kernel = new Kernel()
|
|
117
|
-
|
|
156
|
+
Vla.setGlobalInvokeKernel(kernel) // define as global instance
|
|
157
|
+
|
|
158
|
+
// just an example. you should use a scoped context instead (see below)
|
|
159
|
+
kernel.context(ReqContext, { cookies: req.cookies })
|
|
118
160
|
|
|
119
|
-
const settings = await
|
|
161
|
+
const settings = await ShowUserSettings.invoke(userId)
|
|
120
162
|
// -> { timezone: 'GMT+1', hasSubscription: true }
|
|
121
163
|
```
|
|
122
164
|
|
|
@@ -124,23 +166,29 @@ const settings = await ShowUserSettingsAction.invoke(userId)
|
|
|
124
166
|
|
|
125
167
|
```tsx
|
|
126
168
|
import { cache } from 'react'
|
|
127
|
-
import {
|
|
169
|
+
import { Vla } from 'vla'
|
|
128
170
|
import { kernel } from '@/data/kernel'
|
|
129
171
|
|
|
130
172
|
const kernel = new Kernel()
|
|
131
173
|
|
|
132
|
-
// React's cache() will return a new scoped kernel for each request
|
|
133
|
-
|
|
134
|
-
|
|
174
|
+
// React's cache() will return a new scoped kernel for each request
|
|
175
|
+
Vla.setInvokeKernelProvider(cache(() => {
|
|
176
|
+
return kernel
|
|
177
|
+
.scoped()
|
|
178
|
+
.context(ReqContext, {
|
|
179
|
+
// you can either use await cookies() here or await the promise in Vla
|
|
180
|
+
cookies: cookies()
|
|
181
|
+
})
|
|
182
|
+
}))
|
|
135
183
|
|
|
136
184
|
async function Layout() {
|
|
137
|
-
const settings = await
|
|
185
|
+
const settings = await ShowUserSettings.invoke(userId)
|
|
138
186
|
|
|
139
187
|
return <div><Page /></div>
|
|
140
188
|
}
|
|
141
189
|
|
|
142
190
|
async function Page() {
|
|
143
|
-
const settings = await
|
|
191
|
+
const settings = await ShowUserSettings.invoke(userId)
|
|
144
192
|
// it will not query the db twice. it will use the memoized db query
|
|
145
193
|
|
|
146
194
|
return (
|
|
@@ -157,12 +205,12 @@ async function Page() {
|
|
|
157
205
|
e.g. sveltekit
|
|
158
206
|
|
|
159
207
|
```ts
|
|
160
|
-
import {
|
|
208
|
+
import { Vla } from "vla"
|
|
161
209
|
import type { Handle } from "@sveltejs/kit"
|
|
162
210
|
import { kernel } from '@/data/kernel'
|
|
163
211
|
|
|
164
212
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
165
|
-
return
|
|
213
|
+
return Vla.withKernel(kernel.scoped(), () => resolve(event))
|
|
166
214
|
}
|
|
167
215
|
|
|
168
216
|
import { kernel } from '@/data/kernel';
|
|
@@ -170,7 +218,7 @@ import type { PageServerLoad } from './$types';
|
|
|
170
218
|
|
|
171
219
|
export const load: PageServerLoad = async ({ params }) => {
|
|
172
220
|
return {
|
|
173
|
-
settings: await
|
|
221
|
+
settings: await ShowUserSettings.invoke(userId)
|
|
174
222
|
}
|
|
175
223
|
}
|
|
176
224
|
```
|
|
@@ -178,15 +226,18 @@ export const load: PageServerLoad = async ({ params }) => {
|
|
|
178
226
|
e.g. express
|
|
179
227
|
|
|
180
228
|
```ts
|
|
181
|
-
import {
|
|
229
|
+
import { Vla } from "vla"
|
|
182
230
|
import { kernel } from '@/data/kernel'
|
|
183
231
|
|
|
184
232
|
const app = express()
|
|
185
233
|
|
|
186
|
-
express.use((req, res, next) =>
|
|
234
|
+
express.use((req, res, next) => {
|
|
235
|
+
const scope = kernel.scoped().context(CustomContext, { req })
|
|
236
|
+
return Vla.withKernel(scope, () => next())
|
|
237
|
+
})
|
|
187
238
|
|
|
188
239
|
app.get("/users/:id", async (req, res) => {
|
|
189
|
-
const settings = await
|
|
240
|
+
const settings = await ShowUserSettings.invoke(req.params.id)
|
|
190
241
|
res.json({ data: settings })
|
|
191
242
|
})
|
|
192
243
|
```
|
|
@@ -217,7 +268,13 @@ test("returns some user settings", async () => {
|
|
|
217
268
|
),
|
|
218
269
|
)
|
|
219
270
|
|
|
220
|
-
|
|
271
|
+
// mock a context
|
|
272
|
+
kernel.bindValue(
|
|
273
|
+
ReqContext,
|
|
274
|
+
{ cookies: { currentUserId: 1 }}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
await expect(ShowUserSettings.withKernel(kernel).invoke("1")).resolves.toEqual({
|
|
221
278
|
timezone: "faked",
|
|
222
279
|
hasSubscription: true
|
|
223
280
|
})
|
|
@@ -230,30 +287,31 @@ There aren't any docs yet. This section is just jotting down some notes for myse
|
|
|
230
287
|
|
|
231
288
|
### When to use modules?
|
|
232
289
|
|
|
233
|
-
You don't need to create a module for each separate resource. Modules are meant for domain separation, not necessarily for resources.
|
|
290
|
+
You don't need to create a module for each separate resource. Modules are meant for domain separation, not necessarily for resources. A module can have multiple services, repositories and even multiple facades to separate resources from each other.
|
|
234
291
|
|
|
235
|
-
|
|
292
|
+
Smaller apps may just need a single module for the whole app. You can use `Vla.Action`, `Vla.Repo`, `Vla.Service` etc as a shortcut.
|
|
236
293
|
|
|
237
294
|
### What's a facade, when to use it?
|
|
238
295
|
|
|
239
|
-
Facades are meant as the internal public API to a module, for other modules.
|
|
296
|
+
Facades are meant as the internal public API to a module, for other modules.
|
|
297
|
+
|
|
298
|
+
When one module wants to use something from another module, it should do so by loading a facade, and not by deeply calling a service or repository of a module. Even though this adds a layer of indirection, it lets you better differentiate between internal and external concerns of a module, and prevents code dependencies from becoming messy. If you use facades excessively often, your domain modelling could be improved.
|
|
299
|
+
|
|
300
|
+
Vla prevents that any module can deeply inject any arbitrary class from another module. It only allows injecting Resources and Facades from other modules.
|
|
240
301
|
|
|
241
302
|
### How should I structure files and folders?
|
|
242
303
|
|
|
243
|
-
Roughly follow how
|
|
304
|
+
Roughly follow how Vla structures your code:
|
|
244
305
|
|
|
245
306
|
- Use separate folders for separate modules
|
|
246
307
|
- Create a separate file for each Service, Repo, Facade, Action
|
|
247
|
-
-
|
|
248
|
-
- If you further want to group files inside a module, do it either by resource (`user`, `post`, etc) or by type (`services`, `repos`, `actions`, etc). Start by having a flat hierarchy with all files of a module in a single folder, and separate into multiple folders once you feel it grew too much.
|
|
249
|
-
|
|
250
|
-
If you use multiple modules, it's good to separate them into different folders. This allows you to see: if you're importing unproportionally many files from other folders, you have lots of cross-module dependencies, and your data modelling into modules could be improved.
|
|
308
|
+
- If you further want to group files inside a module, do it either by resource (`user`, `post`, etc) or by type (`services`, `repos`, `actions`, etc). Start by having a flat hierarchy with all files of a module in a single folder, and separate into multiple folders once you feel it grows too much.
|
|
251
309
|
|
|
252
|
-
"How to structure code into files and folders" is often a question of how to manage code dependencies in a way that scales well.
|
|
310
|
+
"How to structure code into files and folders" is often a question of how to manage code dependencies in a way that scales well. Vla's structure already manages your code dependencies, so it makes sense to align your files and folders on Vla's structure.
|
|
253
311
|
|
|
254
312
|
### Class scopes
|
|
255
313
|
|
|
256
|
-
|
|
314
|
+
Vla uses dependency injection to load and use classes in other classes. This means that Vla creates class instances for you and caches them in different caches for different lifetimes. Whenever you call `inject()`, Vla checks the scope of the class and whether an instance is already cached.
|
|
257
315
|
|
|
258
316
|
There are 3 different scopes:
|
|
259
317
|
|
|
@@ -264,8 +322,9 @@ There are 3 different scopes:
|
|
|
264
322
|
Example:
|
|
265
323
|
|
|
266
324
|
```ts
|
|
267
|
-
class DatabasePool extends MyModule.
|
|
268
|
-
//
|
|
325
|
+
class DatabasePool extends MyModule.Resource {
|
|
326
|
+
// Resources are Singletons.
|
|
327
|
+
// Singletons are cached globally
|
|
269
328
|
private dbPool: DbPool | null = null
|
|
270
329
|
|
|
271
330
|
get dbPool() {
|
|
@@ -274,14 +333,12 @@ class DatabasePool extends MyModule.Singleton {
|
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
class Repo extends MyModule.Repo {
|
|
277
|
-
// repos are cached per request (
|
|
336
|
+
// repos are cached per request ("invoke" scope),
|
|
337
|
+
// so this map will be stateful for the current request scope
|
|
338
|
+
private cache = Map<string, User>()
|
|
278
339
|
|
|
279
|
-
// it will only create the db pool once and reuse it
|
|
280
|
-
// through the application's whole lifetime
|
|
281
340
|
dbPool = this.inject(DatabasePool)
|
|
282
341
|
|
|
283
|
-
private cache = Map<string, User>()
|
|
284
|
-
|
|
285
342
|
async findById(id: string) {
|
|
286
343
|
const cached = this.cache.get(id)
|
|
287
344
|
if (cached) return cached
|
|
@@ -293,8 +350,9 @@ class Repo extends MyModule.Repo {
|
|
|
293
350
|
}
|
|
294
351
|
|
|
295
352
|
class ServiceA extends MyModule.Service {
|
|
296
|
-
// it will use the same instance that ServiceB is also using
|
|
297
353
|
repo = this.inject(Repo)
|
|
354
|
+
// This repo instance is exactly the same instance as in ServiceB,
|
|
355
|
+
// so they both share the same `.cache`
|
|
298
356
|
|
|
299
357
|
async doSomething() {
|
|
300
358
|
await this.repo.findById("1")
|
|
@@ -302,8 +360,9 @@ class ServiceA extends MyModule.Service {
|
|
|
302
360
|
}
|
|
303
361
|
|
|
304
362
|
class ServiceB extends MyModule.Service {
|
|
305
|
-
// it will use the same instance that ServiceA is also using
|
|
306
363
|
repo = this.inject(Repo)
|
|
364
|
+
// This repo instance is exactly the same instance as in ServiceA,
|
|
365
|
+
// so they both share the same `.cache`
|
|
307
366
|
|
|
308
367
|
async doOtherThing() {
|
|
309
368
|
await this.repo.findById("1")
|
|
@@ -312,20 +371,21 @@ class ServiceB extends MyModule.Service {
|
|
|
312
371
|
```
|
|
313
372
|
|
|
314
373
|
In this example:
|
|
315
|
-
- The
|
|
316
|
-
- The Repo
|
|
317
|
-
-
|
|
374
|
+
- The `DatabasePool` will only be created once and will be reused forever
|
|
375
|
+
- The `Repo` will be stateful for each request. A new instance will be created for each request, and reused for the lifetime of the request.
|
|
376
|
+
- Both `ServiceA` and `ServiceB` will share the exact same `Repo` instance during a request.
|
|
318
377
|
|
|
319
378
|
### Overriding class scopes
|
|
320
379
|
|
|
321
380
|
```ts
|
|
322
|
-
class
|
|
323
|
-
static scope = FooClass.
|
|
381
|
+
class FooService extends MyModule.Service {
|
|
382
|
+
static scope = FooClass.ScopeTransient
|
|
383
|
+
// Services have a "invoke" scope by default. This overrides it to be
|
|
384
|
+
// "transient" instead. Transient classes get created separately for
|
|
385
|
+
// each usage and aren't cached at all.
|
|
324
386
|
}
|
|
325
387
|
```
|
|
326
388
|
|
|
327
|
-
You _can_ override the default scope of a Repo, Service, etc. But it's better to create a new BaseClass to not confuse things.
|
|
328
|
-
|
|
329
389
|
### Defining scope when injecting a class
|
|
330
390
|
|
|
331
391
|
Example:
|
|
@@ -340,17 +400,18 @@ class FooService extends MyModule.Service {
|
|
|
340
400
|
}
|
|
341
401
|
```
|
|
342
402
|
|
|
343
|
-
This will use a transient version of the FooRepo only
|
|
403
|
+
This will use a transient version of the FooRepo only inside the `FooService`. Meaning this service won't share the same cached version that other services might be using.
|
|
344
404
|
|
|
345
|
-
It's not something you'll likely need to use
|
|
405
|
+
It's not something you'll likely need to use often. But this shows how the scope defines how long the instance should be cached and shared.
|
|
346
406
|
|
|
347
407
|
### Base classes
|
|
348
408
|
|
|
349
|
-
|
|
409
|
+
Vla gives you multiple base classes with semantic names. You can use them as they are, or extend them for your own custom base classes.
|
|
410
|
+
|
|
411
|
+
- `Action` for server-side entry points (scope: `transient`)
|
|
412
|
+
- `Service` for reusable units of code (scope: `invoke`)
|
|
413
|
+
- `Repo` for data access and external adapters (scope: `invoke`)
|
|
414
|
+
- `Resource` for long-lived infrastructure clients such as database pools (scope: `singleton`)
|
|
415
|
+
- `Facade` for the interface of a module for cross-module access (scope: `transient`)
|
|
350
416
|
|
|
351
|
-
|
|
352
|
-
- `Singleton` is a `Class` with scope `singleton`
|
|
353
|
-
- `Service` is a `Class` with scope `invoke`
|
|
354
|
-
- `Repo` is a `Memoizable(Class)` with scope `invoke`
|
|
355
|
-
- `Facade` is a named alias for `Class`
|
|
356
|
-
- `Action` is a special class with scope `transient`, where you implement a `handle()` function.
|
|
417
|
+
Facades and Actions are similar in the way that they provide an interface into the module. The difference is that Actions are meant to be invoked from outside of a module, while Facades are meant to be called from inside of a module.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
2
|
type Scope = "singleton" | "invoke" | "transient";
|
|
3
3
|
type InstantiableClass<T> = new () => T;
|
|
4
|
-
//#endregion
|
|
5
|
-
//#region src/kernel.d.ts
|
|
6
4
|
type Token<T = unknown> = InstantiableClass<T> & {
|
|
7
5
|
readonly scope?: Scope;
|
|
6
|
+
readonly unwrap?: PropertyKey;
|
|
7
|
+
};
|
|
8
|
+
type UnwrapKey<TKey> = TKey extends {
|
|
9
|
+
readonly unwrap: infer K;
|
|
10
|
+
} ? K extends PropertyKey ? K : never : never;
|
|
11
|
+
type Resolved<TKey extends Token> = [UnwrapKey<TKey>] extends [never] ? InstanceType<TKey> : UnwrapKey<TKey> extends keyof InstanceType<TKey> ? InstanceType<TKey>[UnwrapKey<TKey>] : InstanceType<TKey>;
|
|
12
|
+
type Layer = "service" | "repo" | "action" | "resource" | "context";
|
|
13
|
+
type Visibility = "public" | "private" | "global" | "module";
|
|
14
|
+
type ModuleClass<ModuleName extends string, LayerName extends Layer = Layer, TVisibility extends Visibility = Visibility> = InstantiableClass<unknown> & {
|
|
15
|
+
readonly __vla_layer: LayerName;
|
|
16
|
+
readonly __vla_module: ModuleName;
|
|
17
|
+
readonly __vla_visibility: TVisibility;
|
|
18
|
+
readonly scope: Scope;
|
|
19
|
+
readonly unwrap?: PropertyKey;
|
|
20
|
+
readonly parentLayers: readonly Layer[];
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/concerns/memo.d.ts
|
|
24
|
+
type BaseCtor$1<T = object> = abstract new (...args: any[]) => T;
|
|
25
|
+
type ArgsTuple = unknown[];
|
|
26
|
+
type Memoized<Args extends ArgsTuple, R> = ((...args: Args) => R) & {
|
|
27
|
+
memoized: (...args: Args) => R;
|
|
28
|
+
fresh: (...args: Args) => R;
|
|
29
|
+
prime: (...args: Args) => {
|
|
30
|
+
value: (value: R) => void;
|
|
31
|
+
};
|
|
32
|
+
preload: (...args: Args) => R;
|
|
33
|
+
bust: (...args: Args) => void;
|
|
34
|
+
bustAll: () => void;
|
|
8
35
|
};
|
|
36
|
+
declare function Memoizable<TBase extends BaseCtor$1>(Base: TBase): ((abstract new (...args: any[]) => {
|
|
37
|
+
memo<Args extends ArgsTuple, R>(fn: (...args: Args) => R): Memoized<Args, R>;
|
|
38
|
+
}) & TBase) & TBase;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/kernel/kernel.d.ts
|
|
9
41
|
type Ctor<T = unknown> = InstantiableClass<T> & {
|
|
10
42
|
readonly scope?: Scope;
|
|
11
43
|
};
|
|
@@ -20,119 +52,256 @@ declare class Kernel {
|
|
|
20
52
|
});
|
|
21
53
|
private fork;
|
|
22
54
|
private getBinding;
|
|
23
|
-
bind<
|
|
55
|
+
bind<TKey extends Token>(key: TKey, impl: Ctor<unknown>, scope?: Scope): void;
|
|
56
|
+
bindValue<TKey extends Token>(key: TKey, value: Resolved<TKey>, scope?: Scope): void;
|
|
57
|
+
context<TKey extends Token>(key: TKey, value: Resolved<TKey>): this;
|
|
24
58
|
resolve<T>(key: Token<T>, scope?: Scope): T;
|
|
59
|
+
get<T>(key: Token<T>, scope?: Scope): T;
|
|
25
60
|
scoped(): Kernel;
|
|
26
61
|
create<T>(cls: Ctor<T>): T;
|
|
27
62
|
private instantiate;
|
|
28
63
|
private injectInto;
|
|
29
64
|
}
|
|
30
65
|
//#endregion
|
|
31
|
-
//#region src/
|
|
32
|
-
declare
|
|
66
|
+
//#region src/classes/action.d.ts
|
|
67
|
+
declare abstract class BaseAction {
|
|
68
|
+
static readonly __vla_layer: "action";
|
|
69
|
+
static readonly __vla_module: string;
|
|
70
|
+
static readonly __vla_visibility: "private";
|
|
71
|
+
static readonly scope: "transient";
|
|
72
|
+
static readonly parentLayers: readonly [];
|
|
73
|
+
abstract handle(...args: unknown[]): unknown | Promise<unknown>;
|
|
74
|
+
/** Executes the action with the arguments of the handler. */
|
|
75
|
+
static invoke<TAction extends BaseAction, TResult = ReturnType<TAction["handle"]>>(this: new () => TAction, ...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Helper to invoke an action with a kernel instance.
|
|
78
|
+
* @example
|
|
79
|
+
* ExampleAction.withKernel(kernel).invoke(...)
|
|
80
|
+
*/
|
|
81
|
+
static withKernel<TAction extends BaseAction>(this: new () => TAction, kernel: Kernel): {
|
|
82
|
+
/** Executes the action with the arguments of the handler. */
|
|
83
|
+
invoke<TResult = ReturnType<TAction["handle"]>>(...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
33
86
|
//#endregion
|
|
34
|
-
//#region src/kernel-
|
|
35
|
-
|
|
36
|
-
declare function setCurrentKernelFn(fnKernel: CurrentKernelFn): void;
|
|
87
|
+
//#region src/kernel/scoped-als.d.ts
|
|
88
|
+
declare function withKernel<T>(scopedKernel: Kernel, fn: () => T): T;
|
|
37
89
|
//#endregion
|
|
38
|
-
//#region src/kernel-global.d.ts
|
|
39
|
-
declare function
|
|
90
|
+
//#region src/kernel/scoped-global.d.ts
|
|
91
|
+
declare function setGlobalInvokeKernel(kernel: Kernel): void;
|
|
40
92
|
//#endregion
|
|
41
|
-
//#region src/
|
|
42
|
-
type
|
|
43
|
-
|
|
44
|
-
memoized: (...args: Args) => R;
|
|
45
|
-
fresh: (...args: Args) => R;
|
|
46
|
-
prime: (...args: Args) => {
|
|
47
|
-
value: (value: R) => void;
|
|
48
|
-
};
|
|
49
|
-
preload: (...args: Args) => R;
|
|
50
|
-
bust: (...args: Args) => void;
|
|
51
|
-
bustAll: () => void;
|
|
52
|
-
};
|
|
93
|
+
//#region src/kernel/scoped-provider.d.ts
|
|
94
|
+
type CurrentKernelFn = () => Kernel | Promise<Kernel>;
|
|
95
|
+
declare function setInvokeKernelProvider(fn: CurrentKernelFn): void;
|
|
53
96
|
//#endregion
|
|
54
|
-
//#region src/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
//#region src/concerns/devstable.d.ts
|
|
98
|
+
type BaseCtor<T = object> = abstract new (...args: any[]) => T;
|
|
99
|
+
declare function DevStable<TBase extends BaseCtor>(Base: TBase): ((abstract new (...args: any[]) => {
|
|
100
|
+
devStable<T>(key: string, init: () => T): T;
|
|
101
|
+
}) & TBase) & TBase;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/factories/context.d.ts
|
|
104
|
+
type VlaContext<TCtx extends Record<PropertyKey, unknown>> = ModuleClass<"VlaBuiltIn", "context"> & {
|
|
105
|
+
readonly unwrap: "value";
|
|
106
|
+
readonly scope: "invoke";
|
|
107
|
+
readonly __vla_visibility: "global";
|
|
108
|
+
readonly parentLayers: ["action", "service", "repo", "resource", "context"];
|
|
109
|
+
} & (abstract new (...args: any[]) => {
|
|
110
|
+
value: TCtx;
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Create a context that can be injected and accessed in all Actions, Services,
|
|
114
|
+
* Repos, Facades and Resources.
|
|
115
|
+
* @example
|
|
116
|
+
* const AppContext = createContext<{ userId: number }>()
|
|
117
|
+
* const scopedKernel = kernel.scoped().context(AppContext, { userId: 123 })
|
|
118
|
+
* ExampleAction.withKernel(scopedKernel).invoke()
|
|
119
|
+
*/
|
|
120
|
+
declare function createContext<TCtx extends Record<PropertyKey, unknown>>(): VlaContext<TCtx>;
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/factories/module.d.ts
|
|
72
123
|
declare function createModule<const ModuleName extends string>(moduleName: ModuleName): {
|
|
73
124
|
Facade: (abstract new () => {
|
|
74
|
-
inject
|
|
125
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
126
|
+
readonly parentLayers: readonly Layer[];
|
|
127
|
+
} ? TKey extends ModuleClass<ModuleName, Layer> ? "service" extends infer T ? T extends "service" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
128
|
+
readonly __vla_visibility: "global" | "public";
|
|
129
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
75
130
|
}) & {
|
|
76
|
-
|
|
77
|
-
readonly
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
131
|
+
readonly __vla_module: ModuleName;
|
|
132
|
+
readonly __vla_layer: "service";
|
|
133
|
+
readonly __vla_visibility: "global";
|
|
134
|
+
readonly scope: Scope;
|
|
135
|
+
readonly parentLayers: readonly ["action", "service", "repo", "resource"];
|
|
136
|
+
ScopeInvoke: Scope;
|
|
137
|
+
ScopeTransient: Scope;
|
|
138
|
+
ScopeSingleton: Scope;
|
|
81
139
|
};
|
|
82
140
|
Service: (abstract new () => {
|
|
83
|
-
inject
|
|
141
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
142
|
+
readonly parentLayers: readonly Layer[];
|
|
143
|
+
} ? TKey extends ModuleClass<ModuleName, Layer> ? "service" extends infer T ? T extends "service" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
144
|
+
readonly __vla_visibility: "global" | "public";
|
|
145
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
84
146
|
}) & {
|
|
85
|
-
|
|
86
|
-
readonly
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
147
|
+
readonly __vla_module: ModuleName;
|
|
148
|
+
readonly __vla_layer: "service";
|
|
149
|
+
readonly __vla_visibility: "module";
|
|
150
|
+
readonly scope: Scope;
|
|
151
|
+
readonly parentLayers: readonly ["action", "service"];
|
|
152
|
+
ScopeInvoke: Scope;
|
|
153
|
+
ScopeTransient: Scope;
|
|
154
|
+
ScopeSingleton: Scope;
|
|
90
155
|
};
|
|
91
156
|
Repo: (abstract new () => {
|
|
92
|
-
inject
|
|
157
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
158
|
+
readonly parentLayers: readonly Layer[];
|
|
159
|
+
} ? TKey extends ModuleClass<ModuleName, Layer> ? "repo" extends infer T ? T extends "repo" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
160
|
+
readonly __vla_visibility: "global" | "public";
|
|
161
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
93
162
|
memo<Args extends unknown[], R>(fn: (...args: Args) => R): Memoized<Args, R>;
|
|
94
163
|
}) & {
|
|
95
|
-
|
|
96
|
-
readonly
|
|
164
|
+
readonly __vla_module: ModuleName;
|
|
165
|
+
readonly __vla_layer: "repo";
|
|
166
|
+
readonly __vla_visibility: "module";
|
|
167
|
+
readonly scope: Scope;
|
|
168
|
+
readonly parentLayers: readonly ["action", "service", "repo"];
|
|
169
|
+
ScopeInvoke: Scope;
|
|
170
|
+
ScopeTransient: Scope;
|
|
171
|
+
ScopeSingleton: Scope;
|
|
97
172
|
};
|
|
98
173
|
Action: (abstract new () => {
|
|
174
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
175
|
+
readonly parentLayers: readonly Layer[];
|
|
176
|
+
} ? TKey extends ModuleClass<ModuleName, Layer> ? "action" extends infer T ? T extends "action" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
177
|
+
readonly __vla_visibility: "global" | "public";
|
|
178
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
99
179
|
handle(...args: unknown[]): unknown | Promise<unknown>;
|
|
100
|
-
inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
|
|
101
180
|
}) & {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}>(this: new () => TAction, kernel: Kernel): {
|
|
111
|
-
invoke<TResult = ReturnType<TAction["handle"]>>(...args: Parameters<TAction["handle"]>): TResult;
|
|
181
|
+
readonly __vla_module: ModuleName;
|
|
182
|
+
readonly __vla_layer: "action";
|
|
183
|
+
readonly __vla_visibility: "private";
|
|
184
|
+
readonly scope: "transient";
|
|
185
|
+
readonly parentLayers: readonly [];
|
|
186
|
+
invoke<TAction extends BaseAction, TResult = ReturnType<TAction["handle"]>>(this: new () => TAction, ...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
187
|
+
withKernel<TAction extends BaseAction>(this: new () => TAction, kernel: Kernel): {
|
|
188
|
+
invoke<TResult = ReturnType<TAction["handle"]>>(...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
112
189
|
};
|
|
113
|
-
readonly [BRAND]: ClassBrand<ModuleName, "action">;
|
|
114
|
-
InvokeScope: Scope;
|
|
115
|
-
TransientScope: Scope;
|
|
116
|
-
SingletonScope: Scope;
|
|
117
190
|
};
|
|
118
|
-
|
|
119
|
-
inject
|
|
191
|
+
Resource: (abstract new () => {
|
|
192
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
193
|
+
readonly parentLayers: readonly Layer[];
|
|
194
|
+
} ? TKey extends ModuleClass<ModuleName, Layer> ? "resource" extends infer T ? T extends "resource" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
195
|
+
readonly __vla_visibility: "global" | "public";
|
|
196
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
197
|
+
devStable<T>(key: string, init: () => T): T;
|
|
198
|
+
}) & {
|
|
199
|
+
readonly __vla_module: ModuleName;
|
|
200
|
+
readonly __vla_layer: "resource";
|
|
201
|
+
readonly __vla_visibility: "global";
|
|
202
|
+
readonly scope: Scope;
|
|
203
|
+
readonly parentLayers: readonly ["action", "service", "repo", "resource"];
|
|
204
|
+
ScopeInvoke: Scope;
|
|
205
|
+
ScopeTransient: Scope;
|
|
206
|
+
ScopeSingleton: Scope;
|
|
207
|
+
};
|
|
208
|
+
Memoizable: typeof Memoizable;
|
|
209
|
+
DevStable: typeof DevStable;
|
|
210
|
+
};
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/index.d.ts
|
|
213
|
+
declare const Vla: {
|
|
214
|
+
createModule: typeof createModule;
|
|
215
|
+
createContext: typeof createContext;
|
|
216
|
+
withKernel: typeof withKernel;
|
|
217
|
+
setGlobalInvokeKernel: typeof setGlobalInvokeKernel;
|
|
218
|
+
setInvokeKernelProvider: typeof setInvokeKernelProvider;
|
|
219
|
+
Facade: (abstract new () => {
|
|
220
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
221
|
+
readonly parentLayers: readonly Layer[];
|
|
222
|
+
} ? TKey extends ModuleClass<"Vla", Layer> ? "service" extends infer T ? T extends "service" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
223
|
+
readonly __vla_visibility: "global" | "public";
|
|
224
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
225
|
+
}) & {
|
|
226
|
+
readonly __vla_module: "Vla";
|
|
227
|
+
readonly __vla_layer: "service";
|
|
228
|
+
readonly __vla_visibility: "global";
|
|
229
|
+
readonly scope: Scope;
|
|
230
|
+
readonly parentLayers: readonly ["action", "service", "repo", "resource"];
|
|
231
|
+
ScopeInvoke: Scope;
|
|
232
|
+
ScopeTransient: Scope;
|
|
233
|
+
ScopeSingleton: Scope;
|
|
234
|
+
};
|
|
235
|
+
Service: (abstract new () => {
|
|
236
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
237
|
+
readonly parentLayers: readonly Layer[];
|
|
238
|
+
} ? TKey extends ModuleClass<"Vla", Layer> ? "service" extends infer T ? T extends "service" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
239
|
+
readonly __vla_visibility: "global" | "public";
|
|
240
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
120
241
|
}) & {
|
|
121
|
-
|
|
122
|
-
readonly
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
242
|
+
readonly __vla_module: "Vla";
|
|
243
|
+
readonly __vla_layer: "service";
|
|
244
|
+
readonly __vla_visibility: "module";
|
|
245
|
+
readonly scope: Scope;
|
|
246
|
+
readonly parentLayers: readonly ["action", "service"];
|
|
247
|
+
ScopeInvoke: Scope;
|
|
248
|
+
ScopeTransient: Scope;
|
|
249
|
+
ScopeSingleton: Scope;
|
|
250
|
+
};
|
|
251
|
+
Repo: (abstract new () => {
|
|
252
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
253
|
+
readonly parentLayers: readonly Layer[];
|
|
254
|
+
} ? TKey extends ModuleClass<"Vla", Layer> ? "repo" extends infer T ? T extends "repo" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
255
|
+
readonly __vla_visibility: "global" | "public";
|
|
256
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
257
|
+
memo<Args extends unknown[], R>(fn: (...args: Args) => R): Memoized<Args, R>;
|
|
258
|
+
}) & {
|
|
259
|
+
readonly __vla_module: "Vla";
|
|
260
|
+
readonly __vla_layer: "repo";
|
|
261
|
+
readonly __vla_visibility: "module";
|
|
262
|
+
readonly scope: Scope;
|
|
263
|
+
readonly parentLayers: readonly ["action", "service", "repo"];
|
|
264
|
+
ScopeInvoke: Scope;
|
|
265
|
+
ScopeTransient: Scope;
|
|
266
|
+
ScopeSingleton: Scope;
|
|
267
|
+
};
|
|
268
|
+
Action: (abstract new () => {
|
|
269
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
270
|
+
readonly parentLayers: readonly Layer[];
|
|
271
|
+
} ? TKey extends ModuleClass<"Vla", Layer> ? "action" extends infer T ? T extends "action" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
272
|
+
readonly __vla_visibility: "global" | "public";
|
|
273
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
274
|
+
handle(...args: unknown[]): unknown | Promise<unknown>;
|
|
275
|
+
}) & {
|
|
276
|
+
readonly __vla_module: "Vla";
|
|
277
|
+
readonly __vla_layer: "action";
|
|
278
|
+
readonly __vla_visibility: "private";
|
|
279
|
+
readonly scope: "transient";
|
|
280
|
+
readonly parentLayers: readonly [];
|
|
281
|
+
invoke<TAction extends BaseAction, TResult = ReturnType<TAction["handle"]>>(this: new () => TAction, ...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
282
|
+
withKernel<TAction extends BaseAction>(this: new () => TAction, kernel: Kernel): {
|
|
283
|
+
invoke<TResult = ReturnType<TAction["handle"]>>(...args: Parameters<TAction["handle"]>): Promise<TResult>;
|
|
284
|
+
};
|
|
126
285
|
};
|
|
127
|
-
|
|
128
|
-
inject
|
|
286
|
+
Resource: (abstract new () => {
|
|
287
|
+
inject<TKey extends ModuleClass<string>>(key: TKey extends ModuleClass<string, infer TargetLayer extends Layer> ? TKey extends {
|
|
288
|
+
readonly parentLayers: readonly Layer[];
|
|
289
|
+
} ? TKey extends ModuleClass<"Vla", Layer> ? "resource" extends infer T ? T extends "resource" ? T extends TKey["parentLayers"][number] ? TKey : `A ${Capitalize<T>} is not allowed to inject a ${Capitalize<TargetLayer>}. Allowed parent layer: ${TKey["parentLayers"][number] extends Layer ? Capitalize<TKey["parentLayers"][number]> : never}` : never : never : TKey extends {
|
|
290
|
+
readonly __vla_visibility: "global" | "public";
|
|
291
|
+
} ? TKey : `Cross-module ${Capitalize<TargetLayer>} dependency is not allowed. Use a Facade or Resource.` : never : TKey, scope?: Scope): Resolved<TKey>;
|
|
292
|
+
devStable<T>(key: string, init: () => T): T;
|
|
129
293
|
}) & {
|
|
130
|
-
|
|
131
|
-
readonly
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
294
|
+
readonly __vla_module: "Vla";
|
|
295
|
+
readonly __vla_layer: "resource";
|
|
296
|
+
readonly __vla_visibility: "global";
|
|
297
|
+
readonly scope: Scope;
|
|
298
|
+
readonly parentLayers: readonly ["action", "service", "repo", "resource"];
|
|
299
|
+
ScopeInvoke: Scope;
|
|
300
|
+
ScopeTransient: Scope;
|
|
301
|
+
ScopeSingleton: Scope;
|
|
135
302
|
};
|
|
303
|
+
Memoizable: typeof Memoizable;
|
|
304
|
+
DevStable: typeof DevStable;
|
|
136
305
|
};
|
|
137
306
|
//#endregion
|
|
138
|
-
export { Kernel, createModule,
|
|
307
|
+
export { InstantiableClass, Kernel, Layer, ModuleClass, Resolved, Scope, Token, UnwrapKey, Visibility, Vla, createContext, createModule, setGlobalInvokeKernel, setInvokeKernelProvider, withKernel };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import objectHash from "object-hash";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/classes/context.ts
|
|
5
|
+
var BaseContext = class {
|
|
6
|
+
static __vla_layer = "context";
|
|
7
|
+
static __vla_module = "VlaBuiltIn";
|
|
8
|
+
static __vla_visibility = "global";
|
|
9
|
+
static scope = "invoke";
|
|
10
|
+
static parentLayers = [
|
|
11
|
+
"action",
|
|
12
|
+
"service",
|
|
13
|
+
"repo",
|
|
14
|
+
"resource",
|
|
15
|
+
"context"
|
|
16
|
+
];
|
|
17
|
+
static unwrap = "value";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/factories/context.ts
|
|
22
|
+
/**
|
|
23
|
+
* Create a context that can be injected and accessed in all Actions, Services,
|
|
24
|
+
* Repos, Facades and Resources.
|
|
25
|
+
* @example
|
|
26
|
+
* const AppContext = createContext<{ userId: number }>()
|
|
27
|
+
* const scopedKernel = kernel.scoped().context(AppContext, { userId: 123 })
|
|
28
|
+
* ExampleAction.withKernel(scopedKernel).invoke()
|
|
29
|
+
*/
|
|
30
|
+
function createContext() {
|
|
31
|
+
class Context extends BaseContext {}
|
|
32
|
+
return Context;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/lib/tokenizeDeps.ts
|
|
5
37
|
var UnresolvedDependency = class {
|
|
6
38
|
constructor(token, scope) {
|
|
7
39
|
this.token = token;
|
|
@@ -11,13 +43,13 @@ var UnresolvedDependency = class {
|
|
|
11
43
|
function tokenizedDependency(defaultClass, scope) {
|
|
12
44
|
return new UnresolvedDependency(defaultClass, scope);
|
|
13
45
|
}
|
|
14
|
-
function
|
|
46
|
+
function getTokenizedDependency(v) {
|
|
15
47
|
if (v instanceof UnresolvedDependency) return v;
|
|
16
48
|
return null;
|
|
17
49
|
}
|
|
18
50
|
|
|
19
51
|
//#endregion
|
|
20
|
-
//#region src/kernel.ts
|
|
52
|
+
//#region src/kernel/kernel.ts
|
|
21
53
|
var Kernel = class Kernel {
|
|
22
54
|
singletons = /* @__PURE__ */ new Map();
|
|
23
55
|
invokeCache = /* @__PURE__ */ new Map();
|
|
@@ -37,12 +69,39 @@ var Kernel = class Kernel {
|
|
|
37
69
|
bind(key, impl, scope = "transient") {
|
|
38
70
|
this.singletons.delete(key);
|
|
39
71
|
this.bindings.set(key, {
|
|
72
|
+
kind: "class",
|
|
40
73
|
impl,
|
|
41
74
|
scope
|
|
42
75
|
});
|
|
43
76
|
}
|
|
77
|
+
bindValue(key, value, scope = "singleton") {
|
|
78
|
+
this.singletons.delete(key);
|
|
79
|
+
this.bindings.set(key, {
|
|
80
|
+
kind: "value",
|
|
81
|
+
value,
|
|
82
|
+
scope
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
context(key, value) {
|
|
86
|
+
this.bindValue(key, value, "invoke");
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
44
89
|
resolve(key, scope) {
|
|
45
90
|
const binding = this.getBinding(key);
|
|
91
|
+
if (binding?.kind === "value") {
|
|
92
|
+
const requestedScope$1 = binding?.scope ?? scope ?? "singleton";
|
|
93
|
+
if (requestedScope$1 === "singleton") {
|
|
94
|
+
if (this.root.singletons.has(key)) return this.root.singletons.get(key);
|
|
95
|
+
this.root.singletons.set(key, binding.value);
|
|
96
|
+
return binding.value;
|
|
97
|
+
}
|
|
98
|
+
if (requestedScope$1 === "invoke" && !!this.parent) {
|
|
99
|
+
if (this.invokeCache.has(key)) return this.invokeCache.get(key);
|
|
100
|
+
this.invokeCache.set(key, binding.value);
|
|
101
|
+
return binding.value;
|
|
102
|
+
}
|
|
103
|
+
return binding.value;
|
|
104
|
+
}
|
|
46
105
|
const impl = binding?.impl ?? key;
|
|
47
106
|
const requestedScope = binding?.scope ?? scope ?? impl.scope;
|
|
48
107
|
if (requestedScope === "singleton") {
|
|
@@ -59,6 +118,16 @@ var Kernel = class Kernel {
|
|
|
59
118
|
}
|
|
60
119
|
return this.instantiate(impl);
|
|
61
120
|
}
|
|
121
|
+
get(key, scope) {
|
|
122
|
+
const dependency = this.resolve(key, scope);
|
|
123
|
+
if (this.getBinding(key)?.kind === "value") return dependency;
|
|
124
|
+
const unwrapKey = getUnwrapKey(key);
|
|
125
|
+
if (!unwrapKey) return dependency;
|
|
126
|
+
if (typeof dependency === "object" && dependency !== null || typeof dependency === "function") {
|
|
127
|
+
if (unwrapKey in dependency) return dependency[unwrapKey];
|
|
128
|
+
}
|
|
129
|
+
return dependency;
|
|
130
|
+
}
|
|
62
131
|
scoped() {
|
|
63
132
|
return this.fork();
|
|
64
133
|
}
|
|
@@ -73,53 +142,195 @@ var Kernel = class Kernel {
|
|
|
73
142
|
injectInto(instance) {
|
|
74
143
|
const obj = instance;
|
|
75
144
|
Object.entries(obj).map(([key, value]) => {
|
|
76
|
-
const injectionPoint =
|
|
145
|
+
const injectionPoint = getTokenizedDependency(value);
|
|
77
146
|
if (!injectionPoint) return null;
|
|
78
147
|
return [key, injectionPoint];
|
|
79
148
|
}).filter((entry) => entry !== null).forEach(([key, injectionPoint]) => {
|
|
80
|
-
obj[key] = this.
|
|
149
|
+
obj[key] = this.get(injectionPoint.token, injectionPoint.scope);
|
|
81
150
|
});
|
|
82
151
|
}
|
|
83
152
|
};
|
|
153
|
+
function getUnwrapKey(v) {
|
|
154
|
+
if (typeof v !== "function" || v === null) return void 0;
|
|
155
|
+
if (!("unwrap" in v)) return void 0;
|
|
156
|
+
const k = v.unwrap;
|
|
157
|
+
return typeof k === "string" || typeof k === "number" || typeof k === "symbol" ? k : void 0;
|
|
158
|
+
}
|
|
84
159
|
|
|
85
160
|
//#endregion
|
|
86
|
-
//#region src/kernel-als.ts
|
|
161
|
+
//#region src/kernel/scoped-als.ts
|
|
87
162
|
const als = new AsyncLocalStorage();
|
|
88
|
-
function
|
|
163
|
+
function withKernel(scopedKernel, fn) {
|
|
89
164
|
return als.run(scopedKernel, fn);
|
|
90
165
|
}
|
|
91
|
-
function
|
|
166
|
+
function getAlsInvokeKernel() {
|
|
92
167
|
return als.getStore() ?? null;
|
|
93
168
|
}
|
|
94
169
|
|
|
95
170
|
//#endregion
|
|
96
|
-
//#region src/kernel-
|
|
171
|
+
//#region src/kernel/scoped-global.ts
|
|
172
|
+
let globalKernel = null;
|
|
173
|
+
function setGlobalInvokeKernel(kernel) {
|
|
174
|
+
globalKernel = kernel;
|
|
175
|
+
}
|
|
176
|
+
function getGlobalKernel() {
|
|
177
|
+
return globalKernel;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/kernel/scoped-provider.ts
|
|
97
182
|
let currentKernelFn = null;
|
|
98
|
-
function
|
|
99
|
-
currentKernelFn =
|
|
183
|
+
function setInvokeKernelProvider(fn) {
|
|
184
|
+
currentKernelFn = fn;
|
|
100
185
|
}
|
|
101
|
-
function
|
|
102
|
-
return currentKernelFn
|
|
186
|
+
async function getKernelProvider() {
|
|
187
|
+
return currentKernelFn ? await currentKernelFn() : void 0;
|
|
103
188
|
}
|
|
104
189
|
|
|
105
190
|
//#endregion
|
|
106
|
-
//#region src/kernel-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
globalKernel = kernel;
|
|
191
|
+
//#region src/kernel/scoped-invoke.ts
|
|
192
|
+
async function getInvokeKernel() {
|
|
193
|
+
return await getKernelProvider() ?? getAlsInvokeKernel() ?? getGlobalKernel()?.scoped() ?? new Kernel();
|
|
110
194
|
}
|
|
111
|
-
|
|
112
|
-
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/classes/action.ts
|
|
198
|
+
var BaseAction = class {
|
|
199
|
+
static __vla_layer = "action";
|
|
200
|
+
static __vla_module = "none";
|
|
201
|
+
static __vla_visibility = "private";
|
|
202
|
+
static scope = "transient";
|
|
203
|
+
static parentLayers = [];
|
|
204
|
+
/** Executes the action with the arguments of the handler. */
|
|
205
|
+
static async invoke(...args) {
|
|
206
|
+
return (await getInvokeKernel()).create(this).handle(...args);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Helper to invoke an action with a kernel instance.
|
|
210
|
+
* @example
|
|
211
|
+
* ExampleAction.withKernel(kernel).invoke(...)
|
|
212
|
+
*/
|
|
213
|
+
static withKernel(kernel) {
|
|
214
|
+
const ActionClass = this;
|
|
215
|
+
return { async invoke(...args) {
|
|
216
|
+
return kernel.scoped().create(ActionClass).handle(...args);
|
|
217
|
+
} };
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/classes/facade.ts
|
|
223
|
+
var BaseFacade = class {
|
|
224
|
+
static __vla_layer = "service";
|
|
225
|
+
static __vla_module = "none";
|
|
226
|
+
static __vla_visibility = "global";
|
|
227
|
+
static scope = "transient";
|
|
228
|
+
static parentLayers = [
|
|
229
|
+
"action",
|
|
230
|
+
"service",
|
|
231
|
+
"repo",
|
|
232
|
+
"resource"
|
|
233
|
+
];
|
|
234
|
+
static ScopeInvoke = "invoke";
|
|
235
|
+
static ScopeTransient = "transient";
|
|
236
|
+
static ScopeSingleton = "singleton";
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/classes/repo.ts
|
|
241
|
+
var BaseRepo = class {
|
|
242
|
+
static __vla_layer = "repo";
|
|
243
|
+
static __vla_module = "none";
|
|
244
|
+
static __vla_visibility = "module";
|
|
245
|
+
static scope = "invoke";
|
|
246
|
+
static parentLayers = [
|
|
247
|
+
"action",
|
|
248
|
+
"service",
|
|
249
|
+
"repo"
|
|
250
|
+
];
|
|
251
|
+
static ScopeInvoke = "invoke";
|
|
252
|
+
static ScopeTransient = "transient";
|
|
253
|
+
static ScopeSingleton = "singleton";
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/classes/resource.ts
|
|
258
|
+
var BaseResource = class {
|
|
259
|
+
static __vla_layer = "resource";
|
|
260
|
+
static __vla_module = "none";
|
|
261
|
+
static __vla_visibility = "global";
|
|
262
|
+
static scope = "singleton";
|
|
263
|
+
static parentLayers = [
|
|
264
|
+
"action",
|
|
265
|
+
"service",
|
|
266
|
+
"repo",
|
|
267
|
+
"resource"
|
|
268
|
+
];
|
|
269
|
+
static ScopeInvoke = "invoke";
|
|
270
|
+
static ScopeTransient = "transient";
|
|
271
|
+
static ScopeSingleton = "singleton";
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/classes/service.ts
|
|
276
|
+
var BaseService = class {
|
|
277
|
+
static __vla_layer = "service";
|
|
278
|
+
static __vla_module = "none";
|
|
279
|
+
static __vla_visibility = "module";
|
|
280
|
+
static scope = "invoke";
|
|
281
|
+
static parentLayers = ["action", "service"];
|
|
282
|
+
static ScopeInvoke = "invoke";
|
|
283
|
+
static ScopeTransient = "transient";
|
|
284
|
+
static ScopeSingleton = "singleton";
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/concerns/devstable.ts
|
|
289
|
+
const VLA_DEV_STABLE_PREFIX = "__vla_dev_stable__";
|
|
290
|
+
function isDevStableEnabled() {
|
|
291
|
+
if (process.env.VLA_DEV_STABLE === "1") return true;
|
|
292
|
+
return process.env.NODE_ENV !== "production";
|
|
293
|
+
}
|
|
294
|
+
function DevStable(Base) {
|
|
295
|
+
class DevStableBase extends Base {
|
|
296
|
+
devStable(key, init) {
|
|
297
|
+
if (!isDevStableEnabled()) return init();
|
|
298
|
+
const g = globalThis;
|
|
299
|
+
const fullKey = `${VLA_DEV_STABLE_PREFIX}${key}`;
|
|
300
|
+
if (!(fullKey in g)) g[fullKey] = init();
|
|
301
|
+
return g[fullKey];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return DevStableBase;
|
|
113
305
|
}
|
|
114
306
|
|
|
115
307
|
//#endregion
|
|
116
|
-
//#region src/
|
|
117
|
-
function
|
|
118
|
-
return
|
|
308
|
+
//#region src/concerns/injectable.ts
|
|
309
|
+
function createInjectable() {
|
|
310
|
+
return function Injectable(Base) {
|
|
311
|
+
class InjectableBase extends Base {
|
|
312
|
+
inject(key, scope) {
|
|
313
|
+
const parentClass = this.constructor;
|
|
314
|
+
const parentModule = parentClass.__vla_module;
|
|
315
|
+
const parentLayer = parentClass.__vla_layer;
|
|
316
|
+
const targetKey = key;
|
|
317
|
+
const targetModule = targetKey.__vla_module;
|
|
318
|
+
const targetLayer = targetKey.__vla_layer;
|
|
319
|
+
const targetParentLayers = targetKey.parentLayers;
|
|
320
|
+
if (!targetParentLayers.includes(parentLayer)) throw new Error(`Layer ${parentLayer} is not allowed to inject ${targetLayer}. Allowed parent layers for ${targetLayer}: ${targetParentLayers.join(", ")}`);
|
|
321
|
+
if (targetModule !== parentModule) {
|
|
322
|
+
const targetVisibility = targetKey.__vla_visibility;
|
|
323
|
+
if (targetVisibility === "private" || targetVisibility === "module") throw new Error(`Cross-module ${targetLayer} dependency is not allowed. Use a Facade or Resource. (Tried to inject a ${targetLayer} from ${targetModule} into ${parentModule})`);
|
|
324
|
+
}
|
|
325
|
+
return tokenizedDependency(targetKey, scope ?? targetKey.scope);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return InjectableBase;
|
|
329
|
+
};
|
|
119
330
|
}
|
|
120
331
|
|
|
121
332
|
//#endregion
|
|
122
|
-
//#region src/memo.ts
|
|
333
|
+
//#region src/concerns/memo.ts
|
|
123
334
|
function Memoizable(Base) {
|
|
124
335
|
class MemoizableBase extends Base {
|
|
125
336
|
memo(fn) {
|
|
@@ -165,67 +376,45 @@ function Memoizable(Base) {
|
|
|
165
376
|
}
|
|
166
377
|
|
|
167
378
|
//#endregion
|
|
168
|
-
//#region src/
|
|
169
|
-
const BRAND = Symbol("_capsel_brand");
|
|
170
|
-
var ClassBrand = class {
|
|
171
|
-
constructor(moduleName, layerName) {
|
|
172
|
-
this.moduleName = moduleName;
|
|
173
|
-
this.layerName = layerName;
|
|
174
|
-
}
|
|
175
|
-
};
|
|
379
|
+
//#region src/factories/module.ts
|
|
176
380
|
function createModule(moduleName) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
class
|
|
182
|
-
static
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
class Facade extends BaseClass {
|
|
188
|
-
static [BRAND] = new ClassBrand(moduleName, "facade");
|
|
189
|
-
static scope = "transient";
|
|
190
|
-
}
|
|
191
|
-
class Service extends BaseClass {
|
|
192
|
-
static [BRAND] = new ClassBrand(moduleName, "service");
|
|
193
|
-
static scope = "invoke";
|
|
194
|
-
}
|
|
195
|
-
class Repo extends Memoizable(BaseClass) {
|
|
196
|
-
static [BRAND] = new ClassBrand(moduleName, "repo");
|
|
197
|
-
static scope = "invoke";
|
|
198
|
-
}
|
|
199
|
-
class Action extends BaseClass {
|
|
200
|
-
static [BRAND] = new ClassBrand(moduleName, "action");
|
|
201
|
-
static scope = "transient";
|
|
202
|
-
static invoke(...args) {
|
|
203
|
-
return getInvokeKernel().create(this).handle(...args);
|
|
204
|
-
}
|
|
205
|
-
static withKernel(kernel) {
|
|
206
|
-
const ActionClass = this;
|
|
207
|
-
return { invoke(...args) {
|
|
208
|
-
return kernel.scoped().create(ActionClass).handle(...args);
|
|
209
|
-
} };
|
|
210
|
-
}
|
|
381
|
+
const Injectable = createInjectable();
|
|
382
|
+
class Facade extends Injectable(BaseFacade) {
|
|
383
|
+
static __vla_module = moduleName;
|
|
384
|
+
}
|
|
385
|
+
class Service extends Injectable(BaseService) {
|
|
386
|
+
static __vla_module = moduleName;
|
|
387
|
+
}
|
|
388
|
+
class Repo extends Injectable(Memoizable(BaseRepo)) {
|
|
389
|
+
static __vla_module = moduleName;
|
|
211
390
|
}
|
|
212
|
-
class
|
|
213
|
-
static
|
|
214
|
-
static scope = "singleton";
|
|
391
|
+
class Action extends Injectable(BaseAction) {
|
|
392
|
+
static __vla_module = moduleName;
|
|
215
393
|
}
|
|
216
|
-
class
|
|
217
|
-
static
|
|
218
|
-
static scope = "transient";
|
|
394
|
+
class Resource extends Injectable(DevStable(BaseResource)) {
|
|
395
|
+
static __vla_module = moduleName;
|
|
219
396
|
}
|
|
220
397
|
return {
|
|
221
398
|
Facade,
|
|
222
399
|
Service,
|
|
223
400
|
Repo,
|
|
224
401
|
Action,
|
|
225
|
-
|
|
226
|
-
|
|
402
|
+
Resource,
|
|
403
|
+
Memoizable,
|
|
404
|
+
DevStable
|
|
227
405
|
};
|
|
228
406
|
}
|
|
229
407
|
|
|
230
408
|
//#endregion
|
|
231
|
-
|
|
409
|
+
//#region src/index.ts
|
|
410
|
+
const Vla = {
|
|
411
|
+
...createModule("Vla"),
|
|
412
|
+
createModule,
|
|
413
|
+
createContext,
|
|
414
|
+
withKernel,
|
|
415
|
+
setGlobalInvokeKernel,
|
|
416
|
+
setInvokeKernelProvider
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
//#endregion
|
|
420
|
+
export { Kernel, Vla, createContext, createModule, setGlobalInvokeKernel, setInvokeKernelProvider, withKernel };
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vla",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Data layer kernel for backend- and fullstack apps",
|
|
6
6
|
"author": "Timo Mรคmecke <hello@timo.wtf>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.com/timomeh/
|
|
8
|
+
"homepage": "https://github.com/timomeh/vla#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/timomeh/
|
|
11
|
+
"url": "git+https://github.com/timomeh/vla.git"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/timomeh/
|
|
14
|
+
"url": "https://github.com/timomeh/vla/issues"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.mjs",
|