-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathexecutor.ts
More file actions
203 lines (185 loc) · 7.49 KB
/
Copy pathexecutor.ts
File metadata and controls
203 lines (185 loc) · 7.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
* The kernel side of one call: a `SpellCall` in, a `SpellReply` out (#7617 R2.2).
*
* Every way a call can go wrong is a reply, so the executor's own error channel is `never` and a
* caller has one thing to read. The one exception is a spell whose value its own `result` schema
* refuses: that is the spell author's bug, not the caller's, so it dies.
*
* `AnySpell` erases each spell's requirements, so nothing checks that the runtime carries what a
* registered spell needs — the composition root that builds the registry owes those services, and
* the single conversion in `runSpell` is where that obligation is spent. `src/boot.ts` is that
* root and discharges it there: `WindowIndex` and `shellDispatchKernel` are built into the same
* `Kernel` context a caller runs a spell under, and `Kernel` names both, so a dropped provider is
* a compile error at `start` rather than a defect at the first call (#7774).
*/
import {Context, Effect, Layer, Predicate, Schema} from "effect";
import {firstSchemaIssue} from "../protocol/issue.ts";
import {
PROTOCOL_VERSION,
type SpellCall,
type SpellFailure,
type SpellReply,
SpellReplyError,
SpellReplyOk,
} from "../protocol/messages.ts";
import {BadArgs, BadResult, SpellFailed, UnknownSpell} from "./errors.ts";
import {SpellRegistry, type SpellRow} from "./registry.ts";
import {type Client, resolveScope, WindowIndex} from "./scope.ts";
import {renderPath, type Scope, type SpellPath} from "./spell.ts";
/** Levenshtein distance, the measure behind the did-you-mean on an unknown path. */
const distance = (left: string, right: string): number => {
let previous = Array.from({length: right.length + 1}, (_, index) => index);
for (let i = 1; i <= left.length; i++) {
const current = [i];
for (let j = 1; j <= right.length; j++) {
const substitute = (previous[j - 1] ?? 0) + (left[i - 1] === right[j - 1] ? 0 : 1);
current.push(Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, substitute));
}
previous = current;
}
return previous[right.length] ?? 0;
};
/**
* The nearest registered path, when one is near enough to be a typo rather than a different spell.
* A third of the candidate's length is the budget, so `window.split` tolerates four edits and a
* short path tolerates one.
*/
const nearestPath = (target: string, rows: ReadonlyArray<SpellRow>): string | undefined => {
let best: {readonly path: string; readonly gap: number} | undefined;
for (const row of rows) {
const path = renderPath(row.path);
const gap = distance(target, path);
if (gap > Math.max(1, Math.ceil(path.length / 3))) continue;
if (best === undefined || gap < best.gap) best = {path, gap};
}
return best?.path;
};
/** An error that names itself: a `_tag` to discriminate on, and whatever it renders as. */
type NamedError = {readonly _tag: string; readonly message?: unknown};
const isNamed = (value: unknown): value is NamedError =>
Predicate.isObject(value) && typeof value._tag === "string";
/**
* The caught value as an error that names itself. One already carrying a `_tag` is its own;
* anything else becomes `SpellFailed`, a class `errors.ts` declares, so the only source of a
* reply's tag is an error object and no reply can carry a tag naming nothing in the tree.
*/
const named = (call: SpellCall, error: unknown): NamedError =>
isNamed(error) ? error : new SpellFailed({path: renderPath(call.path), original: error});
const messageOf = (error: NamedError): string =>
typeof error.message === "string" && error.message.length > 0
? error.message
: `the call failed with ${error._tag}`;
/**
* A failure as the page reads it. The spell path is always the call's own, so a spell's private
* error cannot claim a different one; only the executor's own errors add `expected`/`didYouMean`.
*/
const failureOf = (call: SpellCall, error: NamedError): SpellFailure => {
const base = {tag: error._tag, message: messageOf(error), path: call.path};
if (error instanceof UnknownSpell && error.didYouMean !== undefined) {
return {...base, didYouMean: error.didYouMean};
}
if (error instanceof BadArgs) return {...base, expected: error.expected};
return base;
};
const succeeded = (call: SpellCall, result: unknown): SpellReply =>
new SpellReplyOk({
type: "spell.reply",
version: PROTOCOL_VERSION,
id: call.id,
ok: true,
result,
});
const refused = (call: SpellCall, error: SpellFailure): SpellReply =>
new SpellReplyError({
type: "spell.reply",
version: PROTOCOL_VERSION,
id: call.id,
ok: false,
error,
});
const runSpell = (row: SpellRow, args: unknown, scope: Scope): Effect.Effect<unknown, unknown> =>
// The registry stores `AnySpell`, so this call's types — its requirements included — are `any`
// here and the checker can prove nothing about them. See the module note.
row.spell.execute(args, scope) as Effect.Effect<unknown, unknown>;
const make = Effect.fn("Tuval.SpellExecutor.make")(function* () {
const registry = yield* SpellRegistry;
const index = yield* WindowIndex;
const lookup = (path: SpellPath) =>
registry.lookup(path).pipe(
Effect.catch((miss) =>
Effect.flatMap(registry.list, (rows) => {
const didYouMean = nearestPath(miss.path, rows);
return Effect.fail(
new UnknownSpell({
path: miss.path,
...(didYouMean === undefined ? {} : {didYouMean}),
}),
);
}),
),
);
// A row's schemas are `any` too, so their decoding/encoding services are erased with the rest of
// the spell; these two conversions carry the same obligation `runSpell`'s does.
const decodeArgs = (row: SpellRow, call: SpellCall) =>
(
Schema.decodeUnknownEffect(row.spell.params)(call.args) as Effect.Effect<
unknown,
Schema.SchemaError
>
).pipe(
Effect.mapError((error) => {
const {expected, at} = firstSchemaIssue(error);
return new BadArgs({path: renderPath(row.path), argument: at, expected});
}),
);
const encodeResult = (row: SpellRow, value: unknown) =>
(
Schema.encodeUnknownEffect(row.spell.result)(value) as Effect.Effect<
unknown,
Schema.SchemaError
>
).pipe(
Effect.mapError(
(error) =>
new BadResult({path: renderPath(row.path), reason: firstSchemaIssue(error).expected}),
),
Effect.orDie,
);
const execute = Effect.fn("Tuval.SpellExecutor.execute")(function* (
call: SpellCall,
client: Client,
) {
const attempt = Effect.gen(function* () {
const row = yield* lookup(call.path);
const args = yield* decodeArgs(row, call);
const scope = yield* resolveScope(call, client);
const value = yield* runSpell(row, args, scope);
return succeeded(call, yield* encodeResult(row, value));
});
return yield* attempt.pipe(
Effect.provideService(WindowIndex, index),
Effect.catch((error) => {
const failure = named(call, error);
const reply = refused(call, failureOf(call, failure));
// The reply carries only what the wire can hold, so the value itself is logged here
// or it is readable nowhere once this frame goes.
return failure instanceof SpellFailed
? Effect.logError("spell executor: a spell failed with an untagged value", {
path: failure.path,
original: failure.original,
}).pipe(Effect.as(reply))
: Effect.succeed(reply);
}),
);
});
return SpellExecutor.of({execute});
});
export class SpellExecutor extends Context.Service<
SpellExecutor,
{
readonly execute: (call: SpellCall, client: Client) => Effect.Effect<SpellReply>;
}
>()("tuval/SpellExecutor") {
static readonly layer: Layer.Layer<SpellExecutor, never, SpellRegistry | WindowIndex> =
Layer.effect(SpellExecutor, make());
}