Skip to content

Commit 79a0eb7

Browse files
committed
fix(experimental): keep event next() chain attached and re-validate patches
1 parent 0f8006c commit 79a0eb7

2 files changed

Lines changed: 52 additions & 6 deletions

File tree

packages/experimental/src/event.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,36 @@ describe("v.event", () => {
7171
expect(result).toEqual({ v: 1 });
7272
expect(order).toEqual(["outer", "veto"]);
7373
});
74+
75+
it("keeps the chain when next is called without awaiting", async () => {
76+
const bus = v.event("evt_detach", {
77+
x: v.object({ v: v.number() }),
78+
});
79+
const order: string[] = [];
80+
bus.subscribe((_e, next) => {
81+
order.push("outer");
82+
// Fire-and-forget: publish must still wait for downstream.
83+
void next();
84+
});
85+
bus.subscribe(async (_e, next) => {
86+
await new Promise((r) => setTimeout(r, 5));
87+
order.push("inner");
88+
await next({ v: 2 });
89+
});
90+
const result = await bus.publish("x", { v: 1 });
91+
expect(result).toEqual({ v: 2 });
92+
expect(order).toEqual(["outer", "inner"]);
93+
});
94+
95+
it("re-validates next() patches against the kind schema", async () => {
96+
const bus = v.event("evt_patch_validate", {
97+
x: v.object({ v: v.number() }),
98+
});
99+
bus.subscribe(async (_e, next) => {
100+
await next({ v: "nope" } as never);
101+
});
102+
await expect(bus.publish("x", { v: 1 })).rejects.toThrow(/expected number/);
103+
});
74104
});
75105

76106
describe("event extension + modules", () => {

packages/experimental/src/event.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,35 +149,51 @@ const thenMaybe = <T, R>(
149149
/**
150150
* Run handlers outermost-first (mount / subscribe order). Each may call
151151
* `next(mutate?)` to continue and patch the payload; skipping `next` vetoes.
152+
* Patches re-validate against the kind schema. Calling `next` without
153+
* awaiting it still keeps the downstream chain attached to `publish`.
152154
*/
153155
const runHandlers = (
154156
handlers: readonly EventHandler<any>[],
155157
type: string,
156158
initial: unknown,
159+
schema: unknown,
160+
path: string,
157161
): unknown | Promise<unknown> => {
158162
let current = initial;
159163
const run = (i: number): void | Promise<void> => {
160164
if (i >= handlers.length) return;
161-
let proceeded = false;
165+
let called = false;
166+
let downstream: void | Promise<void>;
162167
const next = (mutate?: Partial<unknown>) => {
163-
proceeded = true;
168+
called = true;
169+
const continueChain = (value: unknown) => {
170+
current = value;
171+
return run(i + 1);
172+
};
164173
if (
165174
mutate !== undefined &&
166175
mutate !== null &&
167176
typeof mutate === "object"
168177
) {
169-
current = {
178+
const merged = {
170179
...(current as Record<string, unknown>),
171180
...(mutate as Record<string, unknown>),
172181
};
182+
downstream = thenMaybe(
183+
validate(asType(schema), merged, path),
184+
continueChain,
185+
);
186+
} else {
187+
downstream = continueChain(current);
173188
}
174-
return run(i + 1);
189+
return downstream;
175190
};
176191
const handler = handlers[i];
177192
if (!handler) return;
178193
const result = handler({ type, data: current }, next);
179194
return thenMaybe(result, () => {
180-
if (!proceeded) return;
195+
if (!called) return;
196+
return downstream;
181197
});
182198
};
183199
return thenMaybe(run(0), () => current);
@@ -196,7 +212,7 @@ const publishOn = (
196212
}
197213
const handlers = [...bus.mounted, ...bus.direct];
198214
return thenMaybe(validate(asType(schema), data, path), (parsed) =>
199-
runHandlers(handlers, type, parsed),
215+
runHandlers(handlers, type, parsed, schema, path),
200216
);
201217
};
202218

0 commit comments

Comments
 (0)