Compare commits
1 Commits
wave1/buil
...
wave1/core
| Author | SHA1 | Date | |
|---|---|---|---|
| dd720a9e0b |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: build-and-exports-validation
|
||||
name: Validate build, package.json exports, and tsup config for all adapters
|
||||
status: completed
|
||||
status: pending
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
@@ -38,33 +38,8 @@ This task validates the current setup and serves as a checklist item for each ad
|
||||
|
||||
## Notes
|
||||
|
||||
Validated all acceptance criteria. No changes were needed — the existing configuration is correct.
|
||||
|
||||
### Validation Results
|
||||
|
||||
1. **`npm run build` produces correct output** ✅
|
||||
- `src/index.ts` → `dist/index.js` (ESM), `dist/index.cjs` (CJS), `dist/index.d.ts` + `dist/index.d.cts` (declarations)
|
||||
- `src/event-target-redis.ts` → `dist/event-target-redis.js` (ESM), `dist/event-target-redis.cjs` (CJS), `dist/event-target-redis.d.ts` + `dist/event-target-redis.d.cts` (declarations)
|
||||
- Code splitting enabled: shared chunk between entries
|
||||
- Verified both ESM dynamic import and CJS require work for both entry points
|
||||
|
||||
2. **`npm run lint` (tsc --noEmit) passes** ✅
|
||||
3. **`package.json` exports map has correct ESM/CJS/dts paths** ✅
|
||||
4. **`tsup.config.ts` lists all current entry points** ✅
|
||||
5. **`src/index.ts` re-exports everything from all modules** ✅
|
||||
6. **Peer dependencies and peerDependenciesMeta are correct** ✅
|
||||
7. **No runtime dependencies** ✅
|
||||
|
||||
### Verified type resolution
|
||||
- Sub-path and barrel imports resolve correctly with tsc
|
||||
- Works with both skipLibCheck true and false
|
||||
|
||||
### Verified package contents
|
||||
- `npm pack --dry-run` shows 19 files (all dist files + package.json)
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
Validated build pipeline, package.json exports map, and tsup config for all adapters. All acceptance criteria pass with no changes required.
|
||||
- Created: none
|
||||
- Modified: tasks/016-build-and-exports-validation.md (status update only)
|
||||
- Tests: 0 (no test files exist yet)
|
||||
> To be filled on completion
|
||||
272
test/create_pubsub.test.ts
Normal file
272
test/create_pubsub.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { createPubSub } from "../src/create_pubsub.js";
|
||||
import type { EventEnvelope, TypedEventTarget } from "../src/types.js";
|
||||
|
||||
type TestEventMap = {
|
||||
"message.sent": string;
|
||||
"user.joined": { name: string };
|
||||
"session.status": { status: string; code: number };
|
||||
};
|
||||
|
||||
describe("createPubSub", () => {
|
||||
describe("publish", () => {
|
||||
it("dispatches event with correct type:id topic", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<"message.sent", string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "abc123");
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "abc123", "hello");
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].type).toBe("message.sent");
|
||||
expect(received[0].id).toBe("abc123");
|
||||
});
|
||||
|
||||
it("throws on __-prefixed event types", () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
|
||||
expect(() => pubsub.publish("__subscribe" as any, "", {})).toThrow(
|
||||
'Event types starting with "__" are reserved for adapter control messages. Received: "__subscribe"',
|
||||
);
|
||||
});
|
||||
|
||||
it("dispatches EventEnvelope with type, id, and payload as CustomEvent detail", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<"user.joined", { name: string }>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("user.joined", "user1");
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("user.joined", "user1", { name: "Alice" });
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0]).toEqual({
|
||||
type: "user.joined",
|
||||
id: "user1",
|
||||
payload: { name: "Alice" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("subscribe", () => {
|
||||
it("returns async iterable that yields EventEnvelope objects", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "msg1");
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length === 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "msg1", "hello");
|
||||
|
||||
await consume;
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0]).toEqual({
|
||||
type: "message.sent",
|
||||
id: "msg1",
|
||||
payload: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("yields envelope with correct type, id, and payload fields", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
let result: EventEnvelope<"session.status", { status: string; code: number }> | undefined;
|
||||
|
||||
const iterator = pubsub.subscribe("session.status", "sess1");
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
result = envelope;
|
||||
break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("session.status", "sess1", { status: "active", code: 200 });
|
||||
|
||||
await consume;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.type).toBe("session.status");
|
||||
expect(result!.id).toBe("sess1");
|
||||
expect(result!.payload).toEqual({ status: "active", code: 200 });
|
||||
});
|
||||
|
||||
it("receives events only for the subscribed topic", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "msg1");
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length >= 2) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "msg1", "first");
|
||||
pubsub.publish("message.sent", "msg_different", "wrong topic");
|
||||
pubsub.publish("message.sent", "msg1", "second");
|
||||
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(2);
|
||||
expect(received[0].payload).toBe("first");
|
||||
expect(received[1].payload).toBe("second");
|
||||
});
|
||||
|
||||
it("multiple subscribers on the same topic all receive events", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received1: EventEnvelope<string>[] = [];
|
||||
const received2: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator1 = pubsub.subscribe("message.sent", "msg1");
|
||||
const iterator2 = pubsub.subscribe("message.sent", "msg1");
|
||||
|
||||
const consume1 = (async () => {
|
||||
for await (const envelope of iterator1) {
|
||||
received1.push(envelope);
|
||||
if (received1.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
const consume2 = (async () => {
|
||||
for await (const envelope of iterator2) {
|
||||
received2.push(envelope);
|
||||
if (received2.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "msg1", "broadcast");
|
||||
|
||||
await Promise.all([consume1, consume2]);
|
||||
|
||||
expect(received1).toHaveLength(1);
|
||||
expect(received2).toHaveLength(1);
|
||||
expect(received1[0].payload).toBe("broadcast");
|
||||
expect(received2[0].payload).toBe("broadcast");
|
||||
});
|
||||
|
||||
it("cleanup: breaking out of for await loop removes the listener", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "cleanup-test");
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "cleanup-test", "first");
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].payload).toBe("first");
|
||||
|
||||
pubsub.publish("message.sent", "cleanup-test", "after-break");
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
|
||||
const secondIterator = pubsub.subscribe("message.sent", "cleanup-test");
|
||||
const secondReceived: EventEnvelope<string>[] = [];
|
||||
const consume2 = (async () => {
|
||||
for await (const envelope of secondIterator) {
|
||||
secondReceived.push(envelope);
|
||||
if (secondReceived.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "cleanup-test", "second-listener");
|
||||
await consume2;
|
||||
|
||||
expect(secondReceived).toHaveLength(1);
|
||||
expect(secondReceived[0].payload).toBe("second-listener");
|
||||
expect(received).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPubSub config", () => {
|
||||
it("with custom eventTarget dispatches to that target", () => {
|
||||
const customTarget = new EventTarget() as TypedEventTarget<any>;
|
||||
const dispatchSpy = vi.spyOn(customTarget, "dispatchEvent");
|
||||
|
||||
const pubsub = createPubSub<TestEventMap>({ eventTarget: customTarget });
|
||||
|
||||
pubsub.publish("message.sent", "custom1", "hello custom");
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchSpy.mock.calls[0][0].type).toBe("message.sent:custom1");
|
||||
expect((dispatchSpy.mock.calls[0][0] as CustomEvent).detail).toEqual({
|
||||
type: "message.sent",
|
||||
id: "custom1",
|
||||
payload: "hello custom",
|
||||
});
|
||||
|
||||
dispatchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("without eventTarget uses new EventTarget() (in-process)", async () => {
|
||||
const pubsub = createPubSub<TestEventMap>();
|
||||
const received: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "inproc1");
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "inproc1", "in-process works");
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].payload).toBe("in-process works");
|
||||
});
|
||||
|
||||
it("custom eventTarget receives events via subscribe", async () => {
|
||||
const customTarget = new EventTarget() as TypedEventTarget<any>;
|
||||
const pubsub = createPubSub<TestEventMap>({ eventTarget: customTarget });
|
||||
const received: EventEnvelope<string>[] = [];
|
||||
|
||||
const iterator = pubsub.subscribe("message.sent", "custom-sub");
|
||||
|
||||
const consume = (async () => {
|
||||
for await (const envelope of iterator) {
|
||||
received.push(envelope);
|
||||
if (received.length >= 1) break;
|
||||
}
|
||||
})();
|
||||
|
||||
pubsub.publish("message.sent", "custom-sub", "via custom");
|
||||
|
||||
await consume;
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].payload).toBe("via custom");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user