From b3f598dffd027e2d8d4336aa30297fb6e7a1a379 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Fri, 8 May 2026 16:48:33 +0000 Subject: [PATCH] docs: add LICENSE, README, CHANGELOG, and license headers for npm publish - Add combined MIT + Apache-2.0 LICENSE file (Copyright 2026 alk.dev) - Add README.md with quick start for each adapter, lifecycle/close docs, operators, EventEnvelope, TypeScript, exports table, upstream attribution - Add CHANGELOG.md with 0.1.0 entry - Add SPDX license headers to original source files (WS client, WS server, Worker, index barrel) - Update package.json: add author, repository, homepage, bugs; include CHANGELOG.md in files; update description --- CHANGELOG.md | 43 ++++++ LICENSE | 214 +++++++++++++++++++++++++++ README.md | 209 ++++++++++++++++++++++++++ package.json | 14 +- src/event-target-websocket-client.ts | 6 + src/event-target-websocket-server.ts | 6 + src/event-target-worker.ts | 6 + src/index.ts | 6 + 8 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b5bb187 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +## 0.1.0 + +Initial release. + +### Core + +- `createPubSub` factory with type-safe `publish`/`subscribe` API (adapted from graphql-yoga, MIT) +- `TypedEventTarget`, `TypedEvent`, `EventEnvelope` types +- `Repeater` class for backpressure-aware async iteration (inlined from @repeaterjs/repeater, MIT) +- Operators: `filter`, `map`, `pipe`, `take`, `reduce`, `toArray`, `batch`, `dedupe`, `window`, `flat`, `groupBy`, `chain`, `join` + +### Adapters + +- **Redis** — `createRedisEventTarget` (peer dep: `ioredis`). Pub/sub with optional prefix and custom serializer. +- **WebSocket Client** — `createWebSocketClientEventTarget`. Subscribe/publish over a WebSocket connection. +- **WebSocket Server** — `createWebSocketServerEventTarget`. Fan-out hub for WebSocket spokes with topic-based routing, backpressure handling, and subscription control protocol (`__subscribe`/`__unsubscribe`). +- **Worker Host** — `createWorkerHostEventTarget`. Communicate with a Worker thread from the main thread. +- **Worker Thread** — `createWorkerThreadEventTarget`. Communicate with the main thread from inside a Worker. + +### Lifecycle + +All transport adapters provide a `close()` method for graceful teardown: + +- Unsubscribes from all channels/topics +- Restores any intercepted `onmessage`/`onclose` handlers +- Removes message listeners +- Clears internal state maps +- Does **not** destroy the underlying transport (caller-owned) +- Idempotent — safe to call multiple times + +### Build + +- Dual ESM + CJS output via tsup +- Sub-path exports for each adapter (`@alkdev/pubsub/event-target-redis`, etc.) +- Zero runtime dependencies (Repeater is inlined) +- `sideEffects: false` +- TypeScript declarations (`.d.ts` + `.d.cts`) + +### License + +Dual-licensed MIT OR Apache-2.0. Portions adapted from graphql-yoga (MIT) and @repeaterjs/repeater (MIT) retain their upstream attribution. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0305bf5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,214 @@ +MIT License + +Copyright (c) 2026 alk.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to the Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by the Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work by +You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright (c) 2026 alk.dev + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e232d38 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# @alkdev/pubsub + +Type-safe publish/subscribe with pluggable event target adapters. Transport layer only — no call protocol or coordination semantics. + +Every event is an `EventEnvelope` with `{ type, id, payload }`. Adapters implement the `TypedEventTarget` interface so you can swap transports without changing your subscribe logic. + +## Install + +```bash +npm install @alkdev/pubsub +``` + +For Redis transport: + +```bash +npm install ioredis +``` + +WebSocket and Worker adapters use built-in APIs — no additional dependencies. + +## Quick Start + +### In-Process (default) + +```ts +import { createPubSub } from "@alkdev/pubsub"; + +type EventMap = { + "user.created": { name: string }; + "order.placed": { orderId: string }; +}; + +const pubsub = createPubSub(); + +pubsub.subscribe("user.created", (_, payload) => { + console.log(`New user: ${payload.name}`); +}); + +pubsub.publish("user.created", "id-1", { name: "Alice" }); +``` + +### Redis + +```ts +import { createPubSub } from "@alkdev/pubsub"; +import { createRedisEventTarget } from "@alkdev/pubsub/event-target-redis"; +import Redis from "ioredis"; + +const publishClient = new Redis(); +const subscribeClient = new Redis(); + +const eventTarget = createRedisEventTarget({ + publishClient, + subscribeClient, +}); + +const pubsub = createPubSub({ eventTarget }); +``` + +### WebSocket Client (browser/Node) + +```ts +import { createPubSub } from "@alkdev/pubsub"; +import { createWebSocketClientEventTarget } from "@alkdev/pubsub/event-target-websocket-client"; + +const ws = new WebSocket("ws://localhost:8080"); +const eventTarget = createWebSocketClientEventTarget(ws); + +const pubsub = createPubSub({ eventTarget }); +``` + +### WebSocket Server (Node) + +```ts +import { createWebSocketServerEventTarget } from "@alkdev/pubsub/event-target-websocket-server"; + +const server = createWebSocketServerEventTarget({ + onConnection(spoke, ws) { /* new client connected */ }, + onDisconnection(spoke, ws) { /* client disconnected */ }, + maxBufferedAmount: 1_048_576, + onBackpressure(ws, bufferedAmount) { /* optional backpressure signal */ }, +}); + +// When a new WebSocket connects: +server.addConnection(ws); + +// When it disconnects: +server.removeConnection(ws); + +// Subscribe local handlers: +server.addEventListener("user.created:id-1", (event) => { + // event.detail is the EventEnvelope +}); + +// Publish to subscribed connections: +server.dispatchEvent(new CustomEvent("user.created:id-1", { detail: envelope })); +``` + +### Worker (Host ↔ Thread) + +```ts +// Host (main thread) +import { createWorkerHostEventTarget } from "@alkdev/pubsub/event-target-worker"; + +const worker = new Worker("./worker.js"); +const eventTarget = createWorkerHostEventTarget(worker); +``` + +```ts +// Worker thread +import { createWorkerThreadEventTarget } from "@alkdev/pubsub/event-target-worker"; + +const eventTarget = createWorkerThreadEventTarget(); +// Must be called inside a Worker context — throws if globalThis.postMessage is unavailable +``` + +## Lifecycle + +All transport adapters provide a `close()` method for graceful teardown: + +```ts +const eventTarget = createRedisEventTarget({ publishClient, subscribeClient }); +// ... subscribe and publish ... + +eventTarget.close(); // unsubscribes all channels, removes listener, clears state +``` + +After `close()`: +- `addEventListener`, `removeEventListener`, and `dispatchEvent` are no-ops +- Intercepted handlers (`onmessage`, `onclose`) are restored to their originals +- Subscriptions are cleaned up (Redis channels unsubscribed, WebSocket `__unsubscribe` sent) +- The underlying transport (Redis connection, WebSocket, Worker) is **not** destroyed — the caller owns it + +`close()` is idempotent. Calling it multiple times is safe. + +## Operators + +Operators transform `AsyncIterable` streams from `subscribe()`: + +```ts +import { pipe, filter, map, take, batch } from "@alkdev/pubsub"; + +const pubsub = createPubSub(); + +const stream = pubsub.subscribe("user.created"); + +for await (const event of pipe( + stream, + filter((e) => e.payload.name.startsWith("A")), + map((e) => e.payload.name), + take(5), +)) { + console.log(event); +} +``` + +Available operators: `filter`, `map`, `pipe`, `take`, `reduce`, `toArray`, `batch`, `dedupe`, `window`, `flat`, `groupBy`, `chain`, `join`. + +## EventEnvelope + +All events are serialized as `EventEnvelope`: + +```ts +interface EventEnvelope { + type: TType; + id: string; + payload: TPayload; +} +``` + +This is the cross-platform wire format. Adapters serialize/deserialize this automatically (JSON for Redis and WebSocket, structured clone for Worker). + +## Subscription Control Protocol + +Event types starting with `__` are reserved for internal use. Adapters use `__subscribe` and `__unsubscribe` control events to manage topic subscriptions across connections. User code must not define event types with the `__` prefix. + +## TypeScript + +Full type inference through `EventMap`: + +```ts +type EventMap = { + "user.created": { name: string; role: string }; + "order.placed": { orderId: string; total: number }; +}; + +const pubsub = createPubSub(); + +pubsub.publish("user.created", "id-1", { name: "Alice", role: "admin" }); +// ^ full type checking on payload +``` + +## Exports + +| Import | Description | +|--------|-------------| +| `@alkdev/pubsub` | Core: `createPubSub`, `EventEnvelope`, `Repeater`, operators | +| `@alkdev/pubsub/event-target-redis` | Redis adapter (peer dep: `ioredis`) | +| `@alkdev/pubsub/event-target-websocket-client` | WebSocket client adapter | +| `@alkdev/pubsub/event-target-websocket-server` | WebSocket server adapter | +| `@alkdev/pubsub/event-target-worker` | Worker host + thread adapters | + +## Upstream Attribution + +Core `createPubSub`, `TypedEventTarget`, and operators are adapted from [graphql-yoga](https://github.com/graphql-hive/graphql-yoga) (MIT). The `Repeater` class is inlined from [@repeaterjs/repeater](https://github.com/repeaterjs/repeater) (MIT). + +## License + +Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE). Portions adapted from upstream projects retain their MIT attribution. \ No newline at end of file diff --git a/package.json b/package.json index e3265fc..c27eb89 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,20 @@ { "name": "@alkdev/pubsub", "version": "0.1.0", - "description": "Type-safe publish/subscribe with pluggable event target adapters (in-process, Redis, WebSocket, Iroh)", + "description": "Type-safe publish/subscribe with pluggable event target adapters (in-process, Redis, WebSocket, Worker)", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", + "author": "alk.dev", + "repository": { + "type": "git", + "url": "git+https://git.alk.dev/alkdev/pubsub.git" + }, + "homepage": "https://git.alk.dev/alkdev/pubsub", + "bugs": { + "url": "https://git.alk.dev/alkdev/pubsub/issues" + }, "exports": { ".": { "import": { @@ -63,7 +72,8 @@ }, "sideEffects": false, "files": [ - "dist" + "dist", + "CHANGELOG.md" ], "scripts": { "build": "tsup", diff --git a/src/event-target-websocket-client.ts b/src/event-target-websocket-client.ts index cdf694a..9ae31d2 100644 --- a/src/event-target-websocket-client.ts +++ b/src/event-target-websocket-client.ts @@ -1,3 +1,9 @@ +/* + * @alkdev/pubsub — WebSocket Client Event Target + * Copyright (c) 2026 alk.dev + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + import type { TypedEventTarget, TypedEvent, EventEnvelope } from "./types.js"; export interface WebSocketClientEventTarget extends TypedEventTarget { diff --git a/src/event-target-websocket-server.ts b/src/event-target-websocket-server.ts index c488588..b6f9993 100644 --- a/src/event-target-websocket-server.ts +++ b/src/event-target-websocket-server.ts @@ -1,3 +1,9 @@ +/* + * @alkdev/pubsub — WebSocket Server Event Target + * Copyright (c) 2026 alk.dev + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + import type { TypedEventTarget, TypedEvent, EventEnvelope } from "./types.js"; export interface WebSocketLike { diff --git a/src/event-target-worker.ts b/src/event-target-worker.ts index 9407152..40e297a 100644 --- a/src/event-target-worker.ts +++ b/src/event-target-worker.ts @@ -1,3 +1,9 @@ +/* + * @alkdev/pubsub — Worker Event Target + * Copyright (c) 2026 alk.dev + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + import type { TypedEventTarget, TypedEvent, EventEnvelope } from "./types.js"; export interface WorkerHostEventTarget extends TypedEventTarget { diff --git a/src/index.ts b/src/index.ts index d7a5981..4c35d22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,9 @@ +/* + * @alkdev/pubsub — Type-safe publish/subscribe with pluggable event target adapters + * Copyright (c) 2026 alk.dev + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + export { createPubSub, type PubSub, type PubSubConfig, type PubSubEvent, type PubSubEventTarget, type PubSubEventMap } from "./create_pubsub.js"; export { type EventEnvelope, type TypedEvent, type TypedEventTarget, type TypedEventListener, type TypedEventListenerObject, type TypedEventListenerOrEventListenerObject } from "./types.js"; export { filter, map, pipe, take, reduce, toArray, batch, dedupe, window, flat, groupBy, chain, join } from "./operators.js";