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 CHANGED
@@ -1,4 +1,6 @@
1
- # capsel
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
- - ๐Ÿ—๏ธ first-class context injection
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 Capsel 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.
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 { createModule, Kernel } from "capsel"
31
+ import { Kernel, Vla } from "vla"
27
32
 
28
- // Users
29
- const UserModule = createModule("User")
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 ShowUserSettingsAction extends UserModule.Action {
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 UserModule.Service {
52
+ class UserService extends Users.Service {
44
53
  repo = this.inject(UserRepo)
45
54
  billing = this.inject(BillingFacade)
46
- ctx = this.inject(Context) // WIP unimplemented
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
- this.ctx.currentUser.role === "admin" &&
68
- this.ctx.currentUser.teamId === profile.teamId
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 UserRepo extends UserModule.Repo {
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 BillingModule = createModule("Billing")
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
- kernel.setGlobal() // global instance
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 ShowUserSettingsAction.invoke(userId)
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 { setCurrentKernelFn } from 'capsel'
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, giving
133
- // us a new scoped kernel per request without a middleware
134
- setCurrentKernelFn(cache(() => kernel.scoped()))
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 ShowUserSettingsAction.invoke(userId)
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 ShowUserSettingsAction.invoke(userId)
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 { runWithScope } from "capsel"
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 runWithScope(kernel.scoped(), () => resolve(event))
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 ShowUserSettingsAction.invoke(userId)
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 { runWithScope } from "capsel"
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) => runWithScope(kernel.scoped(), () => 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 ShowUserSettingsAction.invoke(req.params.id)
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
- await expect(ShowUserSettingsAction.withKernel(kernel).invoke("1")).resolves.toEqual({
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. Smaller apps may just need a single `AppModule`. Start with a single AppModule and grow.
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
- A module can have multiple services, repositories and even multiple facades to separate resources from each other.
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. When one module wants to call something from another module, it should do so through 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.
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 capsel structures your code:
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
- - You could also have all actions of a resource in a single file
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. Capsel's structure already manages your code dependencies, so it makes sense to align your files and folders on capsel's structure.
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
- Capsel uses dependency injection to load and use classes in other classes. This means that Capsel creates class instances for you and caches them in different caches for different lifetimes. Whenever you call `inject()`, capsel checks the scope of the class and whether an instance is already cached.
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.Singleton {
268
- // singletons are cached globally
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 (`invoke`)
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 Database Pool will only be created once and will be reused forever
316
- - The Repo is stateful for each request. The Repo cache can be shared across all usages
317
- - The Services can both call the Repo and use the same instance
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 FooClass extends MyModule.Class {
323
- static scope = FooClass.SingletonScope
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 for the `FooService`. Meaning this service won't share the same cached version that other services might be using.
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 at all. But this shows how the scope defines how long the instance should be cached and shared.
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
- Capsel gives you multiple base classes to choose from. You can use them as they are or extend them for your own custom base classes.
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
- - `Class` is a generic class with scope `transient`
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<T>(key: Token<T>, impl: Ctor<unknown>, scope?: Scope): void;
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/kernel-als.d.ts
32
- declare function runWithKernel<T>(scopedKernel: Kernel, fn: () => T): T;
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-current.d.ts
35
- type CurrentKernelFn = () => Kernel;
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 setGlobalKernel(kernel: Kernel): void;
90
+ //#region src/kernel/scoped-global.d.ts
91
+ declare function setGlobalInvokeKernel(kernel: Kernel): void;
40
92
  //#endregion
41
- //#region src/memo.d.ts
42
- type ArgsTuple = unknown[];
43
- type Memoized<Args extends ArgsTuple, R> = ((...args: Args) => R) & {
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/modules.d.ts
55
- declare const BRAND: unique symbol;
56
- type Layer = "facade" | "service" | "repo" | "action" | "other";
57
- type Branded<ModuleName extends string, LayerName extends Layer> = {
58
- readonly [BRAND]: ClassBrand<ModuleName, LayerName>;
59
- };
60
- type Scoped = {
61
- readonly scope: Scope;
62
- };
63
- type ModuleClass<ModuleName extends string, LayerName extends Layer = Layer> = InstantiableClass<unknown> & Branded<ModuleName, LayerName> & Scoped;
64
- type LayerOf<T> = T extends ModuleClass<string, infer L> ? L : never;
65
- type ForbiddenCrossModuleClass = ModuleClass<string, Exclude<Layer, "facade">>;
66
- type AllowedDependency<ModuleName extends string, Key> = Key extends ModuleClass<ModuleName, Layer> ? Key : Key extends ForbiddenCrossModuleClass ? `Cross-module ${Capitalize<LayerOf<Key>>} injection is not allowed. Use a Facade.` : Key;
67
- declare class ClassBrand<ModuleName extends string, LayerName extends Layer> {
68
- readonly moduleName: ModuleName;
69
- readonly layerName: LayerName;
70
- constructor(moduleName: ModuleName, layerName: LayerName);
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: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- scope: Scope;
77
- readonly [BRAND]: ClassBrand<ModuleName, "facade">;
78
- InvokeScope: Scope;
79
- TransientScope: Scope;
80
- SingletonScope: Scope;
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: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- scope: Scope;
86
- readonly [BRAND]: ClassBrand<ModuleName, "service">;
87
- InvokeScope: Scope;
88
- TransientScope: Scope;
89
- SingletonScope: Scope;
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: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- scope: Scope;
96
- readonly [BRAND]: ClassBrand<ModuleName, "repo">;
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
- scope: Scope;
103
- invoke<TAction extends {
104
- handle(...args: unknown[]): unknown | Promise<unknown>;
105
- inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
106
- }, TResult = ReturnType<TAction["handle"]>>(this: new () => TAction, ...args: Parameters<TAction["handle"]>): TResult;
107
- withKernel<TAction extends {
108
- handle(...args: unknown[]): unknown | Promise<unknown>;
109
- inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- Singleton: (abstract new () => {
119
- inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- scope: Scope;
122
- readonly [BRAND]: ClassBrand<ModuleName, "other">;
123
- InvokeScope: Scope;
124
- TransientScope: Scope;
125
- SingletonScope: Scope;
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
- Class: (abstract new () => {
128
- inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
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
- scope: Scope;
131
- readonly [BRAND]: ClassBrand<ModuleName, "other">;
132
- InvokeScope: Scope;
133
- TransientScope: Scope;
134
- SingletonScope: Scope;
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, runWithKernel, setCurrentKernelFn, setGlobalKernel };
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/dependencies.ts
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 getInjectionPoint(v) {
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 = getInjectionPoint(value);
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.resolve(injectionPoint.token, injectionPoint.scope);
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 runWithKernel(scopedKernel, fn) {
163
+ function withKernel(scopedKernel, fn) {
89
164
  return als.run(scopedKernel, fn);
90
165
  }
91
- function getAlsKernel() {
166
+ function getAlsInvokeKernel() {
92
167
  return als.getStore() ?? null;
93
168
  }
94
169
 
95
170
  //#endregion
96
- //#region src/kernel-current.ts
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 setCurrentKernelFn(fnKernel) {
99
- currentKernelFn = fnKernel;
183
+ function setInvokeKernelProvider(fn) {
184
+ currentKernelFn = fn;
100
185
  }
101
- function getCurrentKernelFromFn() {
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-global.ts
107
- let globalKernel = null;
108
- function setGlobalKernel(kernel) {
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
- function getGlobalKernel() {
112
- return globalKernel;
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/kernel-invoke.ts
117
- function getInvokeKernel() {
118
- return getCurrentKernelFromFn() ?? getAlsKernel() ?? getGlobalKernel()?.scoped() ?? new Kernel();
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/modules.ts
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
- function inject(key, scope) {
178
- if (key[BRAND].moduleName !== moduleName && key[BRAND].layerName !== "facade") throw new Error(`Cross-module ${key[BRAND].layerName} dependency is not allowed. Use a Facade. (Tried to inject a ${key[BRAND].layerName} from ${key[BRAND].moduleName} into ${moduleName})`);
179
- return tokenizedDependency(key, scope ?? key.scope);
180
- }
181
- class BaseClass {
182
- static InvokeScope = "invoke";
183
- static TransientScope = "transient";
184
- static SingletonScope = "singleton";
185
- inject = inject.bind(this);
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 Singleton extends BaseClass {
213
- static [BRAND] = new ClassBrand(moduleName, "other");
214
- static scope = "singleton";
391
+ class Action extends Injectable(BaseAction) {
392
+ static __vla_module = moduleName;
215
393
  }
216
- class Class extends BaseClass {
217
- static [BRAND] = new ClassBrand(moduleName, "other");
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
- Singleton,
226
- Class
402
+ Resource,
403
+ Memoizable,
404
+ DevStable
227
405
  };
228
406
  }
229
407
 
230
408
  //#endregion
231
- export { Kernel, createModule, runWithKernel, setCurrentKernelFn, setGlobalKernel };
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.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/capsel#readme",
8
+ "homepage": "https://github.com/timomeh/vla#readme",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/timomeh/capsel.git"
11
+ "url": "git+https://github.com/timomeh/vla.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/timomeh/capsel/issues"
14
+ "url": "https://github.com/timomeh/vla/issues"
15
15
  },
16
16
  "exports": {
17
17
  ".": "./dist/index.mjs",