@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.
- 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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inert JSON admission for plugin configuration and cache identity.
|
|
3
|
+
*
|
|
4
|
+
* Reflection is descriptor-only: accessors never execute, hostile proxies are
|
|
5
|
+
* refused, and accepted values are detached before they are frozen.
|
|
6
|
+
*
|
|
7
|
+
* @private
|
|
8
|
+
* @since 1.0.0-rc.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as BoundedJson from "@smthrs/canonical/BoundedJson"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Strict JSON value accepted by the boundary.
|
|
15
|
+
*
|
|
16
|
+
* @private
|
|
17
|
+
* @since 1.0.0-rc.0
|
|
18
|
+
*/
|
|
19
|
+
export type Json = BoundedJson.Json
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resource bounds for one admitted tree.
|
|
23
|
+
*
|
|
24
|
+
* @private
|
|
25
|
+
* @since 1.0.0-rc.0
|
|
26
|
+
*/
|
|
27
|
+
export type Limits = BoundedJson.StrictLimits
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Successful admission.
|
|
31
|
+
*
|
|
32
|
+
* @private
|
|
33
|
+
* @since 1.0.0-rc.0
|
|
34
|
+
*/
|
|
35
|
+
export interface AdmissionSuccess {
|
|
36
|
+
readonly ok: true
|
|
37
|
+
readonly value: Json
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Refusal with a stable value path.
|
|
42
|
+
*
|
|
43
|
+
* @private
|
|
44
|
+
* @since 1.0.0-rc.0
|
|
45
|
+
*/
|
|
46
|
+
export interface AdmissionFailure {
|
|
47
|
+
readonly ok: false
|
|
48
|
+
readonly path: string
|
|
49
|
+
readonly complaint: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Result of inert JSON admission.
|
|
54
|
+
*
|
|
55
|
+
* @private
|
|
56
|
+
* @since 1.0.0-rc.0
|
|
57
|
+
*/
|
|
58
|
+
export type Admission = AdmissionSuccess | AdmissionFailure
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default configuration bounds.
|
|
62
|
+
*
|
|
63
|
+
* @private
|
|
64
|
+
* @since 1.0.0-rc.0
|
|
65
|
+
*/
|
|
66
|
+
export const defaultLimits: Limits = Object.freeze({
|
|
67
|
+
maxBytes: 1024 * 1024,
|
|
68
|
+
maxDepth: 64,
|
|
69
|
+
maxMembers: 4_096,
|
|
70
|
+
maxNodes: 8_192,
|
|
71
|
+
maxStringBytes: 64 * 1024,
|
|
72
|
+
maxKeyBytes: 1_024
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Tests whether a string contains paired UTF-16 surrogate units.
|
|
77
|
+
*
|
|
78
|
+
* @private
|
|
79
|
+
* @since 1.0.0-rc.0
|
|
80
|
+
*/
|
|
81
|
+
export const isWellFormedText = (value: string): boolean => value.isWellFormed()
|
|
82
|
+
|
|
83
|
+
const childPath = (path: string, key: string): string =>
|
|
84
|
+
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`
|
|
85
|
+
|
|
86
|
+
const encodedStringBytes = (value: string): number => BoundedJson.encodedStringBytes(value)!
|
|
87
|
+
|
|
88
|
+
// Paths contain only well-formed text and JSON-escaped keys. Count their JSON
|
|
89
|
+
// bytes without encoding the whole path, reserving quotes and a truncation marker.
|
|
90
|
+
const boundedPath = (path: string): string => {
|
|
91
|
+
let bytes = 2 + 3
|
|
92
|
+
let prefix = ""
|
|
93
|
+
for (const character of path) {
|
|
94
|
+
const point = character.codePointAt(0)!
|
|
95
|
+
bytes += character === "\"" || character === "\\"
|
|
96
|
+
? 2
|
|
97
|
+
: point <= 0x7f
|
|
98
|
+
? 1
|
|
99
|
+
: point <= 0x7ff
|
|
100
|
+
? 2
|
|
101
|
+
: point <= 0xffff
|
|
102
|
+
? 3
|
|
103
|
+
: 4
|
|
104
|
+
if (bytes > 192) return `${prefix}...`
|
|
105
|
+
prefix += character
|
|
106
|
+
}
|
|
107
|
+
return path
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface Totals {
|
|
111
|
+
bytes: number
|
|
112
|
+
members: number
|
|
113
|
+
nodes: number
|
|
114
|
+
depth: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Entry {
|
|
118
|
+
readonly keyBytes: number
|
|
119
|
+
readonly totals: Totals
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface Snapshot {
|
|
123
|
+
readonly totals: Totals
|
|
124
|
+
readonly entries: Map<string, Entry>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Only detached containers constructed here enter this map. Mutable caller
|
|
128
|
+
// objects and Object.freeze alone never establish admission.
|
|
129
|
+
const snapshots = new WeakMap<object, Snapshot>()
|
|
130
|
+
|
|
131
|
+
const scalarTotals = (bytes: number): Totals => ({ bytes, members: 0, nodes: 1, depth: 0 })
|
|
132
|
+
|
|
133
|
+
const total = (entries: ReadonlyMap<string, Entry>): Totals => {
|
|
134
|
+
const totals: Totals = { bytes: 2 + Math.max(0, entries.size - 1), members: entries.size, nodes: 1, depth: 1 }
|
|
135
|
+
for (const entry of entries.values()) {
|
|
136
|
+
totals.bytes += entry.keyBytes + entry.totals.bytes
|
|
137
|
+
totals.members += entry.totals.members
|
|
138
|
+
totals.nodes += entry.totals.nodes
|
|
139
|
+
totals.depth = Math.max(totals.depth, 1 + entry.totals.depth)
|
|
140
|
+
}
|
|
141
|
+
return totals
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const remember = (value: ReadonlyArray<Json> | { readonly [key: string]: Json }): void => {
|
|
145
|
+
const entries = new Map<string, Entry>()
|
|
146
|
+
for (const [key, child] of Object.entries(value)) {
|
|
147
|
+
entries.set(key, {
|
|
148
|
+
keyBytes: Array.isArray(value) ? 0 : encodedStringBytes(key) + 1,
|
|
149
|
+
totals: typeof child === "object" && child !== null ?
|
|
150
|
+
snapshots.get(child)!.totals
|
|
151
|
+
: scalarTotals(typeof child === "string" ? encodedStringBytes(child) : String(child).length)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
snapshots.set(value, { totals: total(entries), entries })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Copies one strict JSON tree under explicit resource bounds.
|
|
159
|
+
*
|
|
160
|
+
* @private
|
|
161
|
+
* @since 1.0.0-rc.0
|
|
162
|
+
*/
|
|
163
|
+
export const admit = (input: unknown, limits: Limits = defaultLimits): Admission => {
|
|
164
|
+
const result = BoundedJson.admitStrict(input, limits, {
|
|
165
|
+
ordinaryRecords: true,
|
|
166
|
+
boundedText: true,
|
|
167
|
+
onContainer: remember
|
|
168
|
+
})
|
|
169
|
+
return result.ok ? result : {
|
|
170
|
+
...result,
|
|
171
|
+
path: boundedPath(result.path),
|
|
172
|
+
complaint: result.complaint === "must be dense and have no extra properties"
|
|
173
|
+
? "must be a dense array with no extra properties" :
|
|
174
|
+
result.complaint
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Admits only a JSON record at the root.
|
|
180
|
+
*
|
|
181
|
+
* @private
|
|
182
|
+
* @since 1.0.0-rc.0
|
|
183
|
+
*/
|
|
184
|
+
export const record = (
|
|
185
|
+
input: unknown,
|
|
186
|
+
limits: Limits = defaultLimits
|
|
187
|
+
): AdmissionSuccess | AdmissionFailure => {
|
|
188
|
+
const admitted = admit(input, limits)
|
|
189
|
+
return admitted.ok && (admitted.value === null || Array.isArray(admitted.value) || typeof admitted.value !== "object")
|
|
190
|
+
? { ok: false, path: "$", complaint: "must be a JSON record" }
|
|
191
|
+
: admitted
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Merges two independently admitted records, retaining unchanged subtrees.
|
|
196
|
+
* Both operands must come from successful admission, and the patch must be a
|
|
197
|
+
* fresh copy: their trees must be disjoint. Each output branch then comes from
|
|
198
|
+
* at most one location in either tree, preserving the no-repeated-reference
|
|
199
|
+
* invariant without walking retained subtrees. Limits apply to the result.
|
|
200
|
+
*
|
|
201
|
+
* @private
|
|
202
|
+
* @since 1.0.0-rc.0
|
|
203
|
+
*/
|
|
204
|
+
export const mergeRecords = (
|
|
205
|
+
base: { readonly [key: string]: Json },
|
|
206
|
+
patch: { readonly [key: string]: Json },
|
|
207
|
+
limits: Limits = defaultLimits
|
|
208
|
+
): Admission => {
|
|
209
|
+
const merge = (
|
|
210
|
+
left: { readonly [key: string]: Json },
|
|
211
|
+
right: { readonly [key: string]: Json },
|
|
212
|
+
path: string
|
|
213
|
+
): Admission => {
|
|
214
|
+
const entries = new Map(snapshots.get(left)!.entries)
|
|
215
|
+
const result = { ...left }
|
|
216
|
+
for (const [key, entry] of snapshots.get(right)!.entries) {
|
|
217
|
+
const previous = left[key]
|
|
218
|
+
const next = right[key]!
|
|
219
|
+
if (
|
|
220
|
+
typeof previous === "object" && previous !== null && !Array.isArray(previous) &&
|
|
221
|
+
typeof next === "object" && next !== null && !Array.isArray(next)
|
|
222
|
+
) {
|
|
223
|
+
const merged = merge(
|
|
224
|
+
previous as { readonly [key: string]: Json },
|
|
225
|
+
next as { readonly [key: string]: Json },
|
|
226
|
+
childPath(path, key)
|
|
227
|
+
)
|
|
228
|
+
if (!merged.ok) return merged
|
|
229
|
+
result[key] = merged.value
|
|
230
|
+
entries.set(key, { keyBytes: entry.keyBytes, totals: snapshots.get(merged.value as object)!.totals })
|
|
231
|
+
} else {
|
|
232
|
+
result[key] = next
|
|
233
|
+
entries.set(key, entry)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const totals = total(entries)
|
|
237
|
+
const complaint = totals.members > limits.maxMembers ?
|
|
238
|
+
`exceeds the ${limits.maxMembers}-member limit`
|
|
239
|
+
: totals.nodes > limits.maxNodes ?
|
|
240
|
+
`exceeds the ${limits.maxNodes}-node limit`
|
|
241
|
+
: totals.bytes > limits.maxBytes ?
|
|
242
|
+
`exceeds the ${limits.maxBytes}-byte limit`
|
|
243
|
+
: totals.depth > limits.maxDepth ?
|
|
244
|
+
`exceeds the depth limit of ${limits.maxDepth}`
|
|
245
|
+
: undefined
|
|
246
|
+
if (complaint !== undefined) return { ok: false, path: boundedPath(path), complaint }
|
|
247
|
+
Object.freeze(result)
|
|
248
|
+
snapshots.set(result, { totals, entries })
|
|
249
|
+
return { ok: true, value: result }
|
|
250
|
+
}
|
|
251
|
+
return merge(base, patch, "$")
|
|
252
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect layer composition for a resolved plugin list.
|
|
3
|
+
*
|
|
4
|
+
* @private
|
|
5
|
+
* @since 1.0.0-rc.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Action } from "@smthrs/flow"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Layer from "effect/Layer"
|
|
11
|
+
import type { FlowsPlugin } from "../Plugin.ts"
|
|
12
|
+
import { PluginError } from "../PluginError.ts"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Merges plugin layers left-to-right, wrapping each build failure as
|
|
16
|
+
* `layer_failed`, then seals the cache environment: it is supplied beneath
|
|
17
|
+
* the plugin layers and overrides anything they output, and an absent
|
|
18
|
+
* declaration stays absent.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
* @since 1.0.0-rc.0
|
|
22
|
+
*/
|
|
23
|
+
export const mergePluginLayers = <H>(
|
|
24
|
+
plugins: ReadonlyArray<FlowsPlugin<H>>,
|
|
25
|
+
cacheEnvironment: Action.CacheEnvironment | undefined
|
|
26
|
+
): Layer.Layer<any, PluginError, any> => {
|
|
27
|
+
const layers = plugins.flatMap((plugin) =>
|
|
28
|
+
plugin.layer
|
|
29
|
+
? [
|
|
30
|
+
Layer.catchCause(plugin.layer, (cause) =>
|
|
31
|
+
Layer.effectDiscard(
|
|
32
|
+
Effect.fail(
|
|
33
|
+
new PluginError({
|
|
34
|
+
code: "layer_failed",
|
|
35
|
+
message: `plugin "${plugin.name}" failed to build its layer`,
|
|
36
|
+
plugin: plugin.name,
|
|
37
|
+
cause
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
)) as Layer.Layer<any, PluginError, any>
|
|
41
|
+
]
|
|
42
|
+
: []
|
|
43
|
+
)
|
|
44
|
+
const merged = (layers.length === 0
|
|
45
|
+
? Layer.empty
|
|
46
|
+
: layers.reduce((accumulated, next) => Layer.provideMerge(next, accumulated))) as Layer.Layer<any, PluginError, any>
|
|
47
|
+
// The sealed environment is provided beneath the plugin layers so they can
|
|
48
|
+
// read it, then merged last so it overrides any plugin output. An absent
|
|
49
|
+
// declaration is sealed as `undefined`, so no plugin layer can declare or
|
|
50
|
+
// replace the composition's cache identity.
|
|
51
|
+
const environment = Layer.succeed(Action.CurrentCacheEnvironment)(cacheEnvironment) as unknown as Layer.Layer<
|
|
52
|
+
any,
|
|
53
|
+
PluginError,
|
|
54
|
+
any
|
|
55
|
+
>
|
|
56
|
+
return Layer.merge(Layer.provideMerge(merged, environment), environment)
|
|
57
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache identity admission for a resolved plugin list.
|
|
3
|
+
*
|
|
4
|
+
* @private
|
|
5
|
+
* @since 1.0.0-rc.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Action } from "@smthrs/flow"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Schema from "effect/Schema"
|
|
11
|
+
import type { FlowsPlugin } from "../Plugin.ts"
|
|
12
|
+
import { PluginError } from "../PluginError.ts"
|
|
13
|
+
import * as Boundary from "./Boundary.ts"
|
|
14
|
+
|
|
15
|
+
const escapePluginIdentityPart = (value: string): string =>
|
|
16
|
+
// Percent must be escaped first so literal escape-looking text remains distinct from the `@` escape added next.
|
|
17
|
+
value.replaceAll("%", "%25").replaceAll("@", "%40")
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Admits a caller cache environment and prepends one escaped `name@version`
|
|
21
|
+
* identity per plugin, in resolved order. An absent input stays absent.
|
|
22
|
+
*
|
|
23
|
+
* @private
|
|
24
|
+
* @since 1.0.0-rc.0
|
|
25
|
+
*/
|
|
26
|
+
export const snapshotCacheEnvironment = <H>(
|
|
27
|
+
input: unknown,
|
|
28
|
+
plugins: ReadonlyArray<FlowsPlugin<H>>
|
|
29
|
+
): Effect.Effect<Action.CacheEnvironment | undefined, PluginError> => {
|
|
30
|
+
if (input === undefined) return Effect.succeed(undefined)
|
|
31
|
+
const pluginIdentities: Array<string> = []
|
|
32
|
+
for (const plugin of plugins) {
|
|
33
|
+
if (plugin.version === undefined) {
|
|
34
|
+
return Effect.fail(
|
|
35
|
+
new PluginError({
|
|
36
|
+
code: "cache_environment_invalid",
|
|
37
|
+
message: `cache environment requires a version for plugin "${plugin.name}"`,
|
|
38
|
+
path: "$.version",
|
|
39
|
+
plugin: plugin.name
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
pluginIdentities.push(`${escapePluginIdentityPart(plugin.name)}@${escapePluginIdentityPart(plugin.version)}`)
|
|
44
|
+
}
|
|
45
|
+
const admitted = Boundary.record(input)
|
|
46
|
+
if (!admitted.ok) {
|
|
47
|
+
return Effect.fail(
|
|
48
|
+
new PluginError({
|
|
49
|
+
code: "cache_environment_invalid",
|
|
50
|
+
message: `cache environment ${admitted.complaint}`,
|
|
51
|
+
path: `$options.cacheEnvironment${admitted.path.slice(1)}`
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return Schema.decodeUnknownEffect(Action.CacheEnvironment)(admitted.value).pipe(
|
|
56
|
+
Effect.mapError(() =>
|
|
57
|
+
new PluginError({
|
|
58
|
+
code: "cache_environment_invalid",
|
|
59
|
+
message: "cache environment does not match the complete cache identity schema",
|
|
60
|
+
path: "$options.cacheEnvironment"
|
|
61
|
+
})
|
|
62
|
+
),
|
|
63
|
+
Effect.map((environment) => {
|
|
64
|
+
const capabilities: Record<string, ReadonlyArray<string>> = {}
|
|
65
|
+
for (const name of Object.keys(environment.capabilities)) {
|
|
66
|
+
Object.defineProperty(capabilities, name, {
|
|
67
|
+
value: Object.freeze([...environment.capabilities[name]!]),
|
|
68
|
+
enumerable: true
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
layers: Object.freeze([...pluginIdentities, ...environment.layers]),
|
|
73
|
+
capabilities: Object.freeze(capabilities)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
}
|