@smthrs/plugin 0.0.0-stage → 1.0.0-rc.3

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.
Files changed (118) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +86 -2
  4. package/dist/cjs/Config.d.ts +129 -0
  5. package/dist/cjs/Config.d.ts.map +1 -0
  6. package/dist/cjs/Config.js +115 -0
  7. package/dist/cjs/Config.js.map +7 -0
  8. package/dist/cjs/Hooks.d.ts +176 -0
  9. package/dist/cjs/Hooks.d.ts.map +1 -0
  10. package/dist/cjs/Hooks.js +34 -0
  11. package/dist/cjs/Hooks.js.map +7 -0
  12. package/dist/cjs/Kernel.d.ts +81 -0
  13. package/dist/cjs/Kernel.d.ts.map +1 -0
  14. package/dist/cjs/Kernel.js +57 -0
  15. package/dist/cjs/Kernel.js.map +7 -0
  16. package/dist/cjs/Plugin.d.ts +71 -0
  17. package/dist/cjs/Plugin.d.ts.map +1 -0
  18. package/dist/cjs/Plugin.js +25 -0
  19. package/dist/cjs/Plugin.js.map +7 -0
  20. package/dist/cjs/PluginError.d.ts +50 -0
  21. package/dist/cjs/PluginError.d.ts.map +1 -0
  22. package/dist/cjs/PluginError.js +58 -0
  23. package/dist/cjs/PluginError.js.map +7 -0
  24. package/dist/cjs/Plugins.d.ts +97 -0
  25. package/dist/cjs/Plugins.d.ts.map +1 -0
  26. package/dist/cjs/Plugins.js +160 -0
  27. package/dist/cjs/Plugins.js.map +7 -0
  28. package/dist/cjs/Resolve.d.ts +128 -0
  29. package/dist/cjs/Resolve.d.ts.map +1 -0
  30. package/dist/cjs/Resolve.js +343 -0
  31. package/dist/cjs/Resolve.js.map +7 -0
  32. package/dist/cjs/index.d.ts +83 -0
  33. package/dist/cjs/index.d.ts.map +1 -0
  34. package/dist/cjs/index.js +45 -0
  35. package/dist/cjs/index.js.map +7 -0
  36. package/dist/cjs/internal/Boundary.d.ts +96 -0
  37. package/dist/cjs/internal/Boundary.d.ts.map +1 -0
  38. package/dist/cjs/internal/Boundary.js +129 -0
  39. package/dist/cjs/internal/Boundary.js.map +7 -0
  40. package/dist/cjs/internal/mergePluginLayers.d.ts +21 -0
  41. package/dist/cjs/internal/mergePluginLayers.d.ts.map +1 -0
  42. package/dist/cjs/internal/mergePluginLayers.js +57 -0
  43. package/dist/cjs/internal/mergePluginLayers.js.map +7 -0
  44. package/dist/cjs/internal/snapshotCacheEnvironment.d.ts +19 -0
  45. package/dist/cjs/internal/snapshotCacheEnvironment.d.ts.map +1 -0
  46. package/dist/cjs/internal/snapshotCacheEnvironment.js +92 -0
  47. package/dist/cjs/internal/snapshotCacheEnvironment.js.map +7 -0
  48. package/dist/cjs/package.json +1 -0
  49. package/dist/esm/Config.d.ts +129 -0
  50. package/dist/esm/Config.d.ts.map +1 -0
  51. package/dist/esm/Config.js +160 -0
  52. package/dist/esm/Config.js.map +1 -0
  53. package/dist/esm/Hooks.d.ts +176 -0
  54. package/dist/esm/Hooks.d.ts.map +1 -0
  55. package/dist/esm/Hooks.js +56 -0
  56. package/dist/esm/Hooks.js.map +1 -0
  57. package/dist/esm/Kernel.d.ts +81 -0
  58. package/dist/esm/Kernel.d.ts.map +1 -0
  59. package/dist/esm/Kernel.js +58 -0
  60. package/dist/esm/Kernel.js.map +1 -0
  61. package/dist/esm/Plugin.d.ts +71 -0
  62. package/dist/esm/Plugin.d.ts.map +1 -0
  63. package/dist/esm/Plugin.js +20 -0
  64. package/dist/esm/Plugin.js.map +1 -0
  65. package/dist/esm/PluginError.d.ts +50 -0
  66. package/dist/esm/PluginError.d.ts.map +1 -0
  67. package/dist/esm/PluginError.js +53 -0
  68. package/dist/esm/PluginError.js.map +1 -0
  69. package/dist/esm/Plugins.d.ts +97 -0
  70. package/dist/esm/Plugins.d.ts.map +1 -0
  71. package/dist/esm/Plugins.js +178 -0
  72. package/dist/esm/Plugins.js.map +1 -0
  73. package/dist/esm/Resolve.d.ts +128 -0
  74. package/dist/esm/Resolve.d.ts.map +1 -0
  75. package/dist/esm/Resolve.js +367 -0
  76. package/dist/esm/Resolve.js.map +1 -0
  77. package/dist/esm/index.d.ts +83 -0
  78. package/dist/esm/index.d.ts.map +1 -0
  79. package/dist/esm/index.js +48 -0
  80. package/dist/esm/index.js.map +1 -0
  81. package/dist/esm/internal/Boundary.d.ts +96 -0
  82. package/dist/esm/internal/Boundary.d.ts.map +1 -0
  83. package/dist/esm/internal/Boundary.js +162 -0
  84. package/dist/esm/internal/Boundary.js.map +1 -0
  85. package/dist/esm/internal/mergePluginLayers.d.ts +21 -0
  86. package/dist/esm/internal/mergePluginLayers.d.ts.map +1 -0
  87. package/dist/esm/internal/mergePluginLayers.js +41 -0
  88. package/dist/esm/internal/mergePluginLayers.js.map +1 -0
  89. package/dist/esm/internal/snapshotCacheEnvironment.d.ts +19 -0
  90. package/dist/esm/internal/snapshotCacheEnvironment.d.ts.map +1 -0
  91. package/dist/esm/internal/snapshotCacheEnvironment.js +63 -0
  92. package/dist/esm/internal/snapshotCacheEnvironment.js.map +1 -0
  93. package/docs/README.md +131 -0
  94. package/docs/api.md +522 -0
  95. package/docs/concepts/configuration.md +111 -0
  96. package/docs/concepts/hook-kinds.md +107 -0
  97. package/docs/concepts/resolution.md +132 -0
  98. package/docs/guides/cache-identity.md +106 -0
  99. package/docs/guides/contribute-services.md +84 -0
  100. package/docs/guides/host-your-own-hooks.md +129 -0
  101. package/docs/guides/order-handlers.md +76 -0
  102. package/docs/guides/select-plugins.md +72 -0
  103. package/docs/guides/testing.md +149 -0
  104. package/docs/installation.md +71 -0
  105. package/docs/quickstart.md +151 -0
  106. package/docs/troubleshooting.md +179 -0
  107. package/package.json +179 -3
  108. package/src/Config.ts +207 -0
  109. package/src/Hooks.ts +212 -0
  110. package/src/Kernel.ts +119 -0
  111. package/src/Plugin.ts +80 -0
  112. package/src/PluginError.ts +62 -0
  113. package/src/Plugins.ts +254 -0
  114. package/src/Resolve.ts +535 -0
  115. package/src/index.ts +91 -0
  116. package/src/internal/Boundary.ts +252 -0
  117. package/src/internal/mergePluginLayers.ts +57 -0
  118. package/src/internal/snapshotCacheEnvironment.ts +77 -0
package/src/Kernel.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Startup: resolve the plugin list, run the config pipeline, hand back a
3
+ * dispatcher, a frozen config, and the merged layer.
4
+ *
5
+ * The public package contract is documented at
6
+ * {@link https://smithers.sh/docs/reference/api/plugin}, with the shipped host catalog in
7
+ * `packages/smithers/agent/src/CellPlugin.ts`. D14 in
8
+ * `docs/pages/design-decisions.md` keeps durable policy outside these hooks.
9
+ *
10
+ * Order of operations, per the spec:
11
+ *
12
+ * 1. flatten / filter / validate / order the plugin list;
13
+ * 2. `config` waterfall: each plugin may return a partial to deep-merge;
14
+ * 3. decode and freeze into a `ResolvedConfig` (`config_invalid` on failure);
15
+ * 4. `configResolved` parallel: observers capture the final config;
16
+ * 5. plugin `layer`s merge left-to-right.
17
+ *
18
+ * @since 1.0.0-rc.0
19
+ */
20
+
21
+ import * as Effect from "effect/Effect"
22
+ import type * as Layer from "effect/Layer"
23
+ import type { FlowsConfig, ResolvedConfig } from "./Config.ts"
24
+ import * as Config from "./Config.ts"
25
+ import type { ContextOf } from "./Hooks.ts"
26
+ import type { FlowsHooks } from "./index.ts"
27
+ import type { PluginInput } from "./Plugin.ts"
28
+ import type { PluginError } from "./PluginError.ts"
29
+ import * as Plugins from "./Plugins.ts"
30
+ import * as Resolve from "./Resolve.ts"
31
+
32
+ /**
33
+ * Everything startup produces from a plugin list and a raw config.
34
+ *
35
+ * `observerErrors` carries `configResolved` failures: parallel observers are
36
+ * lossy by contract, so they are reported rather than fatal.
37
+ *
38
+ * @category models
39
+ * @since 1.0.0-rc.0
40
+ */
41
+ export interface Kernel<H = FlowsHooks> {
42
+ readonly plugins: Plugins.Service<H>
43
+ readonly config: ResolvedConfig
44
+ readonly layer: Layer.Layer<any, PluginError, any>
45
+ readonly observerErrors: ReadonlyArray<PluginError>
46
+ }
47
+
48
+ type HookAt<H, K extends string> = K extends keyof H ? H[K] : never
49
+
50
+ /**
51
+ * The services a hook interface's `config` hook requires at startup. `never`
52
+ * for the shared `FlowsHooks`, whose startup hooks are context-free.
53
+ *
54
+ * @category type-level
55
+ * @since 1.0.0-rc.0
56
+ */
57
+ export type ConfigContext<H> = ContextOf<HookAt<H, "config">>
58
+
59
+ /**
60
+ * The services a hook interface's startup hooks require: the union of the
61
+ * `config` and `configResolved` requirements. `Kernel.make` cannot supply
62
+ * them itself, so they surface in its Effect and the caller provides them.
63
+ *
64
+ * @category type-level
65
+ * @since 1.0.0-rc.0
66
+ */
67
+ export type StartupContext<H> = ConfigContext<H> | ContextOf<HookAt<H, "configResolved">>
68
+
69
+ /**
70
+ * Snapshots the config before dispatching the `config` waterfall, then
71
+ * admits the result as a frozen `ResolvedConfig`.
72
+ *
73
+ * @category constructors
74
+ * @since 1.0.0-rc.0
75
+ */
76
+ export const runConfig = <H = FlowsHooks>(
77
+ plugins: Plugins.Service<H>,
78
+ config: FlowsConfig
79
+ ): Effect.Effect<ResolvedConfig, PluginError, ConfigContext<H>> =>
80
+ Effect.gen(function*() {
81
+ const initial = yield* Config.snapshot(config)
82
+ const merged = yield* (plugins.waterfall as unknown as (
83
+ hook: string,
84
+ initial: FlowsConfig,
85
+ merge: (previous: FlowsConfig, patch: unknown) => FlowsConfig
86
+ ) => Effect.Effect<FlowsConfig, PluginError, ConfigContext<H>>)("config", initial, Config.merge)
87
+ return yield* Config.resolve(merged)
88
+ })
89
+
90
+ /**
91
+ * Resolves plugins, executes the config pipeline, and merges plugin layers.
92
+ * The positional `config` is the kernel's only pre-resolution configuration;
93
+ * the options object cannot declare a second source, and a `config` key
94
+ * supplied anyway fails with `invalid_plugin` at `$options.config`.
95
+ *
96
+ * @category constructors
97
+ * @since 1.0.0-rc.0
98
+ */
99
+ export const make = <H = FlowsHooks>(
100
+ input: PluginInput<NoInfer<H>>,
101
+ config: FlowsConfig = {},
102
+ options: Omit<Resolve.Options<NoInfer<H>>, "config"> = {}
103
+ ): Effect.Effect<Kernel<H>, PluginError, StartupContext<H>> =>
104
+ Effect.gen(function*() {
105
+ const initial = yield* Config.snapshot(config)
106
+ const resolved = yield* Resolve.resolve<H>(input, options, initial)
107
+ const dispatcher = Plugins.make<H>(resolved)
108
+ const frozen = yield* runConfig(dispatcher, initial)
109
+ const observerErrors = yield* (dispatcher.parallel as unknown as (
110
+ hook: string,
111
+ value: ResolvedConfig
112
+ ) => Effect.Effect<ReadonlyArray<PluginError>, never, StartupContext<H>>)("configResolved", frozen)
113
+ return Object.freeze({
114
+ plugins: dispatcher,
115
+ config: frozen,
116
+ layer: Resolve.layer(resolved),
117
+ observerErrors
118
+ })
119
+ })
package/src/Plugin.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The plugin object: a plain record produced by a factory function.
3
+ *
4
+ * The public package contract is documented at
5
+ * {@link https://smithers.sh/docs/reference/api/plugin}. The shipped consumer is the
6
+ * assembled cell host in `@smthrs/agent`;
7
+ * durable-core policy continues to use Effect services and constructor
8
+ * options rather than plugin lifecycle hooks.
9
+ *
10
+ * @since 1.0.0-rc.0
11
+ */
12
+
13
+ import type * as Layer from "effect/Layer"
14
+ import type { FlowsConfig } from "./Config.ts"
15
+ import type { FlowsHooks } from "./index.ts"
16
+
17
+ /**
18
+ * Conditional-inclusion predicate, mirroring Vite's `apply`.
19
+ *
20
+ * `"engine"` and `"harness"` select the matching plugin-kernel host. The
21
+ * assembled cell host resolves as `"harness"`; this does not make `"engine"` an
22
+ * engine-lifecycle hook registration.
23
+ *
24
+ * @category models
25
+ * @since 1.0.0-rc.0
26
+ */
27
+ export type Apply = "engine" | "harness" | ((config: FlowsConfig) => boolean)
28
+
29
+ /**
30
+ * A flows plugin.
31
+ *
32
+ * Generic over the hook interface so a harness can hold plugins typed against
33
+ * its augmented `FlowsHooks` without any inheritance machinery.
34
+ *
35
+ * @category models
36
+ * @since 1.0.0-rc.0
37
+ */
38
+ export interface FlowsPlugin<H = FlowsHooks> {
39
+ /** Required, unique. Convention: `flows-plugin-<thing>`. */
40
+ readonly name: string
41
+ /**
42
+ * Semantic identity folded into sealed cache keys. Required when the host
43
+ * declares a cache environment. `Action.CacheEnvironment` is complete by
44
+ * contract, so a versionless composition is refused instead of claiming an
45
+ * ambiguous cross-run identity.
46
+ */
47
+ readonly version?: string | undefined
48
+ /** Ordering group; omitted means `"normal"`. */
49
+ readonly enforce?: "pre" | "post" | undefined
50
+ /** Conditional inclusion. */
51
+ readonly apply?: Apply | undefined
52
+ /** Services this plugin contributes to the engine environment. */
53
+ readonly layer?: Layer.Layer<never, any, any> | undefined
54
+ /** Typed hooks; only keys declared in the hook interface compile. */
55
+ readonly hooks?: Partial<H> | undefined
56
+ }
57
+
58
+ /**
59
+ * What a `plugins` array may contain: plugins, falsy entries, and arrays nested
60
+ * up to `Resolve.maximumPluginDepth`: so a preset is just a function returning
61
+ * plugins.
62
+ *
63
+ * @category models
64
+ * @since 1.0.0-rc.0
65
+ */
66
+ export type PluginInput<H = FlowsHooks> =
67
+ | FlowsPlugin<H>
68
+ | false
69
+ | null
70
+ | undefined
71
+ | ReadonlyArray<PluginInput<H>>
72
+
73
+ /**
74
+ * Identity constructor that pins a plugin literal to `FlowsPlugin` so excess
75
+ * hook keys are rejected at the definition site.
76
+ *
77
+ * @category constructors
78
+ * @since 1.0.0-rc.0
79
+ */
80
+ export const make = <H = FlowsHooks>(plugin: FlowsPlugin<H>): FlowsPlugin<H> => plugin
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The single typed failure of the plugin kernel.
3
+ *
4
+ * The public package contract is documented at
5
+ * {@link https://smithers.sh/docs/reference/api/plugin}; D14 in
6
+ * `docs/pages/design-decisions.md` bounds it to host-owned cell hooks.
7
+ *
8
+ * @since 1.0.0-rc.0
9
+ */
10
+
11
+ import * as Schema from "effect/Schema"
12
+
13
+ /**
14
+ * Closed set of plugin-system failure codes.
15
+ *
16
+ * @category models
17
+ * @since 1.0.0-rc.0
18
+ */
19
+ export const PluginErrorCode = Schema.Literals([
20
+ "duplicate_name",
21
+ "unknown_hook",
22
+ "hook_kind_mismatch",
23
+ "invalid_plugin",
24
+ "apply_failed",
25
+ "config_invalid",
26
+ "cache_environment_invalid",
27
+ "invalid_hook_result",
28
+ "resource_limit",
29
+ "hook_failed",
30
+ "layer_failed"
31
+ ])
32
+
33
+ /**
34
+ * Closed set of plugin-system failure codes.
35
+ *
36
+ * @category models
37
+ * @since 1.0.0-rc.0
38
+ */
39
+ export type PluginErrorCode = typeof PluginErrorCode.Type
40
+
41
+ /**
42
+ * Failure raised by plugin resolution, hook dispatch, config execution, or
43
+ * layer construction.
44
+ *
45
+ * Parallel observer failures never fail the caller; they are returned from
46
+ * `Plugins.parallel` with this same shape so they can be journalled on the
47
+ * lossy telemetry channel.
48
+ *
49
+ * `cause` is the plugin's unredacted failure and may hold secrets; hosts
50
+ * redact it before logging, persisting, or transmitting the error.
51
+ *
52
+ * @category errors
53
+ * @since 1.0.0-rc.0
54
+ */
55
+ export class PluginError extends Schema.TaggedError<PluginError>()("flows/plugin/PluginError", {
56
+ code: PluginErrorCode,
57
+ message: Schema.String,
58
+ plugin: Schema.optional(Schema.String),
59
+ hook: Schema.optional(Schema.String),
60
+ path: Schema.optional(Schema.String),
61
+ cause: Schema.optional(Schema.Unknown)
62
+ }) {}
package/src/Plugins.ts ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The dispatcher: a plain service over a resolved plugin list, generic over the
3
+ * hook interface.
4
+ *
5
+ * The public package contract is documented at
6
+ * {@link https://smithers.sh/docs/reference/api/plugin}. Each host holds an instance over its
7
+ * augmented `FlowsHooks` and may dispatch only the runtime catalog it supplied.
8
+ * The shipped cell host owns the three
9
+ * waterfalls declared in `packages/smithers/agent/src/CellPlugin.ts`; there is
10
+ * no engine-wide lifecycle dispatcher.
11
+ *
12
+ * Cancellation is fiber interruption via scope closure; nothing here threads an
13
+ * `AbortSignal`.
14
+ *
15
+ * @since 1.0.0-rc.0
16
+ */
17
+
18
+ import * as ImmutableMap from "@smthrs/canonical/ReadonlyMap"
19
+ import type * as Cause from "effect/Cause"
20
+ import * as Context from "effect/Context"
21
+ import * as Effect from "effect/Effect"
22
+ import * as Layer from "effect/Layer"
23
+ import * as Option from "effect/Option"
24
+ import type { ArgsOf, ContextOf, HookKind, KeysOfKind, SuccessOf } from "./Hooks.ts"
25
+ import { engineHooks } from "./Hooks.ts"
26
+ import type { FlowsHooks } from "./index.ts"
27
+ import { PluginError } from "./PluginError.ts"
28
+ import { defaultParallelConcurrency, type HandlerRecord, type Resolved } from "./Resolve.ts"
29
+
30
+ const empty: ReadonlyArray<HandlerRecord> = Object.freeze([])
31
+
32
+ // The typed catalog and the runtime catalog can only drift when a host casts
33
+ // around `KeysOfKind`; refuse before any handler runs rather than execute a
34
+ // hook under semantics its declaration never promised.
35
+ const kindMismatch = (resolved: Resolved<unknown>, hook: string, kind: HookKind): PluginError | undefined => {
36
+ if (!Object.hasOwn(resolved.kinds, hook)) return undefined
37
+ const declared = resolved.kinds[hook]
38
+ if (declared === kind) return undefined
39
+ return new PluginError({
40
+ code: "hook_kind_mismatch",
41
+ message: `hook "${hook}" is declared ${JSON.stringify(declared)} in the runtime catalog but was dispatched as ${
42
+ JSON.stringify(kind)
43
+ }`,
44
+ hook
45
+ })
46
+ }
47
+
48
+ const runHandler = (
49
+ record: HandlerRecord,
50
+ args: ReadonlyArray<unknown>
51
+ ): Effect.Effect<unknown, PluginError, any> => {
52
+ const onCause = (cause: Cause.Cause<unknown>) =>
53
+ Effect.fail(
54
+ new PluginError({
55
+ code: "hook_failed",
56
+ message: `hook "${record.hook}" failed in plugin "${record.plugin}"`,
57
+ plugin: record.plugin,
58
+ hook: record.hook,
59
+ cause
60
+ })
61
+ )
62
+
63
+ return Effect.sync(() => record.handler(...args)).pipe(
64
+ Effect.catchCause(onCause),
65
+ Effect.flatMap((result) =>
66
+ Effect.isEffect(result)
67
+ ? result.pipe(Effect.catchCause(onCause))
68
+ : Effect.fail(
69
+ new PluginError({
70
+ code: "hook_failed",
71
+ message: `hook "${record.hook}" in plugin "${record.plugin}" returned a non-Effect (${typeof result})`,
72
+ plugin: record.plugin,
73
+ hook: record.hook
74
+ })
75
+ )
76
+ )
77
+ )
78
+ }
79
+
80
+ /**
81
+ * The dispatch surface.
82
+ *
83
+ * @category models
84
+ * @since 1.0.0-rc.0
85
+ */
86
+ export interface Service<H = FlowsHooks> {
87
+ /** The resolved list this dispatcher walks. */
88
+ readonly resolved: Resolved<H>
89
+ /** Frozen, ordered handler records for a hook. */
90
+ readonly handlers: (hook: string) => ReadonlyArray<HandlerRecord>
91
+ /** Runs every handler in resolved order, one at a time. */
92
+ readonly sequential: <K extends KeysOfKind<H, "sequential">>(
93
+ hook: K,
94
+ ...args: ArgsOf<H[K]>
95
+ ) => Effect.Effect<ReadonlyArray<SuccessOf<H[K]>>, PluginError, ContextOf<H[K]>>
96
+ /**
97
+ * Runs every handler concurrently, ignoring results. Never fails: handler
98
+ * failures are returned so the caller can report them at its own boundary.
99
+ */
100
+ readonly parallel: <K extends KeysOfKind<H, "parallel">>(
101
+ hook: K,
102
+ ...args: ArgsOf<H[K]>
103
+ ) => Effect.Effect<ReadonlyArray<PluginError>, never, ContextOf<H[K]>>
104
+ /** Runs handlers in order until one returns `Option.some`. */
105
+ readonly first: <K extends KeysOfKind<H, "first">>(
106
+ hook: K,
107
+ ...args: ArgsOf<H[K]>
108
+ ) => Effect.Effect<SuccessOf<H[K]>, PluginError, ContextOf<H[K]>>
109
+ /** Threads a value through every handler, merging each returned patch. */
110
+ readonly waterfall: <K extends KeysOfKind<H, "waterfall">>(
111
+ hook: K,
112
+ initial: ArgsOf<H[K]>[0],
113
+ merge: (previous: ArgsOf<H[K]>[0], patch: Exclude<SuccessOf<H[K]>, void>) => ArgsOf<H[K]>[0]
114
+ ) => Effect.Effect<ArgsOf<H[K]>[0], PluginError, ContextOf<H[K]>>
115
+ }
116
+
117
+ /**
118
+ * Builds a dispatcher over an already resolved plugin list.
119
+ *
120
+ * @category constructors
121
+ * @since 1.0.0-rc.0
122
+ */
123
+ export const make = <H = FlowsHooks>(resolved: Resolved<H>): Service<H> => {
124
+ const handlers = (hook: string): ReadonlyArray<HandlerRecord> => resolved.handlers.get(hook) ?? empty
125
+
126
+ const sequential = ((hook: string, ...args: Array<unknown>) =>
127
+ Effect.gen(function*() {
128
+ const mismatch = kindMismatch(resolved, hook, "sequential")
129
+ if (mismatch !== undefined) return yield* Effect.fail(mismatch)
130
+ const results: Array<unknown> = []
131
+ for (const record of handlers(hook)) {
132
+ results.push(yield* runHandler(record, args))
133
+ }
134
+ return results
135
+ })) as Service<H>["sequential"]
136
+
137
+ const parallel = ((hook: string, ...args: Array<unknown>) => {
138
+ const mismatch = kindMismatch(resolved, hook, "parallel")
139
+ if (mismatch !== undefined) return Effect.succeed([mismatch])
140
+ return Effect.forEach(
141
+ handlers(hook),
142
+ (record) =>
143
+ runHandler(record, args).pipe(
144
+ Effect.match({ onFailure: (error) => [error], onSuccess: () => [] as Array<PluginError> })
145
+ ),
146
+ { concurrency: resolved.parallelConcurrency }
147
+ ).pipe(Effect.map((chunks) => chunks.flat()))
148
+ }) as Service<H>["parallel"]
149
+
150
+ const first = ((hook: string, ...args: Array<unknown>) =>
151
+ Effect.gen(function*() {
152
+ const mismatch = kindMismatch(resolved, hook, "first")
153
+ if (mismatch !== undefined) return yield* Effect.fail(mismatch)
154
+ for (const record of handlers(hook)) {
155
+ const result = yield* runHandler(record, args)
156
+ if (!Option.isOption(result)) {
157
+ return yield* Effect.fail(
158
+ new PluginError({
159
+ code: "invalid_hook_result",
160
+ message: `first hook "${record.hook}" in plugin "${record.plugin}" did not return an Option`,
161
+ plugin: record.plugin,
162
+ hook: record.hook
163
+ })
164
+ )
165
+ }
166
+ if (Option.isSome(result)) return result
167
+ }
168
+ return Option.none()
169
+ })) as unknown as Service<H>["first"]
170
+
171
+ const waterfall =
172
+ ((hook: string, initial: unknown, merge: (previous: unknown, patch: unknown) => unknown) =>
173
+ Effect.gen(function*() {
174
+ const mismatch = kindMismatch(resolved, hook, "waterfall")
175
+ if (mismatch !== undefined) return yield* Effect.fail(mismatch)
176
+ let value = initial
177
+ for (const record of handlers(hook)) {
178
+ const patch = yield* runHandler(record, [value])
179
+ if (patch !== undefined) {
180
+ value = yield* Effect.try({
181
+ try: () => merge(value, patch),
182
+ catch: (cause) =>
183
+ cause instanceof PluginError
184
+ ? new PluginError({
185
+ code: cause.code,
186
+ message: cause.message,
187
+ plugin: record.plugin,
188
+ hook: record.hook,
189
+ path: cause.path
190
+ })
191
+ : new PluginError({
192
+ code: "config_invalid",
193
+ message: `waterfall hook "${record.hook}" in plugin "${record.plugin}" returned an invalid patch`,
194
+ plugin: record.plugin,
195
+ hook: record.hook
196
+ })
197
+ })
198
+ }
199
+ }
200
+ return value
201
+ })) as Service<H>["waterfall"]
202
+
203
+ return Object.freeze({ resolved, handlers, sequential, parallel, first, waterfall })
204
+ }
205
+
206
+ /**
207
+ * A dispatcher with no plugins at all.
208
+ *
209
+ * @category constructors
210
+ * @since 1.0.0-rc.0
211
+ */
212
+ export const makeNoop = <H = FlowsHooks>(): Service<H> =>
213
+ make<H>(Object.freeze({
214
+ plugins: Object.freeze([]),
215
+ handlers: ImmutableMap.make<string, ReadonlyArray<HandlerRecord>>(),
216
+ kinds: engineHooks,
217
+ parallelConcurrency: defaultParallelConcurrency
218
+ }))
219
+
220
+ /**
221
+ * The shared dispatcher, as a service tag.
222
+ *
223
+ * This tag holds one dispatcher over the process-wide augmented `FlowsHooks`.
224
+ * `declare module "@smthrs/plugin"` augmentation is the supported extension
225
+ * mechanism for values placed in the tag. A host typed against a separate hook
226
+ * interface holds its `Service<H>` directly, as `@smthrs/agent` does, and does
227
+ * not use this tag or its layers.
228
+ *
229
+ * @category context
230
+ * @since 1.0.0-rc.0
231
+ */
232
+ export class Plugins extends Context.Service<Plugins, Service>()("flows/plugin/Plugins") {}
233
+
234
+ /**
235
+ * Layer providing a dispatcher over an already resolved plugin list.
236
+ *
237
+ * This layer targets the shared augmented `FlowsHooks` catalog only. A host
238
+ * with a separate hook interface holds its `Service<H>` directly.
239
+ *
240
+ * @category layers
241
+ * @since 1.0.0-rc.0
242
+ */
243
+ export const layer = (resolved: Resolved): Layer.Layer<Plugins> => Layer.succeed(Plugins)(make(resolved))
244
+
245
+ /**
246
+ * Layer providing a dispatcher with no plugins.
247
+ *
248
+ * This layer targets the shared augmented `FlowsHooks` catalog only. A host
249
+ * with a separate hook interface holds its `Service<H>` directly.
250
+ *
251
+ * @category layers
252
+ * @since 1.0.0-rc.0
253
+ */
254
+ export const layerNoop: Layer.Layer<Plugins> = Layer.succeed(Plugins)(makeNoop())