vla 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # capsel
2
+
3
+ A TypeScript data layer kernel for backend- and fullstack apps. Compatible with whatever framework or library you're using.
4
+
5
+ 🚧 WIP, under development. Exports and names will very likely change often.
6
+
7
+ - Dependency Injection without decorators and without reflection
8
+ - Works in any server-side framework
9
+ - Write code that's easy to test without module mocks all over the place
10
+ - Structures code into modules, layers and interfaces
11
+ - Familiar patterns: Actions, Services, Repos, Facades
12
+ - Ensures that Facades are used for cross-module dependencies to prevent messy code dependencies
13
+ - Tree shakeable
14
+ - Memoziation for Repos
15
+ - request-based context with AsyncLocalStorage
16
+ - 🏗️ first-class context injection
17
+ - ...
18
+
19
+ ## Why?
20
+
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.
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { createModule, Kernel } from "capsel"
27
+
28
+ // Users
29
+ const UserModule = createModule("User")
30
+
31
+ class ShowUserSettingsAction extends UserModule.Action {
32
+ users = this.inject(UserService)
33
+
34
+ async handle(userId: string) {
35
+ const settings = await this.users.getSettings(userId)
36
+ return {
37
+ timezone: settings.timezone,
38
+ hasSubscription: settings.hasSubscription
39
+ }
40
+ }
41
+ }
42
+
43
+ class UserService extends UserModule.Service {
44
+ repo = this.inject(UserRepo)
45
+ billing = this.inject(BillingFacade)
46
+ ctx = this.inject(Context) // WIP unimplemented
47
+
48
+ async getSettings(userId: string) {
49
+ await canViewProfile(userId)
50
+
51
+ const profile = await this.repo.findById(userId)
52
+ const hasSubscription = await this.billing.hasSubscription(userId)
53
+
54
+ return {
55
+ ...profile,
56
+ hasSubscription
57
+ }
58
+ }
59
+
60
+ private canViewProfile(userId: string) {
61
+ const isSameUser = this.ctx.currentUser.id !== userId
62
+ if (!isSameUser) throw new Forbidden()
63
+
64
+ // repo method calls are memoized
65
+ const profile = await this.repo.findById(userId)
66
+ const isTeamAdmin =
67
+ this.ctx.currentUser.role === "admin" &&
68
+ this.ctx.currentUser.teamId === profile.teamId
69
+
70
+ if (!isTeamAdmin) throw new Forbidden()
71
+ }
72
+ }
73
+
74
+ class UserRepo extends UserModule.Repo {
75
+ findById = this.memo((id: string) => {
76
+ // this method is memoized per request.
77
+ // memoized methods can be called like any normal method, but
78
+ // if it's called multiple times with the same args, it's only
79
+ // executed once and the result is cached
80
+ return db.users.find({ id })
81
+ })
82
+
83
+ async create(data: UserValues) {
84
+ const createdUser = await db.users.create({ data })
85
+
86
+ this.findById.prime(createUser.id).value(createdUser)
87
+ // memoized methods support multiple utilities:
88
+ // - .fresh(args) to skip memoized cache and execute the function again
89
+ // - .prime(args).value({ ... }) to set a cached value
90
+ // - .preload(args) to run the method in the background and preload the cache
91
+ // - .bust(args) to bust the cache for the provided args
92
+ // - .bustAll() to bust the cache for all args
93
+
94
+ return createdUser
95
+ }
96
+ }
97
+
98
+ // Billing
99
+ const BillingModule = createModule("Billing")
100
+
101
+ class BillingFacade extends Billing.Facade {
102
+ repo = this.inject(BillingRepo)
103
+
104
+ async hasSubscription(userId: string) {
105
+ const subscription = await this.repo.findSubscriptionByUser(userId)
106
+ return Boolean(subscription)
107
+ }
108
+ }
109
+
110
+ class BillingRepo extends BillingModule.Repo {
111
+ async findSubscriptionByUser(userId: string) {
112
+ return db.subscriptions.find({ userId })
113
+ }
114
+ }
115
+
116
+ const kernel = new Kernel()
117
+ kernel.setGlobal() // global instance
118
+
119
+ const settings = await ShowUserSettingsAction.invoke(userId)
120
+ // -> { timezone: 'GMT+1', hasSubscription: true }
121
+ ```
122
+
123
+ ### React usage
124
+
125
+ ```tsx
126
+ import { cache } from 'react'
127
+ import { setCurrentKernelFn } from 'capsel'
128
+ import { kernel } from '@/data/kernel'
129
+
130
+ const kernel = new Kernel()
131
+
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()))
135
+
136
+ async function Layout() {
137
+ const settings = await ShowUserSettingsAction.invoke(userId)
138
+
139
+ return <div><Page /></div>
140
+ }
141
+
142
+ async function Page() {
143
+ const settings = await ShowUserSettingsAction.invoke(userId)
144
+ // it will not query the db twice. it will use the memoized db query
145
+
146
+ return (
147
+ <ul>
148
+ <li>Timezone: {settings.timezone}</li>
149
+ <li>Subscriber: {settings.hasSubscription ? "yes" : "no"}</li>
150
+ </ul>
151
+ )
152
+ }
153
+ ```
154
+
155
+ ### In any middleware-based app
156
+
157
+ e.g. sveltekit
158
+
159
+ ```ts
160
+ import { runWithScope } from "capsel"
161
+ import type { Handle } from "@sveltejs/kit"
162
+ import { kernel } from '@/data/kernel'
163
+
164
+ export const handle: Handle = async ({ event, resolve }) => {
165
+ return runWithScope(kernel.scoped(), () => resolve(event))
166
+ }
167
+
168
+ import { kernel } from '@/data/kernel';
169
+ import type { PageServerLoad } from './$types';
170
+
171
+ export const load: PageServerLoad = async ({ params }) => {
172
+ return {
173
+ settings: await ShowUserSettingsAction.invoke(userId)
174
+ }
175
+ }
176
+ ```
177
+
178
+ e.g. express
179
+
180
+ ```ts
181
+ import { runWithScope } from "capsel"
182
+ import { kernel } from '@/data/kernel'
183
+
184
+ const app = express()
185
+
186
+ express.use((req, res, next) => runWithScope(kernel.scoped(), () => next()))
187
+
188
+ app.get("/users/:id", async (req, res) => {
189
+ const settings = await ShowUserSettingsAction.invoke(req.params.id)
190
+ res.json({ data: settings })
191
+ })
192
+ ```
193
+
194
+ ### Testing
195
+
196
+ ```ts
197
+ test("returns some user settings", async () => {
198
+ const kernel = new Kernel()
199
+
200
+ // mocks for database calls
201
+ kernel.bind(
202
+ UserRepo,
203
+ vi.fn(
204
+ class {
205
+ findById = vi.fn().mockResolvedValue({ timezone: "faked" })
206
+ },
207
+ ),
208
+ )
209
+
210
+ // mocks for cross-module dependencies
211
+ kernel.bind(
212
+ BillingFacade,
213
+ vi.fn(
214
+ class {
215
+ hasSubscription = vi.fn().mockResolvedValue(true)
216
+ },
217
+ ),
218
+ )
219
+
220
+ await expect(ShowUserSettingsAction.withKernel(kernel).invoke("1")).resolves.toEqual({
221
+ timezone: "faked",
222
+ hasSubscription: true
223
+ })
224
+ })
225
+ ```
226
+
227
+ ## Docs
228
+
229
+ There aren't any docs yet. This section is just jotting down some notes for myself.
230
+
231
+ ### When to use modules?
232
+
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.
234
+
235
+ A module can have multiple services, repositories and even multiple facades to separate resources from each other.
236
+
237
+ ### What's a facade, when to use it?
238
+
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.
240
+
241
+ ### How should I structure files and folders?
242
+
243
+ Roughly follow how capsel structures your code:
244
+
245
+ - Use separate folders for separate modules
246
+ - 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.
251
+
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.
253
+
254
+ ### Class scopes
255
+
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.
257
+
258
+ There are 3 different scopes:
259
+
260
+ - `transient` creates a new instance every time, it does not use a cached instance
261
+ - `invoke` creates a new instance per request handler and reuses this cached instance within the request.
262
+ - `singleton` creates a new instance only once and caches it forever
263
+
264
+ Example:
265
+
266
+ ```ts
267
+ class DatabasePool extends MyModule.Singleton {
268
+ // singletons are cached globally
269
+ private dbPool: DbPool | null = null
270
+
271
+ get dbPool() {
272
+ return this.dbPool || new DbPool()
273
+ }
274
+ }
275
+
276
+ class Repo extends MyModule.Repo {
277
+ // repos are cached per request (`invoke`)
278
+
279
+ // it will only create the db pool once and reuse it
280
+ // through the application's whole lifetime
281
+ dbPool = this.inject(DatabasePool)
282
+
283
+ private cache = Map<string, User>()
284
+
285
+ async findById(id: string) {
286
+ const cached = this.cache.get(id)
287
+ if (cached) return cached
288
+
289
+ const user = await dbPool.user.find({ id })
290
+ this.cache.set(id, user)
291
+ return user
292
+ }
293
+ }
294
+
295
+ class ServiceA extends MyModule.Service {
296
+ // it will use the same instance that ServiceB is also using
297
+ repo = this.inject(Repo)
298
+
299
+ async doSomething() {
300
+ await this.repo.findById("1")
301
+ }
302
+ }
303
+
304
+ class ServiceB extends MyModule.Service {
305
+ // it will use the same instance that ServiceA is also using
306
+ repo = this.inject(Repo)
307
+
308
+ async doOtherThing() {
309
+ await this.repo.findById("1")
310
+ }
311
+ }
312
+ ```
313
+
314
+ 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
318
+
319
+ ### Overriding class scopes
320
+
321
+ ```ts
322
+ class FooClass extends MyModule.Class {
323
+ static scope = FooClass.SingletonScope
324
+ }
325
+ ```
326
+
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
+ ### Defining scope when injecting a class
330
+
331
+ Example:
332
+
333
+ ```ts
334
+ class FooRepo extends MyModule.Repo {
335
+ async foo() {}
336
+ }
337
+
338
+ class FooService extends MyModule.Service {
339
+ repo = this.inject(FooRepo, "transient")
340
+ }
341
+ ```
342
+
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.
344
+
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.
346
+
347
+ ### Base classes
348
+
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.
350
+
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.
@@ -0,0 +1,138 @@
1
+ //#region src/types.d.ts
2
+ type Scope = "singleton" | "invoke" | "transient";
3
+ type InstantiableClass<T> = new () => T;
4
+ //#endregion
5
+ //#region src/kernel.d.ts
6
+ type Token<T = unknown> = InstantiableClass<T> & {
7
+ readonly scope?: Scope;
8
+ };
9
+ type Ctor<T = unknown> = InstantiableClass<T> & {
10
+ readonly scope?: Scope;
11
+ };
12
+ declare class Kernel {
13
+ private readonly singletons;
14
+ private readonly invokeCache;
15
+ private readonly bindings;
16
+ private readonly parent?;
17
+ private readonly root;
18
+ constructor(opts?: {
19
+ parent?: Kernel;
20
+ });
21
+ private fork;
22
+ private getBinding;
23
+ bind<T>(key: Token<T>, impl: Ctor<unknown>, scope?: Scope): void;
24
+ resolve<T>(key: Token<T>, scope?: Scope): T;
25
+ scoped(): Kernel;
26
+ create<T>(cls: Ctor<T>): T;
27
+ private instantiate;
28
+ private injectInto;
29
+ }
30
+ //#endregion
31
+ //#region src/kernel-als.d.ts
32
+ declare function runWithKernel<T>(scopedKernel: Kernel, fn: () => T): T;
33
+ //#endregion
34
+ //#region src/kernel-current.d.ts
35
+ type CurrentKernelFn = () => Kernel;
36
+ declare function setCurrentKernelFn(fnKernel: CurrentKernelFn): void;
37
+ //#endregion
38
+ //#region src/kernel-global.d.ts
39
+ declare function setGlobalKernel(kernel: Kernel): void;
40
+ //#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
+ };
53
+ //#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
+ }
72
+ declare function createModule<const ModuleName extends string>(moduleName: ModuleName): {
73
+ Facade: (abstract new () => {
74
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
75
+ }) & {
76
+ scope: Scope;
77
+ readonly [BRAND]: ClassBrand<ModuleName, "facade">;
78
+ InvokeScope: Scope;
79
+ TransientScope: Scope;
80
+ SingletonScope: Scope;
81
+ };
82
+ Service: (abstract new () => {
83
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
84
+ }) & {
85
+ scope: Scope;
86
+ readonly [BRAND]: ClassBrand<ModuleName, "service">;
87
+ InvokeScope: Scope;
88
+ TransientScope: Scope;
89
+ SingletonScope: Scope;
90
+ };
91
+ Repo: (abstract new () => {
92
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
93
+ memo<Args extends unknown[], R>(fn: (...args: Args) => R): Memoized<Args, R>;
94
+ }) & {
95
+ scope: Scope;
96
+ readonly [BRAND]: ClassBrand<ModuleName, "repo">;
97
+ };
98
+ Action: (abstract new () => {
99
+ handle(...args: unknown[]): unknown | Promise<unknown>;
100
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
101
+ }) & {
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;
112
+ };
113
+ readonly [BRAND]: ClassBrand<ModuleName, "action">;
114
+ InvokeScope: Scope;
115
+ TransientScope: Scope;
116
+ SingletonScope: Scope;
117
+ };
118
+ Singleton: (abstract new () => {
119
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
120
+ }) & {
121
+ scope: Scope;
122
+ readonly [BRAND]: ClassBrand<ModuleName, "other">;
123
+ InvokeScope: Scope;
124
+ TransientScope: Scope;
125
+ SingletonScope: Scope;
126
+ };
127
+ Class: (abstract new () => {
128
+ inject: <TKey extends ModuleClass<string>>(key: AllowedDependency<ModuleName, TKey>, scope?: Scope) => InstanceType<TKey>;
129
+ }) & {
130
+ scope: Scope;
131
+ readonly [BRAND]: ClassBrand<ModuleName, "other">;
132
+ InvokeScope: Scope;
133
+ TransientScope: Scope;
134
+ SingletonScope: Scope;
135
+ };
136
+ };
137
+ //#endregion
138
+ export { Kernel, createModule, runWithKernel, setCurrentKernelFn, setGlobalKernel };
package/dist/index.mjs ADDED
@@ -0,0 +1,231 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import objectHash from "object-hash";
3
+
4
+ //#region src/dependencies.ts
5
+ var UnresolvedDependency = class {
6
+ constructor(token, scope) {
7
+ this.token = token;
8
+ this.scope = scope;
9
+ }
10
+ };
11
+ function tokenizedDependency(defaultClass, scope) {
12
+ return new UnresolvedDependency(defaultClass, scope);
13
+ }
14
+ function getInjectionPoint(v) {
15
+ if (v instanceof UnresolvedDependency) return v;
16
+ return null;
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/kernel.ts
21
+ var Kernel = class Kernel {
22
+ singletons = /* @__PURE__ */ new Map();
23
+ invokeCache = /* @__PURE__ */ new Map();
24
+ bindings = /* @__PURE__ */ new Map();
25
+ parent;
26
+ root;
27
+ constructor(opts = {}) {
28
+ this.root = opts.parent?.root ?? this;
29
+ this.parent = opts.parent;
30
+ }
31
+ fork() {
32
+ return new Kernel({ parent: this });
33
+ }
34
+ getBinding(key) {
35
+ return this.bindings.get(key) ?? this.parent?.getBinding(key);
36
+ }
37
+ bind(key, impl, scope = "transient") {
38
+ this.singletons.delete(key);
39
+ this.bindings.set(key, {
40
+ impl,
41
+ scope
42
+ });
43
+ }
44
+ resolve(key, scope) {
45
+ const binding = this.getBinding(key);
46
+ const impl = binding?.impl ?? key;
47
+ const requestedScope = binding?.scope ?? scope ?? impl.scope;
48
+ if (requestedScope === "singleton") {
49
+ if (this.root.singletons.has(key)) return this.root.singletons.get(key);
50
+ const created = this.instantiate(impl);
51
+ this.root.singletons.set(key, created);
52
+ return created;
53
+ }
54
+ if (requestedScope === "invoke" && !!this.parent) {
55
+ if (this.invokeCache.has(key)) return this.invokeCache.get(key);
56
+ const created = this.instantiate(impl);
57
+ this.invokeCache.set(key, created);
58
+ return created;
59
+ }
60
+ return this.instantiate(impl);
61
+ }
62
+ scoped() {
63
+ return this.fork();
64
+ }
65
+ create(cls) {
66
+ return this.instantiate(cls);
67
+ }
68
+ instantiate(cls) {
69
+ const instance = new cls();
70
+ this.injectInto(instance);
71
+ return instance;
72
+ }
73
+ injectInto(instance) {
74
+ const obj = instance;
75
+ Object.entries(obj).map(([key, value]) => {
76
+ const injectionPoint = getInjectionPoint(value);
77
+ if (!injectionPoint) return null;
78
+ return [key, injectionPoint];
79
+ }).filter((entry) => entry !== null).forEach(([key, injectionPoint]) => {
80
+ obj[key] = this.resolve(injectionPoint.token, injectionPoint.scope);
81
+ });
82
+ }
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/kernel-als.ts
87
+ const als = new AsyncLocalStorage();
88
+ function runWithKernel(scopedKernel, fn) {
89
+ return als.run(scopedKernel, fn);
90
+ }
91
+ function getAlsKernel() {
92
+ return als.getStore() ?? null;
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/kernel-current.ts
97
+ let currentKernelFn = null;
98
+ function setCurrentKernelFn(fnKernel) {
99
+ currentKernelFn = fnKernel;
100
+ }
101
+ function getCurrentKernelFromFn() {
102
+ return currentKernelFn?.();
103
+ }
104
+
105
+ //#endregion
106
+ //#region src/kernel-global.ts
107
+ let globalKernel = null;
108
+ function setGlobalKernel(kernel) {
109
+ globalKernel = kernel;
110
+ }
111
+ function getGlobalKernel() {
112
+ return globalKernel;
113
+ }
114
+
115
+ //#endregion
116
+ //#region src/kernel-invoke.ts
117
+ function getInvokeKernel() {
118
+ return getCurrentKernelFromFn() ?? getAlsKernel() ?? getGlobalKernel()?.scoped() ?? new Kernel();
119
+ }
120
+
121
+ //#endregion
122
+ //#region src/memo.ts
123
+ function Memoizable(Base) {
124
+ class MemoizableBase extends Base {
125
+ memo(fn) {
126
+ const cache = /* @__PURE__ */ new Map();
127
+ const keyOf = (args) => objectHash(args);
128
+ const memoized = ((...args) => {
129
+ const key = keyOf(args);
130
+ const hit = cache.get(key);
131
+ if (hit !== void 0) return hit;
132
+ const value = fn.apply(this, args);
133
+ if (typeof value.then === "function") value.catch(() => {
134
+ cache.delete(key);
135
+ });
136
+ cache.set(key, value);
137
+ return value;
138
+ });
139
+ memoized.memoized = memoized;
140
+ memoized.fresh = ((...args) => {
141
+ return fn.apply(this, args);
142
+ });
143
+ memoized.preload = ((...args) => {
144
+ return memoized(...args);
145
+ });
146
+ memoized.prime = ((...args) => {
147
+ const key = keyOf(args);
148
+ return { value: (value) => {
149
+ if (typeof value.then === "function") value.catch(() => {
150
+ cache.delete(key);
151
+ });
152
+ cache.set(key, value);
153
+ } };
154
+ });
155
+ memoized.bust = (...args) => {
156
+ cache.delete(keyOf(args));
157
+ };
158
+ memoized.bustAll = () => {
159
+ cache.clear();
160
+ };
161
+ return memoized;
162
+ }
163
+ }
164
+ return MemoizableBase;
165
+ }
166
+
167
+ //#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
+ };
176
+ 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
+ }
211
+ }
212
+ class Singleton extends BaseClass {
213
+ static [BRAND] = new ClassBrand(moduleName, "other");
214
+ static scope = "singleton";
215
+ }
216
+ class Class extends BaseClass {
217
+ static [BRAND] = new ClassBrand(moduleName, "other");
218
+ static scope = "transient";
219
+ }
220
+ return {
221
+ Facade,
222
+ Service,
223
+ Repo,
224
+ Action,
225
+ Singleton,
226
+ Class
227
+ };
228
+ }
229
+
230
+ //#endregion
231
+ export { Kernel, createModule, runWithKernel, setCurrentKernelFn, setGlobalKernel };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "vla",
3
+ "type": "module",
4
+ "version": "0.1.1",
5
+ "description": "Data layer kernel for backend- and fullstack apps",
6
+ "author": "Timo Mämecke <hello@timo.wtf>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/timomeh/capsel#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/timomeh/capsel.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/timomeh/capsel/issues"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "main": "./dist/index.mjs",
21
+ "module": "./dist/index.mjs",
22
+ "types": "./dist/index.d.mts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "dev": "tsdown --watch",
29
+ "test": "vitest",
30
+ "typecheck": "tsc --noEmit",
31
+ "prepublishOnly": "pnpm run build"
32
+ },
33
+ "devDependencies": {
34
+ "@biomejs/biome": "2.3.10",
35
+ "@types/node": "^25.0.3",
36
+ "@types/object-hash": "^3.0.6",
37
+ "bumpp": "^10.3.2",
38
+ "tsdown": "^0.18.1",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "^4.0.16"
41
+ },
42
+ "dependencies": {
43
+ "object-hash": "^3.0.0"
44
+ }
45
+ }