@owlmeans/context 0.1.18-rc.15 → 0.1.18-rc.17

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,116 +1,300 @@
1
1
  # @owlmeans/context
2
2
 
3
- Lightweight dependency injection and service lifecycle management for fullstack OwlMeans applications.
4
-
5
- ## Overview
6
-
7
- - Context holds services, entrypoints, and resources in three flat registries keyed by alias; passed top-down through the app
8
- - Services are registered by alias and retrieved via `context.service(alias)`
9
- - One context is built per process by a single factory and extended with idempotent `append*` mixins
10
- - Route constants (`ROOT`, `HOME`, `GUEST`, `BASE`) are used by the entrypoint system across all packages
3
+ Lightweight dependency injection and service lifecycle for fullstack OwlMeans applications. An app
4
+ uses it to author its own services (`createService`, `createLazyService`), to type its context and
5
+ config, and to look up services, resources and entrypoints at runtime. An app almost never calls
6
+ `makeBasicContext` or `makeBasicConfig` itself — it builds its context with the layer factory
7
+ (`makeContext` from `@owlmeans/server-app` or `@owlmeans/web-panel`, `makeClientContext` from
8
+ `@owlmeans/client-context`) and its config with `config()` from `@owlmeans/config` or the layer
9
+ config package. Protocol declarations live in `@owlmeans/entrypoint`; data access contracts live in
10
+ `@owlmeans/resource`.
11
11
 
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/context@^0.1.18-rc.7
15
+ bun add @owlmeans/context@^0.1.18-rc.17
16
16
  ```
17
17
 
18
+ ## Concepts
19
+
20
+ - **Context** — the DI container: three flat registries (services, resources, entrypoints) keyed
21
+ by alias, plus a middleware table. One context is built per process by one factory; there is no
22
+ child context and nothing is stored for re-creation.
23
+ - **Contextual** — any object carrying `alias`, `registerContext` and `assertCtx`. It binds to
24
+ exactly one context, the first one that registers it.
25
+ - **Service** — a contextual with an initialization state. `createService` gets an eager `init()`
26
+ run during `context.init()`; `createLazyService` gets `lazyInit()` started on first lookup.
27
+ Both expose `ready()`.
28
+ - **Stage** — `ContextStage.Configuration` → `Loading` (after `configure()`) → `Ready` (after
29
+ `init()` resolves). `waitForConfigured()` / `waitForInitialized()` resolve at those points.
30
+ - **Middleware** — a hook keyed by `MiddlewareType` (`Config` / `Context`) and `MiddlewareStage`,
31
+ applied at fixed points of the lifecycle (see Usage 4).
32
+ - **`append*` mixin** — an idempotent function that registers a service, resource or middleware on
33
+ a context, optionally adds a typed accessor, and returns the same context. Layer factories are
34
+ compositions of these.
35
+
18
36
  ## Usage
19
37
 
20
- Creating and registering a service:
38
+ ### 1. Author a service
21
39
 
22
- ```typescript
40
+ `createService` wraps a partial implementation with `alias`, `initialized`, `init`, `ready` and the
41
+ contextual helpers. Inside methods, `service.assertCtx()` returns the context it was registered in.
42
+
43
+ ```ts
23
44
  import { createService } from '@owlmeans/context'
45
+ import type { InitializedService } from '@owlmeans/context'
46
+ import type { AppConfig, AppContext } from '../types.js'
47
+
48
+ export const INVOICE_SERVICE = 'invoice'
24
49
 
25
- export const makeIamService = (alias: string = IAM_SERVICE) => {
26
- const service = createService<IamService>(alias, {
27
- getEntityAdminConfig: async (entityId) => {
28
- const ctx = service.assertCtx<Config, Context>()
29
- return ctx.service<KeycloakService>(KC_SERVICE).getConfig(entityId)
30
- }
50
+ export interface InvoiceService extends InitializedService {
51
+ total: (projectId: string) => Promise<number>
52
+ }
53
+
54
+ export const makeInvoiceService = (alias: string = INVOICE_SERVICE): InvoiceService => {
55
+ const service: InvoiceService = createService<InvoiceService>(alias, {
56
+ total: async projectId => {
57
+ const ctx = service.assertCtx<AppConfig, AppContext>()
58
+ const { items } = await ctx.invoices().list({ projectId })
59
+
60
+ return items.reduce((sum, invoice) => sum + invoice.amount, 0)
61
+ },
31
62
  })
63
+
32
64
  return service
33
65
  }
34
66
  ```
35
67
 
36
- Registering and retrieving services from context:
68
+ A custom initializer is the third argument. It receives the service and returns the async function
69
+ `context.init()` runs — and it must set `initialized` itself:
70
+
71
+ ```ts
72
+ export const makeReminderService = (alias: string = 'reminder'): ReminderService =>
73
+ createService<ReminderService>(alias, { staleAfter: () => STALE_AFTER_MS }, service => async () => {
74
+ // open connections, start timers, read config — then mark the service usable
75
+ service.initialized = true
76
+ })
77
+ ```
78
+
79
+ ### 2. Register it through an `append*` mixin
80
+
81
+ The mixin registers the service, adds a typed accessor and returns the same context. The app's
82
+ factory calls the factory of the layer below and then its own mixins.
83
+
84
+ ```ts
85
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
86
+ import { makeContext as makeServerContext } from '@owlmeans/server-app'
87
+
88
+ export interface WithInvoiceService {
89
+ invoice: () => InvoiceService
90
+ }
91
+
92
+ export const appendInvoiceService = <C extends BasicConfig, T extends BasicContext<C>>(
93
+ ctx: T, alias: string = INVOICE_SERVICE
94
+ ): T & WithInvoiceService => {
95
+ const context = ctx as T & WithInvoiceService
96
+ if (!context.hasService(alias)) {
97
+ context.registerService(makeInvoiceService(alias))
98
+ }
99
+ context.invoice = () => context.service(alias)
100
+
101
+ return context
102
+ }
37
103
 
38
- ```typescript
39
- const context = makeBasicContext(config)
40
- context.registerService(makeIamService())
104
+ export const makeContext = <C extends AppConfig, T extends AppContext<C>>(cfg: C): T => {
105
+ const context = makeServerContext<C, T>(cfg)
106
+ appendInvoiceService<C, T>(context)
107
+
108
+ return context
109
+ }
41
110
 
111
+ const context = makeContext(config)
42
112
  await context.configure().init()
43
113
 
44
- const iamService = context.service<IamService>(IAM_SERVICE)
114
+ await context.invoice().total(projectId)
45
115
  ```
46
116
 
47
- ## API
117
+ ### 3. Look things up
48
118
 
49
- ### `createService<S>(alias, implementation): S`
119
+ `service`, `resource` and `entrypoint` throw a `SyntaxError` when the alias is not registered; use
120
+ the `has*` predicates for optional collaborators. Entrypoints are looked up by the shared protocol
121
+ declaration, which also supplies request and response types.
50
122
 
51
- Creates a service registered under `alias`. Inside method implementations, call `service.assertCtx()` to get the current context.
123
+ ```ts
124
+ import { projectProtocols } from 'my-app-common'
52
125
 
53
- ### `createLazyService<S>(alias, implementation): S`
126
+ const project = await context.entrypoint(projectProtocols.get).call({
127
+ params: { id },
128
+ })
54
129
 
55
- Like `createService` but initialized on first access via `lazyInit()`.
130
+ const policy = context.hasService(RELEASE_POLICY)
131
+ ? context.service<ReleasePolicy>(RELEASE_POLICY)
132
+ : null
56
133
 
57
- ### `appendContextual(alias, obj): Contextual`
134
+ const cache = context.resource<Resource<ProjectRecord>>(PROJECT_CACHE)
135
+ ```
58
136
 
59
- Wraps a plain object as a contextual entity (adds `registerContext`, `assertCtx`, etc.).
137
+ ### 4. Lazy services and lifecycle hooks
60
138
 
61
- ### `assertContext(ctx, location?): T`
139
+ A lazy service starts `lazyInit()` the first time `context.service(alias)` returns it and does not
140
+ wait for it, so its methods await `ready()` first:
62
141
 
63
- Asserts that a context is defined; throws if not.
142
+ ```ts
143
+ import { createLazyService } from '@owlmeans/context'
144
+ import type { GateService } from '@owlmeans/entrypoint'
64
145
 
65
- ### `makeBasicContext<C>(cfg): BasicContext<C>`
146
+ export const makeProjectGate = (alias: string = PROJECT_GATE): GateService => {
147
+ const service: GateService = createLazyService<GateService>(alias, {
148
+ assert: async (req, _res, params) => {
149
+ await service.ready()
150
+ const ctx = service.assertCtx<AppConfig, AppContext>()
151
+ // ... check req.auth against params using ctx resources
152
+ },
153
+ })
66
154
 
67
- Creates a context from a config object. Rarely used directly — prefer the higher-level `makeContext` from `@owlmeans/server-app` or `@owlmeans/client-context`.
155
+ return service
156
+ }
157
+ ```
68
158
 
69
- A factory calls the factory of the layer below it, applies its own idempotent `append*` mixins, and returns that same context:
159
+ Middlewares run at fixed points:
160
+
161
+ | When | Type / stage |
162
+ |------|--------------|
163
+ | `configure()` | `Config` / `Configuration` |
164
+ | `init()`, before services | `Context` / `Configuration` |
165
+ | after every eager `service.init()` | `Config` / `Loading` |
166
+ | after every `resource.init()` | `Context` / `Loading` |
167
+ | after the stage becomes `Ready` (not awaited) | `Context` / `Ready` |
168
+
169
+ `Context` / `Loading` is the moment services are initialized but nothing has started listening yet —
170
+ the place to mount a raw server route:
171
+
172
+ ```ts
173
+ import { MiddlewareStage, MiddlewareType } from '@owlmeans/context'
174
+
175
+ export const appendHealthEndpoint = <C extends AppConfig, T extends AppContext<C>>(context: T): T => {
176
+ context.registerMiddleware({
177
+ type: MiddlewareType.Context,
178
+ stage: MiddlewareStage.Loading,
179
+ apply: async ctx => {
180
+ const server = (ctx as unknown as T).getApiServer().server
181
+ server.route({ method: 'GET', url: '/healthz', handler: async () => ({ ok: true }) })
182
+ },
183
+ })
70
184
 
71
- ```typescript
72
- export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
73
- const context = makeServerContext<C, T>(cfg)
74
- appendMyService<C, T>(context)
75
185
  return context
76
186
  }
77
187
  ```
78
188
 
79
- Nothing is stored for re-creation — a service, a resource and an entrypoint each bind to exactly one context.
80
-
81
- ### Registries
189
+ Work that needs the whole context ready without blocking the factory hangs off
190
+ `waitForInitialized()`:
82
191
 
83
- Three flat maps keyed by alias. Registering the same alias twice replaces the earlier entry.
84
-
85
- ```typescript
86
- context.registerService(service) // context.service(alias), context.hasService(alias)
87
- context.registerResource(resource) // context.resource(alias), context.hasResource(alias)
88
- context.registerEntrypoints(entrypoints) // context.entrypoint(protocol), context.entrypoints(), context.hasEntrypoint(alias)
192
+ ```ts
193
+ context.waitForInitialized().then(async () => {
194
+ context.service<DevService>(DEV_SERVICE).setMetadataFactory(metadataFactory)
195
+ })
89
196
  ```
90
197
 
91
- ### Enums
198
+ ### 5. A plain contextual object
92
199
 
93
- ```typescript
94
- enum AppType { Backend, Frontend }
95
- enum ContextStage { Configuration, Loading, Ready }
96
- ```
200
+ `appendContextual` turns any object into something a context can register — the basis of custom
201
+ resources and entrypoints:
97
202
 
98
- ### Route Constants
203
+ ```ts
204
+ import { appendContextual } from '@owlmeans/context'
205
+ import type { BasicResource } from '@owlmeans/context'
99
206
 
100
- ```typescript
101
- ROOT // intermediate base route
102
- HOME // default/home route
103
- GUEST // unauthenticated area
104
- BASE // authenticated area
105
- CRASH // error fallback screen
207
+ const resource = appendContextual<BasicResource>('project-cache', {
208
+ init: async () => { /* warm up */ },
209
+ })
210
+
211
+ context.registerResource(resource)
106
212
  ```
107
213
 
108
- ## Related Packages
214
+ ## API
109
215
 
110
- - [`@owlmeans/config`](../config) — configuration builder used with context
111
- - [`@owlmeans/server-context`](../server-context) — server-side context extension
112
- - [`@owlmeans/client-context`](../client-context) — client-side context extension
216
+ ### Functions
217
+
218
+ | Symbol | Kind | Purpose |
219
+ |--------|------|---------|
220
+ | `makeBasicContext<C>(cfg)` | function | Build the bare container; wrapped by every layer factory |
221
+ | `makeBasicConfig<C>(type, service, cfg?)` | function | Build a `BasicConfig` with `ready: false`, empty `services`, `records`, `debug` |
222
+ | `createService<S>(alias, service, init?)` | function | Service initialized eagerly by `context.init()` |
223
+ | `createLazyService<S>(alias, service, init?)` | function | Service initialized on first `context.service(alias)` |
224
+ | `appendContextual<T>(alias, obj)` | function | Add `alias`, `registerContext`, `assertCtx` to an object |
225
+ | `assertContext(ctx, location?)` | function | Return `ctx` or throw `SyntaxError` naming `location` |
226
+
227
+ ### `BasicContext<C>` members
228
+
229
+ | Member | Purpose |
230
+ |--------|---------|
231
+ | `cfg`, `config` | Config object; `config` is a promise resolving after configuration |
232
+ | `stage` | Current `ContextStage` |
233
+ | `configure()`, `init()` | Run the lifecycle; `init()` resolves once the context is `Ready` |
234
+ | `waitForConfigured()`, `waitForInitialized()` | Promises for those two points |
235
+ | `registerService`, `registerResource`, `registerEntrypoint`, `registerEntrypoints`, `registerMiddleware` | Registration; a repeated alias replaces the earlier entry |
236
+ | `service(alias)`, `resource(alias)`, `entrypoint(protocol \| alias)` | Lookup; throws `SyntaxError` when missing or (eager service) not initialized |
237
+ | `hasService`, `hasResource`, `hasEntrypoint` | Presence checks by alias |
238
+ | `entrypoints()` | Every registered entrypoint |
239
+
240
+ ### Constants and enums
241
+
242
+ | Symbol | Kind | Purpose |
243
+ |--------|------|---------|
244
+ | `AppType` | enum | `Backend`, `Frontend` |
245
+ | `ContextStage` | enum | `Configuration`, `Loading`, `Ready` |
246
+ | `MiddlewareType` | enum | `Config`, `Context` |
247
+ | `MiddlewareStage` | enum | `Configuration`, `Loading`, `Ready` |
248
+ | `CONFIG_RECORD` | const | Config key holding `ConfigRecord[]` (`'records'`) |
249
+ | `EMPTY_ENTITY`, `EMPTY_PROFILE` | const | Placeholders for an unset organization entity or profile |
250
+ | `ROOT` | const | Intermediate base route alias |
251
+ | `HOME` | const | Default final route alias |
252
+ | `GUEST` | const | Intermediate area without authentication |
253
+ | `BASE` | const | Intermediate area requiring authentication |
254
+ | `CRASH` | const | Fallback error screen alias |
255
+
256
+ ### Types
257
+
258
+ | Symbol | Kind | Purpose |
259
+ |--------|------|---------|
260
+ | `BasicConfig` | interface | `ready`, `service`, `alias?`, `type`, `services?`, `records?`, `debug?` |
261
+ | `ConfigRecord` | interface | Free-form config record with `id` and optional `recordType` |
262
+ | `BasicContext<C>` | interface | The container (members above) |
263
+ | `Contextual` | interface | `ctx?`, `alias`, `registerContext`, `assertCtx` |
264
+ | `Service` | interface | Contextual with `initialized`, `init?`, `lazyInit?`, `ready?` |
265
+ | `InitializedService` | interface | Service with required `init` and `ready` |
266
+ | `LazyService` | interface | Service with required `lazyInit` and `ready` |
267
+ | `InitMethod<S>` | interface | `(service) => () => Promise<void>` custom initializer |
268
+ | `BasicResource` | interface | Contextual with optional `init` |
269
+ | `BasicEntrypoint` | interface | Contextual marked `_entrypoint: true` |
270
+ | `EntrypointReference<T>` | interface | `{ alias, entrypointType? }` — what a protocol declaration satisfies for typed lookup |
271
+ | `Middleware` | interface | `type`, `stage`, `apply(context, args?)` |
272
+
273
+ ## Common pitfalls
274
+
275
+ - Pass protocol declarations to `context.entrypoint(...)`, never alias strings — the declaration
276
+ carries the request and response types.
277
+ - Build one context per process with one factory; do not create child contexts or reuse a service
278
+ object in a second context — it stays bound to the first one.
279
+ - A custom `init` passed to `createService` must set `service.initialized = true`, or
280
+ `context.service(alias)` throws "is not initialized".
281
+ - `context.service(alias)` on a lazy service does not wait for `lazyInit()`; methods await
282
+ `service.ready()` before touching state.
283
+ - Registering an alias twice silently replaces the earlier entry — guard `append*` mixins with
284
+ `has*` so they stay idempotent.
285
+ - `Context` / `Ready` middlewares are not awaited by `init()`; do not rely on them having finished.
286
+ - Keep the organization `entitySlug` on the wire; resolve the stable `entityId` inside the context
287
+ before persisting relations or calling third parties.
288
+
289
+ ## Related packages
290
+
291
+ - [`@owlmeans/config`](../config) — the application config built on `BasicConfig`
292
+ - [`@owlmeans/entrypoint`](../entrypoint) — protocol declarations looked up through `context.entrypoint`
293
+ - [`@owlmeans/resource`](../resource) — the resource contract registered through `registerResource`
294
+ - [`@owlmeans/server-context`](../server-context) — server-side context layer
295
+ - [`@owlmeans/client-context`](../client-context) — client-side context layer
113
296
  - [`@owlmeans/server-app`](../server-app) — top-level server context factory
297
+ - [`@owlmeans/web-panel`](../web-panel) — top-level browser context factory
114
298
 
115
299
  <!-- owlmeans:agent-guidance:start -->
116
300
  ## Agent guidance
@@ -120,7 +304,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
120
304
  your project's skill store (`.agents/skills/`):
121
305
 
122
306
  ```sh
123
- npx @owlmeans/agent-skills@^0.1.18-rc.19
307
+ npx @owlmeans/agent-skills@^0.1.18-rc.21
124
308
  ```
125
309
 
126
310
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/context",
4
- "version": "0.1.18-rc.15",
5
- "generatedAt": "2026-09-12T13:51:53.542Z",
4
+ "version": "0.1.18-rc.17",
5
+ "generatedAt": "2026-09-15T12:21:31.949Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -6,13 +6,13 @@ description: Use OwlMeans context services, resources and typed entrypoint proto
6
6
 
7
7
  # Context lookups
8
8
 
9
- **Install:** `bun add @owlmeans/context@^0.1.18-rc.14`
9
+ **Install:** `bun add @owlmeans/context@^0.1.18-rc.16`
10
10
 
11
11
  Use a protocol declaration directly when obtaining an entrypoint. The declaration supplies the
12
12
  registered entrypoint’s request and response types.
13
13
 
14
14
  ```ts
15
- const project = await context.entrypoint(projectEntrypoints.get).call({
15
+ const project = await context.entrypoint(projectProtocols.get).call({
16
16
  params: { id },
17
17
  })
18
18
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/context",
3
- "version": "0.1.18-rc.15",
3
+ "version": "0.1.18-rc.17",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {