Skip to content

Commit 14e9ea6

Browse files
committed
⚡️ statically compute api tree at decorate time (#1183)
The computational complexity of reifying an api handle was being deferred until the moment the api was being called, and then every invocation paid the cost of calculating the _entire_ middleware stack from the root all they way back to the decorate scope. The handle was cached for subsequent invocations, but any new `around()` invalidated that scope and every descendant, which meant that every single one had to walk the entire scope chain on the next invocation. The handle is now materialized at decoration time. Each api context carries `local` (this scope's contributions), `total` (the inherited stack), and `handle` (the assembled api). `around()` appends the new decorator onto `local` and then walks the subtree in `install()`, rebuilding each descendant's handle in place. Critically, the inherited middleware stack is cached at every scope that has middleware, so any subsequent calls to around() can begin from that total, and do not need to walk back up the entire scope chain. In all cases, because the api handle is computed eagerly, `invoke()` is single context lookup and dispatch.
1 parent e10cd3a commit 14e9ea6

3 files changed

Lines changed: 325 additions & 98 deletions

File tree

lib/api-internal.ts

Lines changed: 102 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ import type {
99
Operation,
1010
Scope,
1111
} from "./types.ts";
12-
import type { ScopeInternal } from "./scope-internal.ts";
1312
import { Children } from "./contexts.ts";
1413
import { Ok } from "./result.ts";
1514

1615
export interface ApiInternal<A> extends Api<A> {
1716
context: Context<{
18-
max: Partial<Around<A>>[];
19-
min: Partial<Around<A>>[];
17+
local: Decorator<A>;
18+
total: Decorator<A>;
19+
handle: A;
2020
}>;
2121
core: A;
22-
cache: WeakMap<Scope, A>;
23-
invalidate(scope: Scope): void;
2422
}
2523

2624
export function createApiInternal<A extends {}>(
@@ -31,27 +29,13 @@ export function createApiInternal<A extends {}>(
3129

3230
let context = createContext(`api::${name}`) as ApiInternal<A>["context"];
3331

34-
let cache = new WeakMap<Scope, A>();
35-
3632
let api: ApiInternal<A> = {
3733
core,
3834
context,
39-
cache,
40-
invalidate(scope: Scope) {
41-
cache.delete(scope);
42-
let children = scope.get(Children);
43-
if (children) {
44-
for (let child of children) {
45-
api.invalidate(child);
46-
}
47-
}
48-
},
35+
4936
invoke: (scope, key, args) => {
50-
let handle = cache.get(scope);
51-
if (!handle) {
52-
handle = createHandle(api, scope as ScopeInternal);
53-
cache.set(scope, handle);
54-
}
37+
let handle = scope.get(api.context)?.handle ?? core;
38+
5539
let member = handle[key];
5640
if (typeof member === "function") {
5741
return member(...args);
@@ -62,7 +46,7 @@ export function createApiInternal<A extends {}>(
6246
around: (decorator, options = { at: "max" }) => ({
6347
*[Symbol.iterator]() {
6448
let scope = (yield GetScope) as Scope;
65-
scope.around(api, decorator, options);
49+
decorateApi(scope, api, decorator, options);
6650
},
6751
}),
6852
operations: fields.reduce((sum, field) => {
@@ -93,50 +77,107 @@ export function createApiInternal<A extends {}>(
9377
return api;
9478
}
9579

96-
function createHandle<A extends {}>(
80+
export function decorateApi<A>(
81+
scope: Scope,
9782
api: ApiInternal<A>,
98-
scope: ScopeInternal,
99-
): A {
100-
// there is no middleware at all for this api, so the handle _is_ the core.
101-
if (!scope.get(api.context)) {
102-
return api.core;
83+
decorator: Partial<Around<A>>,
84+
options?: {
85+
at: "min" | "max";
86+
},
87+
) {
88+
// read existing total and local
89+
let current = scope.get(api.context) ?? { total: {}, local: {} };
90+
91+
let local = decorate(current.local, {
92+
[options?.at ?? "max"]: decorator,
93+
});
94+
95+
if (!scope.hasOwn(api.context)) {
96+
scope.set(api.context, {
97+
local,
98+
total: current.total,
99+
handle: api.core,
100+
});
101+
} else {
102+
current.local = local;
103103
}
104104

105-
let handle = Object.create(api.core);
105+
install(scope, api, current.total);
106+
}
106107

107-
for (let key of Object.keys(api.core) as Array<keyof A>) {
108-
let { min, max } = scope.reduce(api.context, (sum, current) => {
109-
let min = current.min.flatMap((around) =>
110-
around[key] ? [around[key]] : []
111-
);
112-
let max = current.max.flatMap((around) =>
113-
around[key] ? [around[key]] : []
114-
);
108+
function decorate<A>(base: Decorator<A>, next: Decorator<A>): Decorator<A> {
109+
return {
110+
max: base.max ? next.max ? append(base.max, next.max) : base.max : next.max,
111+
min: next.min ? base.min ? append(next.min, base.min) : next.min : base.min,
112+
};
113+
}
115114

116-
sum.min.push(...min);
117-
sum.max.unshift(...max);
115+
function append<A>(
116+
outer: Partial<Around<A>>,
117+
inner: Partial<Around<A>>,
118+
): Partial<Around<A>> {
119+
let result: Partial<Around<A>> = { ...outer };
120+
for (let key of Object.keys(inner) as (keyof A)[]) {
121+
let current = outer[key];
122+
let decoration = inner[key];
123+
if (!current) {
124+
result[key] = decoration;
125+
} else {
126+
let pair = [current, decoration] as Middleware<unknown[], unknown>[];
127+
result[key] = combine(pair) as Around<A>[keyof A];
128+
}
129+
}
130+
return result;
131+
}
118132

119-
return sum;
120-
}, {
121-
min: [] as Around<A>[typeof key][],
122-
max: [] as Around<A>[typeof key][],
123-
});
133+
function install<A>(
134+
scope: Scope,
135+
api: ApiInternal<A>,
136+
total: Decorator<A>,
137+
): void {
138+
if (scope.hasOwn(api.context)) {
139+
let context = scope.expect(api.context);
140+
141+
context.total = total;
142+
143+
total = decorate(context.total, context.local);
144+
145+
context.handle = createApiHandle(total, api.core);
146+
}
124147

125-
let stack = combine(max.concat(min) as Middleware<unknown[], unknown>[]);
148+
for (let child of scope.expect(Children)) {
149+
install(child, api, total);
150+
}
151+
}
126152

127-
if (typeof api.core[key] === "function") {
128-
handle[key] = (...args: unknown[]) =>
129-
stack(args, api.core[key] as (...args: unknown[]) => unknown);
153+
function createApiHandle<A>(decoration: Decorator<A>, core: A): A {
154+
let around = decoration.max
155+
? decoration.min ? append(decoration.max, decoration.min) : decoration.max
156+
: decoration.min;
157+
if (!around) {
158+
return core;
159+
}
160+
let handle: A = {} as A;
161+
for (let key of Object.keys(core as object) as Array<keyof A>) {
162+
let middleware = around[key] as Middleware<unknown[], unknown> | undefined;
163+
if (middleware) {
164+
if (typeof core[key] === "function") {
165+
handle[key] = ((...args: unknown[]) =>
166+
middleware(args, core[key] as (...args: unknown[]) => unknown)) as A[
167+
keyof A
168+
];
169+
} else {
170+
Object.defineProperty(handle, key, {
171+
enumerable: true,
172+
get() {
173+
return middleware([], () => core[key]);
174+
},
175+
});
176+
}
130177
} else {
131-
Object.defineProperty(handle, key, {
132-
enumerable: true,
133-
get() {
134-
return stack([], () => api.core[key]);
135-
},
136-
});
178+
handle[key] = core[key];
137179
}
138180
}
139-
140181
return handle;
141182
}
142183

@@ -165,6 +206,11 @@ function combine<TArgs extends unknown[], TReturn>(
165206
);
166207
}
167208

209+
interface Decorator<A> {
210+
min?: Partial<Around<A>>;
211+
max?: Partial<Around<A>>;
212+
}
213+
168214
const GetScope: Effect<Scope> = {
169215
description: "Fast, non-typesafe lookup of co-routine scope",
170216
enter: (resolve, routine) => {

lib/scope-internal.ts

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ApiInternal } from "./api-internal.ts";
1+
import { type ApiInternal, decorateApi } from "./api-internal.ts";
22
import { api as effection } from "./api.ts";
33
import { Children, Priority } from "./contexts.ts";
44
import { createFuture } from "./future.ts";
@@ -71,51 +71,17 @@ export function buildScopeInternal(
7171
},
7272
};
7373
},
74-
7574
around<A extends {}>(
7675
api: ApiInternal<A>,
7776
...params: Parameters<ApiInternal<A>["around"]>
7877
) {
79-
let [around, options] = params;
80-
if (!scope.hasOwn(api.context)) {
81-
scope.set(api.context, { min: [], max: [] });
82-
}
83-
84-
let { min, max } = scope.expect(api.context);
85-
86-
if (options?.at === "min") {
87-
min.push(around);
88-
} else {
89-
max.push(around);
90-
}
91-
92-
api.invalidate(scope);
78+
decorateApi(scope, api, ...params);
9379
},
9480

9581
ensure(op: () => Operation<void>): () => void {
9682
destructors.add(op);
9783
return () => destructors.delete(op);
9884
},
99-
100-
reduce<T, S>(
101-
context: Context<T>,
102-
fn: (sum: S, item: T) => S,
103-
initial: S,
104-
): S {
105-
let sum = initial;
106-
let current = contexts;
107-
while (current) {
108-
if (Object.hasOwn(current, context.name)) {
109-
let item = current[context.name] as T;
110-
if (item) {
111-
sum = fn(sum, item);
112-
}
113-
}
114-
115-
current = Object.getPrototypeOf(current);
116-
}
117-
return sum;
118-
},
11985
});
12086

12187
scope.set(Priority, scope.expect(Priority) + 1);

0 commit comments

Comments
 (0)