@smthrs/plugin 0.0.0-stage → 1.0.0-rc.4
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/CHANGELOG.md +79 -0
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/dist/cjs/Config.d.ts +129 -0
- package/dist/cjs/Config.d.ts.map +1 -0
- package/dist/cjs/Config.js +115 -0
- package/dist/cjs/Config.js.map +7 -0
- package/dist/cjs/Hooks.d.ts +176 -0
- package/dist/cjs/Hooks.d.ts.map +1 -0
- package/dist/cjs/Hooks.js +34 -0
- package/dist/cjs/Hooks.js.map +7 -0
- package/dist/cjs/Kernel.d.ts +81 -0
- package/dist/cjs/Kernel.d.ts.map +1 -0
- package/dist/cjs/Kernel.js +57 -0
- package/dist/cjs/Kernel.js.map +7 -0
- package/dist/cjs/Plugin.d.ts +71 -0
- package/dist/cjs/Plugin.d.ts.map +1 -0
- package/dist/cjs/Plugin.js +25 -0
- package/dist/cjs/Plugin.js.map +7 -0
- package/dist/cjs/PluginError.d.ts +50 -0
- package/dist/cjs/PluginError.d.ts.map +1 -0
- package/dist/cjs/PluginError.js +58 -0
- package/dist/cjs/PluginError.js.map +7 -0
- package/dist/cjs/Plugins.d.ts +97 -0
- package/dist/cjs/Plugins.d.ts.map +1 -0
- package/dist/cjs/Plugins.js +160 -0
- package/dist/cjs/Plugins.js.map +7 -0
- package/dist/cjs/Resolve.d.ts +128 -0
- package/dist/cjs/Resolve.d.ts.map +1 -0
- package/dist/cjs/Resolve.js +343 -0
- package/dist/cjs/Resolve.js.map +7 -0
- package/dist/cjs/index.d.ts +83 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +45 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Boundary.d.ts +96 -0
- package/dist/cjs/internal/Boundary.d.ts.map +1 -0
- package/dist/cjs/internal/Boundary.js +129 -0
- package/dist/cjs/internal/Boundary.js.map +7 -0
- package/dist/cjs/internal/mergePluginLayers.d.ts +21 -0
- package/dist/cjs/internal/mergePluginLayers.d.ts.map +1 -0
- package/dist/cjs/internal/mergePluginLayers.js +57 -0
- package/dist/cjs/internal/mergePluginLayers.js.map +7 -0
- package/dist/cjs/internal/snapshotCacheEnvironment.d.ts +19 -0
- package/dist/cjs/internal/snapshotCacheEnvironment.d.ts.map +1 -0
- package/dist/cjs/internal/snapshotCacheEnvironment.js +92 -0
- package/dist/cjs/internal/snapshotCacheEnvironment.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Config.d.ts +129 -0
- package/dist/esm/Config.d.ts.map +1 -0
- package/dist/esm/Config.js +160 -0
- package/dist/esm/Config.js.map +1 -0
- package/dist/esm/Hooks.d.ts +176 -0
- package/dist/esm/Hooks.d.ts.map +1 -0
- package/dist/esm/Hooks.js +56 -0
- package/dist/esm/Hooks.js.map +1 -0
- package/dist/esm/Kernel.d.ts +81 -0
- package/dist/esm/Kernel.d.ts.map +1 -0
- package/dist/esm/Kernel.js +58 -0
- package/dist/esm/Kernel.js.map +1 -0
- package/dist/esm/Plugin.d.ts +71 -0
- package/dist/esm/Plugin.d.ts.map +1 -0
- package/dist/esm/Plugin.js +20 -0
- package/dist/esm/Plugin.js.map +1 -0
- package/dist/esm/PluginError.d.ts +50 -0
- package/dist/esm/PluginError.d.ts.map +1 -0
- package/dist/esm/PluginError.js +53 -0
- package/dist/esm/PluginError.js.map +1 -0
- package/dist/esm/Plugins.d.ts +97 -0
- package/dist/esm/Plugins.d.ts.map +1 -0
- package/dist/esm/Plugins.js +178 -0
- package/dist/esm/Plugins.js.map +1 -0
- package/dist/esm/Resolve.d.ts +128 -0
- package/dist/esm/Resolve.d.ts.map +1 -0
- package/dist/esm/Resolve.js +367 -0
- package/dist/esm/Resolve.js.map +1 -0
- package/dist/esm/index.d.ts +83 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Boundary.d.ts +96 -0
- package/dist/esm/internal/Boundary.d.ts.map +1 -0
- package/dist/esm/internal/Boundary.js +162 -0
- package/dist/esm/internal/Boundary.js.map +1 -0
- package/dist/esm/internal/mergePluginLayers.d.ts +21 -0
- package/dist/esm/internal/mergePluginLayers.d.ts.map +1 -0
- package/dist/esm/internal/mergePluginLayers.js +41 -0
- package/dist/esm/internal/mergePluginLayers.js.map +1 -0
- package/dist/esm/internal/snapshotCacheEnvironment.d.ts +19 -0
- package/dist/esm/internal/snapshotCacheEnvironment.d.ts.map +1 -0
- package/dist/esm/internal/snapshotCacheEnvironment.js +63 -0
- package/dist/esm/internal/snapshotCacheEnvironment.js.map +1 -0
- package/docs/README.md +131 -0
- package/docs/api.md +522 -0
- package/docs/concepts/configuration.md +111 -0
- package/docs/concepts/hook-kinds.md +107 -0
- package/docs/concepts/resolution.md +132 -0
- package/docs/guides/cache-identity.md +106 -0
- package/docs/guides/contribute-services.md +84 -0
- package/docs/guides/host-your-own-hooks.md +129 -0
- package/docs/guides/order-handlers.md +76 -0
- package/docs/guides/select-plugins.md +72 -0
- package/docs/guides/testing.md +149 -0
- package/docs/installation.md +71 -0
- package/docs/quickstart.md +151 -0
- package/docs/troubleshooting.md +179 -0
- package/package.json +179 -3
- package/src/Config.ts +207 -0
- package/src/Hooks.ts +212 -0
- package/src/Kernel.ts +119 -0
- package/src/Plugin.ts +80 -0
- package/src/PluginError.ts +62 -0
- package/src/Plugins.ts +254 -0
- package/src/Resolve.ts +535 -0
- package/src/index.ts +91 -0
- package/src/internal/Boundary.ts +252 -0
- package/src/internal/mergePluginLayers.ts +57 -0
- package/src/internal/snapshotCacheEnvironment.ts +77 -0
package/docs/api.md
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "API reference"
|
|
3
|
+
description: "Every export of @smthrs/plugin: the plugin and hook declarations, the Kernel, Plugins, Resolve, and Config modules, the PluginError codes, and the published limits."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Import the declarations from the root and the four stateful modules either as
|
|
7
|
+
namespaces from the root or from their own subpaths:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { engineHooks, type FlowsPlugin, make, PluginError } from "@smthrs/plugin"
|
|
11
|
+
import * as Config from "@smthrs/plugin/Config"
|
|
12
|
+
import * as Kernel from "@smthrs/plugin/Kernel"
|
|
13
|
+
import * as Plugins from "@smthrs/plugin/Plugins"
|
|
14
|
+
import * as Resolve from "@smthrs/plugin/Resolve"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Every function that can refuse returns `Effect<A, PluginError>`, except
|
|
18
|
+
`Config.merge` and `Config.deepFreeze`, which are synchronous and throw a
|
|
19
|
+
`PluginError`.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Kernel, make } from "@smthrs/plugin"
|
|
25
|
+
|
|
26
|
+
const editor = make({
|
|
27
|
+
name: "flows-plugin-editor",
|
|
28
|
+
hooks: { config: () => Effect.succeed({ editor: { readOnly: false } }) }
|
|
29
|
+
})
|
|
30
|
+
const kernel = await Effect.runPromise(Kernel.make([editor], {}, { target: "harness" }))
|
|
31
|
+
console.log(kernel.config) // { editor: { readOnly: false } }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Root exports
|
|
35
|
+
|
|
36
|
+
| Export | Kind | Summary |
|
|
37
|
+
| ------------------------------------------------------------------------ | ------------------ | --------------------------------------------------------------------- |
|
|
38
|
+
| `FlowsHooks` | interface | The augmentable hook catalog. Declares `config` and `configResolved`. |
|
|
39
|
+
| `Apply`, `FlowsPlugin`, `PluginInput`, `make` | from `Plugin` | The plugin record and its constructor. |
|
|
40
|
+
| Hook kinds and type-level helpers, `engineHooks`, `handlerOf`, `orderOf` | from `Hooks` | The hook surface. |
|
|
41
|
+
| `PluginErrorCode`, `PluginError` | from `PluginError` | The single typed failure. |
|
|
42
|
+
| `Config`, `Kernel`, `Plugins`, `Resolve` | namespaces | The four modules. |
|
|
43
|
+
|
|
44
|
+
### FlowsHooks
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface FlowsHooks {
|
|
48
|
+
readonly config: WaterfallHook<(config: FlowsConfig) => Effect.Effect<Partial<FlowsConfig> | void, any, never>>
|
|
49
|
+
readonly configResolved: ParallelHook<(config: ResolvedConfig) => Effect.Effect<void, any, never>>
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Declared in the entry point so that `declare module "@smthrs/plugin"` can
|
|
54
|
+
augment it. Open for augmentation, closed for dispatch: the kernel dispatches
|
|
55
|
+
only the config lifecycle, and a host supplies and dispatches its own catalog
|
|
56
|
+
over the same augmented interface.
|
|
57
|
+
|
|
58
|
+
Both startup hooks are context-free. `Kernel.make` runs them before any plugin
|
|
59
|
+
layer is built and supplies no services of its own, so no externally supplied
|
|
60
|
+
service is available during startup. A handler that needs one provides it
|
|
61
|
+
inside the hook with `Effect.provide`. A plugin literal whose `config` hook
|
|
62
|
+
requires a service fails to compile against `FlowsHooks`; a host whose startup
|
|
63
|
+
hooks do require services declares a separate hook interface, and
|
|
64
|
+
`Kernel.make<H>` then carries the requirement as `Kernel.StartupContext<H>`.
|
|
65
|
+
|
|
66
|
+
## Plugin
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
type Apply = "engine" | "harness" | ((config: FlowsConfig) => boolean)
|
|
70
|
+
|
|
71
|
+
interface FlowsPlugin<H = FlowsHooks> {
|
|
72
|
+
readonly name: string
|
|
73
|
+
readonly version?: string | undefined
|
|
74
|
+
readonly enforce?: "pre" | "post" | undefined
|
|
75
|
+
readonly apply?: Apply | undefined
|
|
76
|
+
readonly layer?: Layer.Layer<never, any, any> | undefined
|
|
77
|
+
readonly hooks?: Partial<H> | undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type PluginInput<H = FlowsHooks> =
|
|
81
|
+
| FlowsPlugin<H>
|
|
82
|
+
| false
|
|
83
|
+
| null
|
|
84
|
+
| undefined
|
|
85
|
+
| ReadonlyArray<PluginInput<H>>
|
|
86
|
+
|
|
87
|
+
const make: <H = FlowsHooks>(plugin: FlowsPlugin<H>) => FlowsPlugin<H>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Field | Meaning |
|
|
91
|
+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `name` | Required and unique among selected plugins. Compared as an exact Unicode string, at most 256 UTF-16 code units, never empty or whitespace only. Convention: `flows-plugin-<thing>`. |
|
|
93
|
+
| `version` | Semantic identity folded into sealed cache keys. Required when the host declares a `cacheEnvironment`; refused as empty or malformed text. |
|
|
94
|
+
| `enforce` | Ordering group. Omitted means normal. |
|
|
95
|
+
| `apply` | Conditional inclusion. A literal names the host target; a predicate receives the pre-resolution configuration and returns a boolean. |
|
|
96
|
+
| `layer` | Services this plugin contributes to the composition. Must be an Effect `Layer`. |
|
|
97
|
+
| `hooks` | Typed hook entries. Only keys declared in `H` compile, and only names in the host's runtime catalog resolve. |
|
|
98
|
+
|
|
99
|
+
`make` is an identity function that pins a plugin literal to `FlowsPlugin<H>`,
|
|
100
|
+
so an excess or misspelled hook key fails at the definition site.
|
|
101
|
+
`PluginInput` accepts falsy entries and arrays nested up to
|
|
102
|
+
`maximumPluginDepth`, which is what makes a preset an ordinary function that
|
|
103
|
+
returns plugins.
|
|
104
|
+
|
|
105
|
+
## Hooks
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type HookKind = "sequential" | "parallel" | "first" | "waterfall"
|
|
109
|
+
|
|
110
|
+
interface HookObject<F> {
|
|
111
|
+
readonly order?: "pre" | "post" | undefined
|
|
112
|
+
readonly handler: F
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type HookEntry<K extends HookKind, F> = (F | HookObject<F>) & HookMeta<K, F>
|
|
116
|
+
|
|
117
|
+
type SequentialHook<F> = HookEntry<"sequential", F>
|
|
118
|
+
type ParallelHook<F> = HookEntry<"parallel", F>
|
|
119
|
+
type FirstHook<F> = HookEntry<"first", F>
|
|
120
|
+
type WaterfallHook<F> = HookEntry<"waterfall", F>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`HookMeta<K, F>` is a phantom carrier: it records the kind and the handler type
|
|
124
|
+
in the type system and adds nothing at runtime. A hook entry is either the bare
|
|
125
|
+
handler or the `{ order, handler }` object; `undefined` and `null` are refused.
|
|
126
|
+
|
|
127
|
+
Type-level helpers, used by the dispatcher's signatures and available to hosts:
|
|
128
|
+
|
|
129
|
+
| Helper | Extracts |
|
|
130
|
+
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `KindOf<T>` | The declared kind of a hook entry type. |
|
|
132
|
+
| `HandlerOf<T>` | The handler function type. |
|
|
133
|
+
| `KeysOfKind<H, K>` | The hook names of kind `K` in interface `H`. |
|
|
134
|
+
| `ArgsOf<T>` | The positional argument tuple the handler accepts. |
|
|
135
|
+
| `ReturnOf<T>` | The Effect the handler returns. |
|
|
136
|
+
| `SuccessOf<T>` | That Effect's success value. |
|
|
137
|
+
| `ContextOf<T>` | That Effect's required context. |
|
|
138
|
+
| `HookCatalog<H>` | The runtime catalog for `H`: a name declared in `H` must carry the kind its type declares; other names admit any kind. |
|
|
139
|
+
|
|
140
|
+
Values:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const engineHooks: Readonly<{ readonly config: "waterfall"; readonly configResolved: "parallel" }>
|
|
144
|
+
const handlerOf: (entry: unknown) => (...args: Array<any>) => unknown
|
|
145
|
+
const orderOf: (entry: unknown) => "pre" | "post" | undefined
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`engineHooks` is the frozen runtime catalog for the shared configuration hooks,
|
|
149
|
+
and the catalog `Resolve.resolve` checks against when a host supplies none.
|
|
150
|
+
Spread it into your own catalog to keep the config lifecycle available.
|
|
151
|
+
`handlerOf` and `orderOf` normalize either hook entry form.
|
|
152
|
+
|
|
153
|
+
## Kernel
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
interface Kernel<H = FlowsHooks> {
|
|
157
|
+
readonly plugins: Plugins.Service<H>
|
|
158
|
+
readonly config: ResolvedConfig
|
|
159
|
+
readonly layer: Layer.Layer<any, PluginError, any>
|
|
160
|
+
readonly observerErrors: ReadonlyArray<PluginError>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type ConfigContext<H> = ContextOf<H["config"]>
|
|
164
|
+
type StartupContext<H> = ConfigContext<H> | ContextOf<H["configResolved"]>
|
|
165
|
+
|
|
166
|
+
const make: <H = FlowsHooks>(
|
|
167
|
+
input: PluginInput<NoInfer<H>>,
|
|
168
|
+
config?: FlowsConfig,
|
|
169
|
+
options?: Omit<Resolve.Options<NoInfer<H>>, "config">
|
|
170
|
+
) => Effect.Effect<Kernel<H>, PluginError, StartupContext<H>>
|
|
171
|
+
|
|
172
|
+
const runConfig: <H = FlowsHooks>(
|
|
173
|
+
plugins: Plugins.Service<H>,
|
|
174
|
+
config: FlowsConfig
|
|
175
|
+
) => Effect.Effect<ResolvedConfig, PluginError, ConfigContext<H>>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`StartupContext<H>` is `never` for `FlowsHooks`, whose startup hooks are
|
|
179
|
+
context-free, so `Kernel.make([...])` runs with `Effect.runPromise` as is. For a
|
|
180
|
+
separate hook interface whose `config` or `configResolved` handlers require
|
|
181
|
+
services, the requirement is the startup Effect's context and the caller
|
|
182
|
+
provides it before running; the kernel never supplies startup services itself.
|
|
183
|
+
|
|
184
|
+
`make` performs startup in this order:
|
|
185
|
+
|
|
186
|
+
1. Snapshot the positional `config`.
|
|
187
|
+
2. Resolve the plugin list against `options`.
|
|
188
|
+
3. Run the `config` waterfall, deep-merging each returned patch.
|
|
189
|
+
4. Decode and freeze the result into a `ResolvedConfig`.
|
|
190
|
+
5. Run `configResolved` observers in parallel, collecting their failures into
|
|
191
|
+
`observerErrors` rather than failing.
|
|
192
|
+
6. Merge the selected plugins' layers, left to right in resolved order.
|
|
193
|
+
|
|
194
|
+
The positional `config` is the kernel's only pre-resolution configuration
|
|
195
|
+
source. `Options.config` is excluded from the options type so a caller cannot
|
|
196
|
+
declare a second one, and a `config` key smuggled past the type fails at
|
|
197
|
+
runtime with `invalid_plugin` at `$options.config`, the same refusal an
|
|
198
|
+
unknown option key gets. `runConfig` exposes step 3 and step 4 on their own, for a
|
|
199
|
+
host that resolved its plugin list separately. Before dispatching the first
|
|
200
|
+
hook, it validates and copies the supplied config into a detached, recursively
|
|
201
|
+
frozen snapshot. Invalid or accessor-bearing input fails with `config_invalid`
|
|
202
|
+
before any hook runs, without executing accessors. The final waterfall result
|
|
203
|
+
is admitted again through `Config.resolve`.
|
|
204
|
+
|
|
205
|
+
`observerErrors` is a return value, not a log: a `configResolved` failure never
|
|
206
|
+
fails startup, so a host that ignores the array cannot tell a working observer
|
|
207
|
+
from a broken one.
|
|
208
|
+
|
|
209
|
+
## Plugins
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
interface Service<H = FlowsHooks> {
|
|
213
|
+
readonly resolved: Resolved<H>
|
|
214
|
+
readonly handlers: (hook: string) => ReadonlyArray<HandlerRecord>
|
|
215
|
+
|
|
216
|
+
readonly sequential: <K extends KeysOfKind<H, "sequential">>(
|
|
217
|
+
hook: K,
|
|
218
|
+
...args: ArgsOf<H[K]>
|
|
219
|
+
) => Effect.Effect<ReadonlyArray<SuccessOf<H[K]>>, PluginError, ContextOf<H[K]>>
|
|
220
|
+
|
|
221
|
+
readonly parallel: <K extends KeysOfKind<H, "parallel">>(
|
|
222
|
+
hook: K,
|
|
223
|
+
...args: ArgsOf<H[K]>
|
|
224
|
+
) => Effect.Effect<ReadonlyArray<PluginError>, never, ContextOf<H[K]>>
|
|
225
|
+
|
|
226
|
+
readonly first: <K extends KeysOfKind<H, "first">>(
|
|
227
|
+
hook: K,
|
|
228
|
+
...args: ArgsOf<H[K]>
|
|
229
|
+
) => Effect.Effect<SuccessOf<H[K]>, PluginError, ContextOf<H[K]>>
|
|
230
|
+
|
|
231
|
+
readonly waterfall: <K extends KeysOfKind<H, "waterfall">>(
|
|
232
|
+
hook: K,
|
|
233
|
+
initial: ArgsOf<H[K]>[0],
|
|
234
|
+
merge: (previous: ArgsOf<H[K]>[0], patch: Exclude<SuccessOf<H[K]>, void>) => ArgsOf<H[K]>[0]
|
|
235
|
+
) => Effect.Effect<ArgsOf<H[K]>[0], PluginError, ContextOf<H[K]>>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const make: <H = FlowsHooks>(resolved: Resolved<H>) => Service<H>
|
|
239
|
+
const makeNoop: <H = FlowsHooks>() => Service<H>
|
|
240
|
+
|
|
241
|
+
class Plugins extends Context.Service<Plugins, Service>()("flows/plugin/Plugins") {}
|
|
242
|
+
const layer: (resolved: Resolved) => Layer.Layer<Plugins>
|
|
243
|
+
const layerNoop: Layer.Layer<Plugins>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Dispatch semantics, one row per kind:
|
|
247
|
+
|
|
248
|
+
| Method | Runs | Returns | On handler failure |
|
|
249
|
+
| ------------ | --------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
250
|
+
| `sequential` | Every handler, in resolved order, one at a time. | Each handler's success value, in resolved order. | Fails the caller with `hook_failed` and stops. |
|
|
251
|
+
| `parallel` | Every handler, bounded by `Resolved.parallelConcurrency`. | The failures, in resolved handler order. Success values are discarded. | Never fails the caller. |
|
|
252
|
+
| `first` | Handlers in order until one returns `Option.some`. | That `Option`, or `Option.none()`. | Fails with `hook_failed`; a non-`Option` value fails with `invalid_hook_result`. |
|
|
253
|
+
| `waterfall` | Every handler, threading the merged value. | The final value. | Fails with `hook_failed` and stops. |
|
|
254
|
+
|
|
255
|
+
Every method first compares its kind with the runtime catalog the resolution
|
|
256
|
+
admitted (`Resolved.kinds`). A hook the catalog declares with a different kind
|
|
257
|
+
is refused before any handler runs: `sequential`, `first`, and `waterfall` fail
|
|
258
|
+
with `hook_kind_mismatch`, and `parallel` returns that one error. A hook name
|
|
259
|
+
absent from the catalog dispatches nothing. The typed signatures make the
|
|
260
|
+
mismatch unreachable without a cast; the runtime check covers the cast.
|
|
261
|
+
|
|
262
|
+
Handlers must return an Effect. Synchronous throws, Effect failures and defects,
|
|
263
|
+
and non-Effect results (including `undefined`, scalars, and Promises) become
|
|
264
|
+
`hook_failed` with the plugin and hook names. Non-Effect error messages include
|
|
265
|
+
the result's JavaScript `typeof`. Parallel dispatch collects these errors and
|
|
266
|
+
continues running sibling observers, including with `parallelConcurrency: 1`.
|
|
267
|
+
`Kernel.make` succeeds and reports them in `observerErrors`. Sequential dispatch
|
|
268
|
+
fails with the attributed error and skips later handlers.
|
|
269
|
+
|
|
270
|
+
A waterfall handler whose Effect succeeds with `undefined` leaves the value
|
|
271
|
+
unchanged: the merge function is not called. A merge that throws a `PluginError` keeps that
|
|
272
|
+
error's code and path and gains the handler's attribution; a merge that throws
|
|
273
|
+
anything else becomes `config_invalid` attributed to the handler.
|
|
274
|
+
|
|
275
|
+
Cancellation is fiber interruption through scope closure. Nothing threads an
|
|
276
|
+
`AbortSignal`. Interrupting a dispatch interrupts the handler in flight, runs
|
|
277
|
+
its finalizers, and skips the handlers that had not started.
|
|
278
|
+
|
|
279
|
+
The `Plugins` service tag, `layer`, and `layerNoop` hold one dispatcher over the
|
|
280
|
+
process-wide augmented `FlowsHooks`. A host typed against a separate hook
|
|
281
|
+
interface holds its `Service<H>` directly and uses none of the three.
|
|
282
|
+
|
|
283
|
+
## Resolve
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
interface HandlerRecord {
|
|
287
|
+
readonly plugin: string
|
|
288
|
+
readonly hook: string
|
|
289
|
+
readonly handler: (...args: Array<any>) => unknown
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface Resolved<H = FlowsHooks> {
|
|
293
|
+
readonly plugins: ReadonlyArray<FlowsPlugin<H>>
|
|
294
|
+
readonly handlers: ReadonlyMap<string, ReadonlyArray<HandlerRecord>>
|
|
295
|
+
readonly kinds: Readonly<Record<string, HookKind>>
|
|
296
|
+
readonly parallelConcurrency: number
|
|
297
|
+
readonly cacheEnvironment?: Action.CacheEnvironment | undefined
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
interface Options<H = FlowsHooks> {
|
|
301
|
+
readonly config?: FlowsConfig | undefined
|
|
302
|
+
readonly target?: "engine" | "harness" | undefined
|
|
303
|
+
readonly hooks?: HookCatalog<H> | undefined
|
|
304
|
+
readonly cacheEnvironment?: Action.CacheEnvironment | undefined
|
|
305
|
+
readonly parallelConcurrency?: number | undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const resolve: <H = FlowsHooks>(
|
|
309
|
+
input: PluginInput<NoInfer<H>>,
|
|
310
|
+
options?: Options<NoInfer<H>>,
|
|
311
|
+
configOverride?: FlowsConfig
|
|
312
|
+
) => Effect.Effect<Resolved<H>, PluginError>
|
|
313
|
+
|
|
314
|
+
const layer: <H>(resolved: Resolved<H>) => Layer.Layer<any, PluginError, any>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| Option | Default | Meaning |
|
|
318
|
+
| --------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
319
|
+
| `config` | `{}` | Pre-resolution configuration tested by `apply` predicates. `Kernel.make` supplies it positionally and omits this field. Supplying both `config` and `configOverride` fails with `invalid_plugin` at `$options.config`. |
|
|
320
|
+
| `target` | `"engine"` | The host whose literal `apply` selectors are active. |
|
|
321
|
+
| `hooks` | `engineHooks` | The hook names and kinds this host recognizes, typed as `HookCatalog<H>`. The `unknown_hook` guard checks against it, the frozen copy is `Resolved.kinds`, and `config` or `configResolved` labelled with any kind but `waterfall` or `parallel` fails with `hook_kind_mismatch` at `$options.hooks.<name>`. |
|
|
322
|
+
| `cacheEnvironment` | absent | Complete composition identity for sealed activity keys. Requires a `version` on every selected plugin. |
|
|
323
|
+
| `parallelConcurrency` | `16` | Maximum observers run at once. A positive safe integer through 256. |
|
|
324
|
+
|
|
325
|
+
`resolve` is once-only and everything it returns is a copy the kernel owns.
|
|
326
|
+
Plugin records and hook objects are snapshotted, handler records and ordered
|
|
327
|
+
arrays are frozen, and `handlers` is a read-only map facade with no `set`,
|
|
328
|
+
`delete`, or `clear`. Reflection is descriptor-only: accessors never execute,
|
|
329
|
+
exotic prototypes are refused, and a hostile proxy observes a bounded number of
|
|
330
|
+
traps whose results are copied rather than retained.
|
|
331
|
+
|
|
332
|
+
Order of the checks: options, flatten, validate every record, filter by `apply`,
|
|
333
|
+
check hook names against the catalog for selected plugins, reject duplicate
|
|
334
|
+
names, order, then decode the cache environment. Structure is validated before
|
|
335
|
+
filtering, so exclusion cannot hide a malformed plugin; catalog names are
|
|
336
|
+
checked after, so a shared preset carrying harness-only hooks still resolves
|
|
337
|
+
under an engine kernel.
|
|
338
|
+
|
|
339
|
+
Ordering follows Vite exactly. `enforce` sorts the plugin list once into pre,
|
|
340
|
+
normal, and post, stably within each group. The per-hook `order` then
|
|
341
|
+
re-partitions that list for one hook, so `{ order: "pre", handler }` runs ahead
|
|
342
|
+
of every normal-order handler even when its plugin is `enforce: "post"`. Ties
|
|
343
|
+
keep resolved plugin order.
|
|
344
|
+
|
|
345
|
+
`layer` merges every selected plugin's layer left to right, so an earlier
|
|
346
|
+
plugin's services are visible to a later plugin's layer, and the later plugin
|
|
347
|
+
wins when two provide the same tag. A layer that fails during acquisition
|
|
348
|
+
becomes `layer_failed` with the plugin's name. `layer` also seals
|
|
349
|
+
`Action.CurrentCacheEnvironment`: plugin layers read the declared environment,
|
|
350
|
+
or `undefined` when none was declared, and a plugin layer that provides its own
|
|
351
|
+
environment is overridden.
|
|
352
|
+
|
|
353
|
+
### Limits
|
|
354
|
+
|
|
355
|
+
| Constant | Value | Refusal |
|
|
356
|
+
| ---------------------------- | ----- | ----------------------------- |
|
|
357
|
+
| `maximumPlugins` | 256 | `resource_limit` |
|
|
358
|
+
| `maximumHandlers` | 1,024 | `resource_limit` |
|
|
359
|
+
| `maximumPluginInputNodes` | 4,096 | `resource_limit` |
|
|
360
|
+
| `maximumPluginDepth` | 64 | `resource_limit` |
|
|
361
|
+
| `maximumParallelConcurrency` | 256 | `resource_limit` |
|
|
362
|
+
| `maximumPluginNameLength` | 256 | `invalid_plugin` |
|
|
363
|
+
| `defaultParallelConcurrency` | 16 | none, it is the default value |
|
|
364
|
+
|
|
365
|
+
A `resource_limit` carries the path of the offending entry or preset array, and
|
|
366
|
+
the handler bound counts the handlers the kernel dispatches. Plugin, version,
|
|
367
|
+
and hook names are a separate check, not a budget: they are control-free,
|
|
368
|
+
well-formed strings within `maximumPluginNameLength` that are neither empty nor
|
|
369
|
+
entirely whitespace, and a name that is not fails with `invalid_plugin` at its
|
|
370
|
+
own path even when every budget is free.
|
|
371
|
+
|
|
372
|
+
## Config
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const ConfigValue: Schema.Json
|
|
376
|
+
type ConfigValue = typeof ConfigValue.Type
|
|
377
|
+
|
|
378
|
+
const FlowsConfig: Schema.Codec<Readonly<Record<string, ConfigValue>>, unknown>
|
|
379
|
+
type FlowsConfig = typeof FlowsConfig.Type
|
|
380
|
+
|
|
381
|
+
const ResolvedConfig: typeof FlowsConfig
|
|
382
|
+
type ResolvedConfig = typeof ResolvedConfig.Type
|
|
383
|
+
|
|
384
|
+
const defaults: ResolvedConfig
|
|
385
|
+
|
|
386
|
+
const merge: (base: FlowsConfig, patch: unknown) => FlowsConfig
|
|
387
|
+
const deepFreeze: <A extends ConfigValue>(value: A) => A
|
|
388
|
+
const snapshot: (config: unknown) => Effect.Effect<FlowsConfig, PluginError>
|
|
389
|
+
const resolve: (config: unknown) => Effect.Effect<ResolvedConfig, PluginError>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| Export | Behavior |
|
|
393
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
394
|
+
| `defaults` | The frozen empty configuration. Engine policy is deliberately not defaulted here. |
|
|
395
|
+
| `merge` | Copies and deep-merges a patch over a base. Records merge key by key; every other JSON value replaces wholesale. Raw operands are admitted; unchanged snapshot subtrees are reused. Cached totals enforce result bounds. Throws `PluginError` with code `config_invalid`. |
|
|
396
|
+
| `deepFreeze` | Copies and recursively freezes one JSON value without retaining caller-owned objects. Throws `PluginError`. |
|
|
397
|
+
| `snapshot` | Admits a raw pre-resolution configuration as an immutable snapshot. |
|
|
398
|
+
| `resolve` | Decodes the post-waterfall configuration into its final immutable form. |
|
|
399
|
+
|
|
400
|
+
`FlowsConfig` and `ResolvedConfig` decode through the same admission contract as
|
|
401
|
+
`snapshot` and `resolve`. Decoding raw input detaches and recursively freezes it;
|
|
402
|
+
invalid input produces a schema error. `Schema.is` recognizes admitted snapshots,
|
|
403
|
+
not mutable JSON shapes. Encoding accepts admitted snapshots and returns their
|
|
404
|
+
JSON value. The TypeScript types remain read-only namespace maps.
|
|
405
|
+
|
|
406
|
+
Known snapshots can be reused by `snapshot`, `resolve`, and `merge`. Freezing an
|
|
407
|
+
object yourself does not establish admission. A merge copies each changed record
|
|
408
|
+
and retains unchanged frozen subtrees. It fully admits and detaches patch data,
|
|
409
|
+
including references to previous snapshots, so patches cannot introduce shared
|
|
410
|
+
references. Cached byte, member, node, and depth totals enforce result bounds.
|
|
411
|
+
Admission work scales with the initial tree plus patch data; copying changed
|
|
412
|
+
records also scales with their immediate key counts.
|
|
413
|
+
|
|
414
|
+
Configuration is a plugin-owned JSON namespace map. The root keys `engine`,
|
|
415
|
+
`retry`, `store`, and `plugins` are refused, because the kernel does not apply
|
|
416
|
+
those policies.
|
|
417
|
+
|
|
418
|
+
Refused with `config_invalid`, at the offending path, without executing user
|
|
419
|
+
accessors: cycles, repeated object references, sparse arrays, accessors, symbol
|
|
420
|
+
keys, exotic prototypes, non-finite numbers, unpaired surrogates, `undefined`
|
|
421
|
+
members, and the prototype-control keys `__proto__`, `constructor`, and
|
|
422
|
+
`prototype`. `undefined` is refused rather than dropped, so
|
|
423
|
+
`{ endpoint: undefined }` fails at `$.endpoint` instead of resolving to `{}`.
|
|
424
|
+
Omit the key, or write `null`.
|
|
425
|
+
|
|
426
|
+
### Limits
|
|
427
|
+
|
|
428
|
+
| Constant | Value |
|
|
429
|
+
| ---------------------- | ----------------------- |
|
|
430
|
+
| `maximumConfigBytes` | 1 MiB of encoded JSON |
|
|
431
|
+
| `maximumConfigDepth` | 64 container edges |
|
|
432
|
+
| `maximumConfigMembers` | 4,096 aggregate members |
|
|
433
|
+
| `maximumConfigNodes` | 8,192 aggregate values |
|
|
434
|
+
|
|
435
|
+
Strings are limited to 64 KiB of encoded JSON and keys to 1 KiB.
|
|
436
|
+
|
|
437
|
+
## Cache identity
|
|
438
|
+
|
|
439
|
+
`Options.cacheEnvironment` is optional. Omitting it keeps sealed activity keys
|
|
440
|
+
local to the run and leaves plugin `version` optional. Supplying it requires the
|
|
441
|
+
complete `Action.CacheEnvironment` schema and a bounded, non-empty `version` on
|
|
442
|
+
every selected plugin.
|
|
443
|
+
|
|
444
|
+
Resolution copies and freezes every layer entry, capability record, and
|
|
445
|
+
capability array, then prepends each selected `name@version` in resolved order.
|
|
446
|
+
Both halves percent-escape `%` and then `@`, so the identity is injective:
|
|
447
|
+
`a@b` at version `c` and `a` at version `b@c` produce different layer entries
|
|
448
|
+
instead of one shared sealed identity. A scoped name reads as
|
|
449
|
+
`%40scope/name@1.0.0`. A versionless composition fails with
|
|
450
|
+
`cache_environment_invalid` instead of declaring an ambiguous cross-run
|
|
451
|
+
identity. Invalid or mutable caller data never reaches
|
|
452
|
+
`Action.CurrentCacheEnvironment`, and no plugin layer can declare or replace
|
|
453
|
+
it.
|
|
454
|
+
|
|
455
|
+
## PluginError
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
const PluginErrorCode: Schema.Literals<[
|
|
459
|
+
"duplicate_name",
|
|
460
|
+
"unknown_hook",
|
|
461
|
+
"hook_kind_mismatch",
|
|
462
|
+
"invalid_plugin",
|
|
463
|
+
"apply_failed",
|
|
464
|
+
"config_invalid",
|
|
465
|
+
"cache_environment_invalid",
|
|
466
|
+
"invalid_hook_result",
|
|
467
|
+
"resource_limit",
|
|
468
|
+
"hook_failed",
|
|
469
|
+
"layer_failed"
|
|
470
|
+
]>
|
|
471
|
+
type PluginErrorCode = typeof PluginErrorCode.Type
|
|
472
|
+
|
|
473
|
+
class PluginError extends Schema.TaggedError<PluginError>()("flows/plugin/PluginError", {
|
|
474
|
+
code: PluginErrorCode,
|
|
475
|
+
message: Schema.String,
|
|
476
|
+
plugin: Schema.optional(Schema.String),
|
|
477
|
+
hook: Schema.optional(Schema.String),
|
|
478
|
+
path: Schema.optional(Schema.String),
|
|
479
|
+
cause: Schema.optional(Schema.Unknown)
|
|
480
|
+
}) {}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Every startup and dispatch refusal uses this one error. `plugin` and `hook` are
|
|
484
|
+
attribution, `path` is a diagnostic dollar-path into the refused value, and
|
|
485
|
+
`cause` carries a failing handler's or layer's original failure. Match on
|
|
486
|
+
`code`; the messages are prose.
|
|
487
|
+
|
|
488
|
+
`cause` is not redacted. It holds whatever the plugin failed or threw with,
|
|
489
|
+
which can include credentials or config values. Keep it in process for
|
|
490
|
+
diagnostics, and redact it before a host logs, persists, or transmits a
|
|
491
|
+
`PluginError`.
|
|
492
|
+
|
|
493
|
+
A dollar-path is not an RFC 6901 JSON Pointer: `$` is the input, `.config` an
|
|
494
|
+
identifier property, `[2]` an array index, and `["not an identifier"]` any other
|
|
495
|
+
key, JSON-quoted. So `$[2].hooks.config` is the third plugin's `config` entry
|
|
496
|
+
and `$options.parallelConcurrency` is an option.
|
|
497
|
+
|
|
498
|
+
| Code | Meaning |
|
|
499
|
+
| --------------------------- | ------------------------------------------------------------------------------ |
|
|
500
|
+
| `duplicate_name` | Two selected plugins have the same exact name. |
|
|
501
|
+
| `unknown_hook` | A plugin declares a hook absent from the host catalog. |
|
|
502
|
+
| `hook_kind_mismatch` | A catalog kind disagrees with the kind the kernel or a dispatch method runs. |
|
|
503
|
+
| `invalid_plugin` | A plugin, preset, option, or hook entry has an invalid runtime shape. |
|
|
504
|
+
| `apply_failed` | An `apply` predicate threw. Its raw failure is not retained. |
|
|
505
|
+
| `config_invalid` | Config is not bounded strict JSON or uses a reserved policy key. |
|
|
506
|
+
| `cache_environment_invalid` | Cache identity is malformed or incomplete. |
|
|
507
|
+
| `invalid_hook_result` | A first hook returned a value other than `Option`. |
|
|
508
|
+
| `resource_limit` | Preset, plugin, handler, or concurrency work exceeds a published bound. |
|
|
509
|
+
| `hook_failed` | A hook Effect failed, defected, threw synchronously, or returned a non-Effect. |
|
|
510
|
+
| `layer_failed` | A validated plugin layer failed during acquisition. |
|
|
511
|
+
|
|
512
|
+
`configResolved` is a lossy observer boundary. Its failures do not fail kernel
|
|
513
|
+
startup; `Kernel.observerErrors` returns them for redacted logging by the host.
|
|
514
|
+
What to change for each code is in [Troubleshooting](./troubleshooting.md).
|
|
515
|
+
|
|
516
|
+
## Related packages
|
|
517
|
+
|
|
518
|
+
The shipped hook catalog is the Smithers agent loop in
|
|
519
|
+
[`@smthrs/agent`](/api/agent), which augments `FlowsHooks` with three
|
|
520
|
+
waterfalls. `Action.CacheEnvironment` comes from
|
|
521
|
+
[`@smthrs/flow`](/api/flow). The other runtime dependency,
|
|
522
|
+
[`@smthrs/canonical`](/api/canonical), supplies immutable catalog maps.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Configuration"
|
|
3
|
+
description: "The config waterfall and the JSON boundary it enforces: plugin-owned namespaces, reserved root keys, deep merge semantics, admission rules, and the published size limits."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Configuration is the one lifecycle the kernel owns. It is two hooks: `config`,
|
|
9
|
+
a waterfall in which each plugin may return a patch, and `configResolved`, a
|
|
10
|
+
parallel observer that sees the frozen result. `Kernel.make` runs both.
|
|
11
|
+
|
|
12
|
+
## Configuration is a namespace map, not a policy surface
|
|
13
|
+
|
|
14
|
+
A configuration is a flat record whose root keys name plugin-owned namespaces.
|
|
15
|
+
Each namespace holds JSON, and the kernel never interprets it: the plugin that
|
|
16
|
+
owns a namespace is the only code that knows what its contents mean.
|
|
17
|
+
|
|
18
|
+
Four root keys are refused with `config_invalid`: `engine`, `retry`, `store`,
|
|
19
|
+
and `plugins`. The kernel does not apply retry, storage, concurrency, cache,
|
|
20
|
+
wait, journal, or ownership policy, and accepting those names would imply that
|
|
21
|
+
setting them changes something. Those policies stay on the Effect service or
|
|
22
|
+
constructor option that applies them.
|
|
23
|
+
|
|
24
|
+
## The waterfall merges patches
|
|
25
|
+
|
|
26
|
+
Each `config` handler receives the accumulated configuration and returns a
|
|
27
|
+
partial to merge, or nothing:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { type FlowsPlugin, make } from "@smthrs/plugin"
|
|
31
|
+
import * as Effect from "effect/Effect"
|
|
32
|
+
|
|
33
|
+
const plugin: FlowsPlugin = make({
|
|
34
|
+
name: "flows-plugin-endpoint",
|
|
35
|
+
hooks: {
|
|
36
|
+
config: (config) =>
|
|
37
|
+
config["endpoint"] === undefined
|
|
38
|
+
? Effect.succeed({ endpoint: { url: "https://example.test" } })
|
|
39
|
+
: Effect.void
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`Config.merge` is the merge function `Kernel.make` uses. Records merge key by
|
|
45
|
+
key, recursively. Every other JSON value, including an array, replaces the
|
|
46
|
+
previous value wholesale:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
Config.merge({ a: { b: 1, c: 2 } }, { a: { c: 3, d: 4 } }) // { a: { b: 1, c: 3, d: 4 } }
|
|
50
|
+
Config.merge({ a: [1] }, { a: [2] }) // { a: [2] }
|
|
51
|
+
Config.merge({ a: { b: 1 } }, { a: 5 }) // { a: 5 }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Both operands and the merged result are admitted, so no handler in the chain
|
|
55
|
+
ever observes a value that breaks the rules below, even when each individual
|
|
56
|
+
patch was within them.
|
|
57
|
+
|
|
58
|
+
## What the boundary admits
|
|
59
|
+
|
|
60
|
+
Configuration is strict JSON, copied before any plugin observes it and frozen
|
|
61
|
+
recursively afterward. The following are refused with `config_invalid` and the
|
|
62
|
+
path of the offending value:
|
|
63
|
+
|
|
64
|
+
- Cycles and repeated object references.
|
|
65
|
+
- Sparse arrays, arrays with extra properties, and array subclasses.
|
|
66
|
+
- Accessors, symbol keys, non-enumerable properties, and exotic prototypes.
|
|
67
|
+
- Non-finite numbers, unpaired surrogates, and non-JSON values such as `Date`
|
|
68
|
+
and `Map`.
|
|
69
|
+
- The prototype-control keys `__proto__`, `constructor`, and `prototype`, at
|
|
70
|
+
every depth.
|
|
71
|
+
- `undefined` members.
|
|
72
|
+
|
|
73
|
+
The last one is a decision worth naming. `{ endpoint: undefined }` fails at
|
|
74
|
+
`$.endpoint` instead of resolving to `{}`. A silent drop hid a typo behind a
|
|
75
|
+
value the next handler never saw. To leave something unset, omit the key or
|
|
76
|
+
write `null`.
|
|
77
|
+
|
|
78
|
+
Admission reads a value through `Reflect.ownKeys`, `Object.getPrototypeOf`, and
|
|
79
|
+
one property descriptor per key. An accessor is never called, so a proxy sees a
|
|
80
|
+
bounded number of traps and only the data those traps return is copied. A trap
|
|
81
|
+
that throws refuses the whole tree.
|
|
82
|
+
|
|
83
|
+
## Size limits
|
|
84
|
+
|
|
85
|
+
| Limit | Value | Export |
|
|
86
|
+
| ------------------------------- | ----- | ----------------------------- |
|
|
87
|
+
| Encoded bytes per configuration | 1 MiB | `Config.maximumConfigBytes` |
|
|
88
|
+
| Container nesting depth | 64 | `Config.maximumConfigDepth` |
|
|
89
|
+
| Aggregate members | 4,096 | `Config.maximumConfigMembers` |
|
|
90
|
+
| Aggregate values | 8,192 | `Config.maximumConfigNodes` |
|
|
91
|
+
|
|
92
|
+
Strings are limited to 64 KiB of encoded JSON and keys to 1 KiB. A tree over any
|
|
93
|
+
bound fails with `config_invalid`, and because the merged result is admitted
|
|
94
|
+
too, a chain of small patches cannot add up past them.
|
|
95
|
+
|
|
96
|
+
## configResolved is a lossy observer boundary
|
|
97
|
+
|
|
98
|
+
After the waterfall settles, `Kernel.make` decodes and freezes the result, then
|
|
99
|
+
runs every `configResolved` handler in parallel. Those handlers are observers:
|
|
100
|
+
their success values are discarded, and their failures do not fail startup.
|
|
101
|
+
Read them from `kernel.observerErrors` and log them at your own boundary:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const kernel = await Effect.runPromise(Kernel.make([plugin]))
|
|
105
|
+
for (const error of kernel.observerErrors) {
|
|
106
|
+
console.warn(`plugin ${error.plugin} failed to observe the config: ${error.code}`)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Dropping them on the floor is the one thing to avoid. A plugin whose observer
|
|
111
|
+
fails silently looks configured and is not.
|