@shadow-js/realtime 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +114 -2
  4. package/dist/dev/adapter.d.ts +73 -0
  5. package/dist/dev/client/core.d.ts +22 -0
  6. package/dist/dev/client/core.js +844 -0
  7. package/dist/dev/client/core.js.map +1 -0
  8. package/dist/dev/client/index.d.ts +5 -0
  9. package/dist/dev/client/index.js +3 -0
  10. package/dist/dev/client/presence.js +22 -0
  11. package/dist/dev/client/presence.js.map +1 -0
  12. package/dist/dev/client/sse.js +75 -0
  13. package/dist/dev/client/sse.js.map +1 -0
  14. package/dist/dev/client/types.d.ts +254 -0
  15. package/dist/dev/client/util.js +29 -0
  16. package/dist/dev/client/util.js.map +1 -0
  17. package/dist/dev/client/ws.js +38 -0
  18. package/dist/dev/client/ws.js.map +1 -0
  19. package/dist/dev/cloudflare/adapter.d.ts +78 -0
  20. package/dist/dev/cloudflare/adapter.js +346 -0
  21. package/dist/dev/cloudflare/adapter.js.map +1 -0
  22. package/dist/dev/cloudflare/core.js +700 -0
  23. package/dist/dev/cloudflare/core.js.map +1 -0
  24. package/dist/dev/cloudflare/link.js +396 -0
  25. package/dist/dev/cloudflare/link.js.map +1 -0
  26. package/dist/dev/cloudflare/object.d.ts +80 -0
  27. package/dist/dev/cloudflare/object.js +213 -0
  28. package/dist/dev/cloudflare/object.js.map +1 -0
  29. package/dist/dev/cloudflare/types.d.ts +98 -0
  30. package/dist/dev/cloudflare/wire.js +283 -0
  31. package/dist/dev/cloudflare/wire.js.map +1 -0
  32. package/dist/dev/cloudflare.d.ts +4 -0
  33. package/dist/dev/cloudflare.js +3 -0
  34. package/dist/dev/errors.d.ts +23 -0
  35. package/dist/dev/errors.js +23 -0
  36. package/dist/dev/errors.js.map +1 -0
  37. package/dist/dev/hooks.d.ts +108 -0
  38. package/dist/dev/hooks.js +161 -0
  39. package/dist/dev/hooks.js.map +1 -0
  40. package/dist/dev/index.d.ts +4 -0
  41. package/dist/dev/index.js +3 -0
  42. package/dist/dev/memory.d.ts +31 -0
  43. package/dist/dev/memory.js +254 -0
  44. package/dist/dev/memory.js.map +1 -0
  45. package/dist/dev/protocol.d.ts +209 -0
  46. package/dist/dev/protocol.js +399 -0
  47. package/dist/dev/protocol.js.map +1 -0
  48. package/dist/dev/redis/client.d.ts +137 -0
  49. package/dist/dev/redis/client.js +145 -0
  50. package/dist/dev/redis/client.js.map +1 -0
  51. package/dist/dev/redis/scripts.js +274 -0
  52. package/dist/dev/redis/scripts.js.map +1 -0
  53. package/dist/dev/redis/topics.d.ts +6 -0
  54. package/dist/dev/redis/topics.js +376 -0
  55. package/dist/dev/redis/topics.js.map +1 -0
  56. package/dist/dev/redis.d.ts +66 -0
  57. package/dist/dev/redis.js +421 -0
  58. package/dist/dev/redis.js.map +1 -0
  59. package/dist/dev/server/http.d.ts +46 -0
  60. package/dist/dev/server/http.js +285 -0
  61. package/dist/dev/server/http.js.map +1 -0
  62. package/dist/dev/server/hub.d.ts +112 -0
  63. package/dist/dev/server/hub.js +826 -0
  64. package/dist/dev/server/hub.js.map +1 -0
  65. package/dist/dev/server/index.d.ts +45 -0
  66. package/dist/dev/server/index.js +40 -0
  67. package/dist/dev/server/index.js.map +1 -0
  68. package/dist/dev/server/limits.d.ts +52 -0
  69. package/dist/dev/server/limits.js +84 -0
  70. package/dist/dev/server/limits.js.map +1 -0
  71. package/dist/dev/server/rules.d.ts +41 -0
  72. package/dist/dev/server/rules.js +71 -0
  73. package/dist/dev/server/rules.js.map +1 -0
  74. package/dist/dev/testing.d.ts +113 -0
  75. package/dist/dev/testing.js +426 -0
  76. package/dist/dev/testing.js.map +1 -0
  77. package/dist/prod/client/core.js +2 -0
  78. package/dist/prod/client/core.js.map +1 -0
  79. package/dist/prod/client/index.js +1 -0
  80. package/dist/prod/client/presence.js +2 -0
  81. package/dist/prod/client/presence.js.map +1 -0
  82. package/dist/prod/client/sse.js +5 -0
  83. package/dist/prod/client/sse.js.map +1 -0
  84. package/dist/prod/client/util.js +2 -0
  85. package/dist/prod/client/util.js.map +1 -0
  86. package/dist/prod/client/ws.js +2 -0
  87. package/dist/prod/client/ws.js.map +1 -0
  88. package/dist/prod/cloudflare/adapter.js +3 -0
  89. package/dist/prod/cloudflare/adapter.js.map +1 -0
  90. package/dist/prod/cloudflare/core.js +2 -0
  91. package/dist/prod/cloudflare/core.js.map +1 -0
  92. package/dist/prod/cloudflare/link.js +2 -0
  93. package/dist/prod/cloudflare/link.js.map +1 -0
  94. package/dist/prod/cloudflare/object.js +2 -0
  95. package/dist/prod/cloudflare/object.js.map +1 -0
  96. package/dist/prod/cloudflare/wire.js +2 -0
  97. package/dist/prod/cloudflare/wire.js.map +1 -0
  98. package/dist/prod/cloudflare.js +1 -0
  99. package/dist/prod/errors.js +2 -0
  100. package/dist/prod/errors.js.map +1 -0
  101. package/dist/prod/hooks.js +2 -0
  102. package/dist/prod/hooks.js.map +1 -0
  103. package/dist/prod/index.js +1 -0
  104. package/dist/prod/memory.js +2 -0
  105. package/dist/prod/memory.js.map +1 -0
  106. package/dist/prod/protocol.js +2 -0
  107. package/dist/prod/protocol.js.map +1 -0
  108. package/dist/prod/redis/client.js +2 -0
  109. package/dist/prod/redis/client.js.map +1 -0
  110. package/dist/prod/redis/scripts.js +157 -0
  111. package/dist/prod/redis/scripts.js.map +1 -0
  112. package/dist/prod/redis/topics.js +2 -0
  113. package/dist/prod/redis/topics.js.map +1 -0
  114. package/dist/prod/redis.js +2 -0
  115. package/dist/prod/redis.js.map +1 -0
  116. package/dist/prod/server/http.js +2 -0
  117. package/dist/prod/server/http.js.map +1 -0
  118. package/dist/prod/server/hub.js +2 -0
  119. package/dist/prod/server/hub.js.map +1 -0
  120. package/dist/prod/server/index.js +2 -0
  121. package/dist/prod/server/index.js.map +1 -0
  122. package/dist/prod/server/limits.js +2 -0
  123. package/dist/prod/server/limits.js.map +1 -0
  124. package/dist/prod/server/rules.js +2 -0
  125. package/dist/prod/server/rules.js.map +1 -0
  126. package/dist/prod/testing.js +2 -0
  127. package/dist/prod/testing.js.map +1 -0
  128. package/package.json +105 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ ### Changed
6
+
7
+ - Released at 1.0.1 with the other packages, which share one version. No change to this package.
8
+
9
+ ## 1.0.0
10
+
11
+ ### Added
12
+
13
+ - Typed channels with rules that deny by default, per-channel numbering, resume with replay and gap detection, acknowledged and
14
+ idempotent publishes, request and reply, presence with heartbeats and exactly-once leave, rate limits, slow-consumer protection and
15
+ revocation.
16
+ - WebSocket and server-sent-events transports with an HTTP post route, a client with jittered reconnect, an offline queue and
17
+ automatic fallback, and signal-based hooks.
18
+ - Adapters: in memory, Redis (6.2+ and Valkey) and Cloudflare Durable Objects, all passing one conformance suite
19
+ (`adapterConformance`), plus `connectTest` and `memoryWebSocket` for tests.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShadowJS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ <p align="center"><img src="https://raw.githubusercontent.com/shadowjs-dev/realtime/main/.github/banner.png" alt="ShadowJS" width="100%"></p>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @shadow-js/realtime
4
+
5
+ [![npm](https://img.shields.io/npm/v/@shadow-js/realtime.svg)](https://www.npmjs.com/package/@shadow-js/realtime)
6
+ [![CI](https://github.com/shadowjs-dev/realtime/actions/workflows/ci.yml/badge.svg)](https://github.com/shadowjs-dev/realtime/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/shadowjs-dev/realtime/blob/main/LICENSE)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen.svg)](https://nodejs.org/)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/)
10
+
11
+ Typed channels, presence and resumable delivery over WebSocket or server-sent events.
12
+
13
+ - **Typed channels.** One schema gives every channel its events, presence state and requests, checked at compile time.
14
+ - **Resumable.** Every message has a number. A client that drops and reconnects resumes from its last number: no loss and no duplicates inside the retention window, and a clear signal when the window was exceeded.
15
+ - **Closed by default.** A channel nobody wrote a rule for cannot be subscribed to or published to.
16
+ - **Scales out.** The in-memory adapter serves one process; the Redis adapter (Redis 6.2+ and Valkey) and the Cloudflare Durable Objects adapter serve many.
17
+ - **Runs where Dusk runs.** WebSocket where the host holds sockets, server-sent events plus HTTP posts where it does not (Vercel, Netlify).
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @shadow-js/realtime
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ A hub is the server. Rules say who may do what; `connectTest` talks to it with no network.
28
+
29
+ ```ts title="chat.test.ts" test
30
+ import { createHub } from "@shadow-js/realtime/server";
31
+ import { connectTest } from "@shadow-js/realtime/testing";
32
+ import { expect, it } from "vitest";
33
+
34
+ it("delivers a message to a subscriber, in order, with an acknowledgment", async () => {
35
+ const hub = createHub({
36
+ channels: {
37
+ "chat:{room}": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },
38
+ },
39
+ });
40
+ const ann = connectTest(hub, { user: "ann" });
41
+ const bob = connectTest(hub, { user: "bob" });
42
+ bob.send({ t: "sub", n: 1, c: "chat:lobby" });
43
+ await bob.settled();
44
+ ann.send({ t: "pub", c: "chat:lobby", e: "message", d: { text: "hi" }, n: 2 });
45
+ await ann.settled();
46
+
47
+ expect(bob.frames.find((frame) => frame.t === "msg")).toMatchObject({ s: 1, d: { text: "hi" } });
48
+ expect(ann.frames.find((frame) => frame.t === "ack")).toEqual({ t: "ack", n: 2, s: 1 });
49
+ await hub.close();
50
+ });
51
+ ```
52
+
53
+ In a Dusk app the hub is route exports. `routes/rt.ts` takes WebSockets and server-sent events; `routes/rt/send.ts` exports `POST = hub.post()` for what an SSE client sends:
54
+
55
+ ```ts title="routes/rt.ts" typecheck
56
+ import { createHub } from "@shadow-js/realtime/server";
57
+
58
+ export const hub = createHub({
59
+ channels: {
60
+ "chat:{room}": { subscribe: true, publish: ({ identity }) => identity.user !== undefined },
61
+ },
62
+ });
63
+
64
+ export const WS = hub.websocket();
65
+ export const SSE = hub.sse();
66
+ ```
67
+
68
+ The browser opens one typed connection:
69
+
70
+ ```ts title="client.ts" typecheck
71
+ import { createRealtime } from "@shadow-js/realtime/client";
72
+
73
+ type Schema = {
74
+ [name: `chat:${string}`]: { events: { message: { text: string } } };
75
+ };
76
+
77
+ const rt = createRealtime<Schema>({ url: "/rt" });
78
+ const lobby = rt.channel("chat:lobby");
79
+ lobby.on("message", (data, meta) => console.log(meta.seq, data.text));
80
+ await lobby.publish("message", { text: "hello" });
81
+ ```
82
+
83
+ ## API
84
+
85
+ Root, `@shadow-js/realtime`: the wire protocol and shared types.
86
+
87
+ - `RealtimeError` and `ErrorCode`: an error with a protocol code.
88
+ - `PROTOCOL_VERSION`, `decodeClientFrame`, `decodeServerFrame`, `isChannelName`, `isEventName`: the frame codecs and name validators.
89
+
90
+ `@shadow-js/realtime/server`:
91
+
92
+ - `createHub(options)`: the server. `hub.channel(pattern, rule)`, `hub.publish(channel, event, data)`, `hub.revoke(target)`, `hub.stats()`, `hub.close()`; the routes `hub.websocket()`, `hub.sse()`, `hub.post()`.
93
+ - `memoryAdapter()`: the single-process adapter (the default). `DEFAULT_HUB_LIMITS`: every limit and its default.
94
+
95
+ `@shadow-js/realtime/client`:
96
+
97
+ - `createRealtime(options)`: the client. `rt.channel(name, options)` returns a channel with `on`, `onAny`, `onGap`, `publish`, `request`, `presence` and `close`.
98
+
99
+ `@shadow-js/realtime/hooks`:
100
+
101
+ - `useChannel`, `useChannelEvent`, `useChannelPresence`, `useRealtimeStatus`: signal-based hooks that release their channel with the component.
102
+
103
+ `@shadow-js/realtime/redis`: `redisAdapter`, `fromIoredis`, `fromNodeRedis`.
104
+ `@shadow-js/realtime/cloudflare`: `cloudflareAdapter`, `RealtimeChannelObject`.
105
+ `@shadow-js/realtime/testing`: `connectTest`, `adapterConformance`.
106
+
107
+ ## Learn more
108
+
109
+ See the ShadowJS documentation at https://shadowjs.net, and the realtime guide at https://realtime.shadowjs.net.
110
+
111
+ ## Using, changing and contributing
112
+
113
+ - **Use the package** (a person or a coding agent that builds with it): this README, and [SKILL.md](SKILL.md), the short page an agent reads first.
114
+ - **Change the package** (fix a bug, add a feature): [AGENTS.md](AGENTS.md) says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
115
+ - **Take part**: [CONTRIBUTING.md](CONTRIBUTING.md) is the short guide; [SECURITY.md](SECURITY.md) is for vulnerabilities.
@@ -0,0 +1,73 @@
1
+ import { Member, PresenceEvent, StoredEntry, StoredMessage } from "./protocol.js";
2
+ /** How a publish is retained and deduplicated. @since 1.0.0 */
3
+ export interface PublishOptions {
4
+ /** Messages kept per channel (0 keeps none). */
5
+ readonly history: number;
6
+ /** Milliseconds a message is kept. */
7
+ readonly historyTtlMs: number;
8
+ /** An idempotency key: a second publish with it inside `idempotencyTtlMs` returns the first seq. */
9
+ readonly key?: string;
10
+ /** Milliseconds a key is remembered. */
11
+ readonly idempotencyTtlMs: number;
12
+ }
13
+ /** What a publish returns. @since 1.0.0 */
14
+ export interface PublishResult {
15
+ readonly seq: number;
16
+ /** True when the idempotency key was already accepted and nothing was stored. */
17
+ readonly duplicate: boolean;
18
+ }
19
+ /** What the history of a channel holds. @since 1.0.0 */
20
+ export interface HistoryResult {
21
+ /** Retained messages with `seq > after`, in order, at most `limit`. */
22
+ readonly messages: readonly StoredMessage[];
23
+ /** The channel's head seq (0 for a channel that never had a message). */
24
+ readonly head: number;
25
+ /** The oldest retained seq (0 when nothing is retained). */
26
+ readonly oldest: number;
27
+ }
28
+ /** A member of a channel's presence set, by id. @since 1.0.0 */
29
+ export interface PresenceRef {
30
+ readonly channel: string;
31
+ readonly id: string;
32
+ }
33
+ /** A way to stop listening. @since 1.0.0 */
34
+ export type Unsubscribe = () => Promise<void>;
35
+ /**
36
+ * Storage and fan-out between servers. The adapter rules are in the realtime protocol reference (https://realtime.shadowjs.net/guides/realtime-protocol/); `adapterConformance` from
37
+ * `@shadow-js/realtime/testing` checks an implementation against it.
38
+ *
39
+ * @example
40
+ * import { memoryAdapter } from "@shadow-js/realtime/server";
41
+ *
42
+ * const adapter = memoryAdapter();
43
+ * const { seq } = await adapter.publish(
44
+ * "chat:lobby",
45
+ * { event: "message", data: '"hi"' },
46
+ * { history: 100, historyTtlMs: 60_000, idempotencyTtlMs: 60_000 },
47
+ * );
48
+ * seq; // 1
49
+ * @since 1.0.0
50
+ */
51
+ export interface Adapter {
52
+ /** Assigns the next seq, retains the message, delivers it to every subscriber on every server. */
53
+ publish(channel: string, entry: StoredEntry, options: PublishOptions): Promise<PublishResult>;
54
+ /** Listens to a channel's live messages (in increasing seq order). */
55
+ subscribe(channel: string, listener: (message: StoredMessage) => void): Promise<Unsubscribe>;
56
+ /** Reads retained messages after a seq. */
57
+ history(channel: string, after: number, limit: number): Promise<HistoryResult>;
58
+ /** Adds or updates a presence member, with an expiry. Emits `join` for a new member and `set` for a newer version. */
59
+ presenceSet(channel: string, member: Member, ttlMs: number): Promise<void>;
60
+ /** Removes a member. Resolves true (and emits `leave`) only for the call that removed it. */
61
+ presenceRemove(channel: string, id: string): Promise<boolean>;
62
+ /** The live members of a channel. */
63
+ presenceList(channel: string): Promise<readonly Member[]>;
64
+ /** Refreshes the expiry of members; returns the entries that no longer exist (the caller sets them again). */
65
+ presenceTouch(entries: readonly PresenceRef[], ttlMs: number): Promise<readonly PresenceRef[]>;
66
+ /** Removes the expired members of a channel, emitting one `leave` for each; returns them. */
67
+ presenceReap(channel: string): Promise<readonly Member[]>;
68
+ /** Listens to a channel's presence changes. */
69
+ presenceSubscribe(channel: string, listener: (event: PresenceEvent) => void): Promise<Unsubscribe>;
70
+ /** Releases connections and timers. */
71
+ close(): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { ChannelSchema, Realtime, RealtimeOptions } from "./types.js";
2
+ /**
3
+ * Creates the realtime client: one transport (WebSocket, falling back to server-sent ev), reconnects with backoff, a
4
+ * bounded offline queue, resume from the last sequence number and typed channels. It connects when the first channel
5
+ * listens or the first message is sent, never earlier.
6
+ *
7
+ * @param options The stream URL and the policy.
8
+ * @returns The client.
9
+ * @example
10
+ * import { createRealtime } from "@shadow-js/realtime/client";
11
+ *
12
+ * type Schema = { [name: `chat:${string}`]: { ev: { message: { text: string } } } };
13
+ * const rt = createRealtime<Schema>({ url: "/rt" });
14
+ * const lobby = rt.channel("chat:lobby");
15
+ * const stop = lobby.on("message", (data) => console.log(data.text));
16
+ * await lobby.publish("message", { text: "hello" });
17
+ * stop();
18
+ * rt.close();
19
+ * @since 1.0.0
20
+ */
21
+ export declare function createRealtime<Schema extends ChannelSchema = ChannelSchema>(options: RealtimeOptions): Realtime<Schema>;
22
+ //# sourceMappingURL=core.d.ts.map