Skip to content

Commit 69e86a7

Browse files
committed
granular topics
1 parent 9341d5a commit 69e86a7

File tree

4 files changed

+191
-1
lines changed

4 files changed

+191
-1
lines changed

Diff for: index.spec.ts

+67
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createMutationListener,
77
createPartition,
88
createSyncListener,
9+
createTopic,
910
halt,
1011
} from ".";
1112
import { setTimeout } from "timers/promises";
@@ -263,3 +264,69 @@ describe(`createSyncListener`, () => {
263264
d();
264265
});
265266
});
267+
268+
describe(`createTopic`, () => {
269+
test(`emits to multiple listeners`, () => {
270+
const messages = [] as number[];
271+
272+
const d = createRoot((d) => {
273+
const [onTopic, emitTopic] = createTopic<{ a: number; b: number }>();
274+
275+
onTopic(`a`, (p) => messages.push(p));
276+
onTopic(`b`, (p) => messages.push(p + 1));
277+
278+
emitTopic(`a`, 1);
279+
emitTopic(`b`, 2);
280+
return d;
281+
});
282+
283+
expect(messages).toEqual([1, 3]);
284+
d();
285+
});
286+
287+
test(`emits to nested listeners`, () => {
288+
const messages = [] as number[];
289+
290+
const d = createRoot((d) => {
291+
const [onTopic, emitTopic] = createTopic<{
292+
a: number;
293+
b: { c: number };
294+
}>();
295+
296+
onTopic(`a`, (p) => messages.push(p));
297+
onTopic(`b`, (p) => messages.push(p.c));
298+
onTopic(`b`, `c`, (p) => messages.push(p));
299+
300+
emitTopic(`a`, 1);
301+
emitTopic(`b`, { c: 2 });
302+
emitTopic(`b`, `c`, 3);
303+
emitTopic({ a: 4, b: { c: 5 } });
304+
305+
return d;
306+
});
307+
308+
expect(messages).toEqual([1, 2, 2, 3, 3, 4, 5, 5]);
309+
d();
310+
});
311+
312+
test(`transforms into new topic handlers`, () => {
313+
const messages = [] as number[];
314+
315+
const d = createRoot((d) => {
316+
const [onTopic, emitTopic] = createTopic<{ a: { b: number } }>();
317+
318+
const onB = onTopic(`a`, (p) => p.b);
319+
const onTopicA = onTopic(`a`);
320+
321+
onB((p) => messages.push(p));
322+
onTopicA(`b`, (p) => messages.push(p));
323+
324+
emitTopic(`a`, { b: 1 });
325+
emitTopic(`a`, `b`, 2);
326+
return d;
327+
});
328+
329+
expect(messages).toEqual([1, 1, 2, 2]);
330+
d();
331+
});
332+
});

Diff for: index.ts

+107-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ function makeHandler<E>($: Observable<Promise<E | HaltError> | E>): Handler<E> {
5656

5757
export function createEvent<E = any>(): [Handler<E>, Emitter<E>] {
5858
const $ = new Subject<E>();
59-
return [makeHandler($), (e) => ($.next(e), flushQueues())] as const;
59+
return [
60+
makeHandler($),
61+
(e) => (pureQueue.push(() => $.next(e)), flushQueues()),
62+
] as const;
6063
}
6164

6265
export function createSubject<T>(
@@ -233,3 +236,106 @@ export function introspectQueues() {
233236
listenerQueue.length
234237
);
235238
}
239+
240+
export type TopicHandler<T extends Record<string, any>> = {
241+
<K extends keyof T, O>(
242+
key: K,
243+
transform: (e: T[K]) => Promise<O> | O
244+
): Handler<O>;
245+
<K extends keyof T>(key: K): TopicHandler<T[K]>;
246+
247+
<K1 extends keyof T, K2 extends keyof T[K1], O>(
248+
key1: K1,
249+
key2: K2,
250+
transform: (e: T[K1][K2]) => Promise<O> | O
251+
): Handler<O>;
252+
<K1 extends keyof T, K2 extends keyof T[K1]>(
253+
key1: K1,
254+
key2: K2
255+
): TopicHandler<T[K1][K2]>;
256+
257+
<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], O>(
258+
key1: K1,
259+
key2: K2,
260+
key3: K3,
261+
transform: (e: T[K1][K2][K3]) => Promise<O> | O
262+
): Handler<O>;
263+
<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(
264+
key1: K1,
265+
key2: K2,
266+
key3: K3
267+
): TopicHandler<T[K1][K2][K3]>;
268+
};
269+
export type TopicEmitter<T extends Record<string, any>> = {
270+
(e: T): void;
271+
<K extends keyof T>(key: K, e: T[K]): void;
272+
<K1 extends keyof T, K2 extends keyof T[K1]>(
273+
key1: K1,
274+
key2: K2,
275+
e: T[K1][K2]
276+
): void;
277+
<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(
278+
key1: K1,
279+
key2: K2,
280+
key3: K3,
281+
e: T[K1][K2][K3]
282+
): void;
283+
};
284+
285+
const $EVENT = Symbol("event");
286+
type TopicNode = {
287+
[$EVENT]?: [Handler<any>, Emitter<any>];
288+
[key: string]: TopicNode;
289+
};
290+
291+
export function createTopic<T extends Record<string, any>>() {
292+
const topicTree: TopicNode = {};
293+
294+
// @ts-expect-error
295+
const on: TopicHandler<T> = (...key: (string | ((e: any) => any))[]) => {
296+
const transform = key.pop()!;
297+
298+
if (typeof transform !== "function") {
299+
// @ts-expect-error
300+
return (...args: any[]) => on(...key, transform, ...args);
301+
}
302+
303+
const node = (key as string[]).reduce((node, k) => {
304+
if (!node[k]) node[k] = {};
305+
return node[k];
306+
}, topicTree);
307+
308+
if (!node[$EVENT]) {
309+
node[$EVENT] = createEvent();
310+
}
311+
312+
return node[$EVENT][0](transform);
313+
};
314+
315+
const emit: TopicEmitter<T> = (...key: (string | any)[]) => {
316+
const payload = key.pop()! as any;
317+
318+
if (typeof payload === "object") {
319+
Object.keys(payload).forEach((k) => {
320+
// @ts-expect-error
321+
emit(...key, k, payload[k]);
322+
});
323+
return;
324+
}
325+
326+
for (let i = 0; i <= key.length; i++) {
327+
let p = payload;
328+
key
329+
.slice(i)
330+
.toReversed()
331+
.forEach((k) => {
332+
p = { [k]: p };
333+
});
334+
335+
const node = key.slice(0, i).reduce((node, k) => node[k], topicTree);
336+
node[$EVENT]?.[1](p);
337+
}
338+
};
339+
340+
return [on, emit] as const;
341+
}

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.0.5",
44
"description": "Declarative event composition and state derivation primitives for Solidjs",
55
"main": "index.ts",
6+
"type": "module",
67
"scripts": {
78
"test": "vitest"
89
},

Diff for: tsconfig.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6",
4+
"strict": true,
5+
"esModuleInterop": true,
6+
"skipLibCheck": true,
7+
"forceConsistentCasingInFileNames": true,
8+
"outDir": "./dist",
9+
"rootDir": "./src",
10+
"lib": ["ESNext"],
11+
"moduleResolution": "nodenext",
12+
"module": "NodeNext"
13+
},
14+
"include": ["./index.ts"],
15+
"exclude": ["node_modules", "**/*.spec.ts"]
16+
}

0 commit comments

Comments
 (0)