@shadow-js/realtime 0.0.0-stage → 1.0.0
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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +114 -2
- package/dist/dev/adapter.d.ts +75 -0
- package/dist/dev/client/core.d.ts +24 -0
- package/dist/dev/client/core.js +845 -0
- package/dist/dev/client/core.js.map +1 -0
- package/dist/dev/client/index.d.ts +5 -0
- package/dist/dev/client/index.js +3 -0
- package/dist/dev/client/presence.js +23 -0
- package/dist/dev/client/presence.js.map +1 -0
- package/dist/dev/client/sse.js +76 -0
- package/dist/dev/client/sse.js.map +1 -0
- package/dist/dev/client/types.d.ts +256 -0
- package/dist/dev/client/util.js +30 -0
- package/dist/dev/client/util.js.map +1 -0
- package/dist/dev/client/ws.js +39 -0
- package/dist/dev/client/ws.js.map +1 -0
- package/dist/dev/cloudflare/adapter.d.ts +80 -0
- package/dist/dev/cloudflare/adapter.js +347 -0
- package/dist/dev/cloudflare/adapter.js.map +1 -0
- package/dist/dev/cloudflare/core.js +701 -0
- package/dist/dev/cloudflare/core.js.map +1 -0
- package/dist/dev/cloudflare/link.js +397 -0
- package/dist/dev/cloudflare/link.js.map +1 -0
- package/dist/dev/cloudflare/object.d.ts +82 -0
- package/dist/dev/cloudflare/object.js +214 -0
- package/dist/dev/cloudflare/object.js.map +1 -0
- package/dist/dev/cloudflare/types.d.ts +100 -0
- package/dist/dev/cloudflare/wire.js +284 -0
- package/dist/dev/cloudflare/wire.js.map +1 -0
- package/dist/dev/cloudflare.d.ts +4 -0
- package/dist/dev/cloudflare.js +3 -0
- package/dist/dev/errors.d.ts +25 -0
- package/dist/dev/errors.js +24 -0
- package/dist/dev/errors.js.map +1 -0
- package/dist/dev/hooks.d.ts +110 -0
- package/dist/dev/hooks.js +162 -0
- package/dist/dev/hooks.js.map +1 -0
- package/dist/dev/index.d.ts +4 -0
- package/dist/dev/index.js +3 -0
- package/dist/dev/memory.d.ts +33 -0
- package/dist/dev/memory.js +255 -0
- package/dist/dev/memory.js.map +1 -0
- package/dist/dev/protocol.d.ts +211 -0
- package/dist/dev/protocol.js +400 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/redis/client.d.ts +139 -0
- package/dist/dev/redis/client.js +146 -0
- package/dist/dev/redis/client.js.map +1 -0
- package/dist/dev/redis/scripts.js +275 -0
- package/dist/dev/redis/scripts.js.map +1 -0
- package/dist/dev/redis/topics.d.ts +8 -0
- package/dist/dev/redis/topics.js +377 -0
- package/dist/dev/redis/topics.js.map +1 -0
- package/dist/dev/redis.d.ts +68 -0
- package/dist/dev/redis.js +422 -0
- package/dist/dev/redis.js.map +1 -0
- package/dist/dev/server/http.d.ts +48 -0
- package/dist/dev/server/http.js +286 -0
- package/dist/dev/server/http.js.map +1 -0
- package/dist/dev/server/hub.d.ts +114 -0
- package/dist/dev/server/hub.js +827 -0
- package/dist/dev/server/hub.js.map +1 -0
- package/dist/dev/server/index.d.ts +47 -0
- package/dist/dev/server/index.js +41 -0
- package/dist/dev/server/index.js.map +1 -0
- package/dist/dev/server/limits.d.ts +54 -0
- package/dist/dev/server/limits.js +85 -0
- package/dist/dev/server/limits.js.map +1 -0
- package/dist/dev/server/rules.d.ts +43 -0
- package/dist/dev/server/rules.js +72 -0
- package/dist/dev/server/rules.js.map +1 -0
- package/dist/dev/testing.d.ts +115 -0
- package/dist/dev/testing.js +427 -0
- package/dist/dev/testing.js.map +1 -0
- package/dist/prod/client/core.js +2 -0
- package/dist/prod/client/core.js.map +1 -0
- package/dist/prod/client/index.js +1 -0
- package/dist/prod/client/presence.js +2 -0
- package/dist/prod/client/presence.js.map +1 -0
- package/dist/prod/client/sse.js +5 -0
- package/dist/prod/client/sse.js.map +1 -0
- package/dist/prod/client/util.js +2 -0
- package/dist/prod/client/util.js.map +1 -0
- package/dist/prod/client/ws.js +2 -0
- package/dist/prod/client/ws.js.map +1 -0
- package/dist/prod/cloudflare/adapter.js +3 -0
- package/dist/prod/cloudflare/adapter.js.map +1 -0
- package/dist/prod/cloudflare/core.js +2 -0
- package/dist/prod/cloudflare/core.js.map +1 -0
- package/dist/prod/cloudflare/link.js +2 -0
- package/dist/prod/cloudflare/link.js.map +1 -0
- package/dist/prod/cloudflare/object.js +2 -0
- package/dist/prod/cloudflare/object.js.map +1 -0
- package/dist/prod/cloudflare/wire.js +2 -0
- package/dist/prod/cloudflare/wire.js.map +1 -0
- package/dist/prod/cloudflare.js +1 -0
- package/dist/prod/errors.js +2 -0
- package/dist/prod/errors.js.map +1 -0
- package/dist/prod/hooks.js +2 -0
- package/dist/prod/hooks.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/memory.js +2 -0
- package/dist/prod/memory.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/redis/client.js +2 -0
- package/dist/prod/redis/client.js.map +1 -0
- package/dist/prod/redis/scripts.js +157 -0
- package/dist/prod/redis/scripts.js.map +1 -0
- package/dist/prod/redis/topics.js +2 -0
- package/dist/prod/redis/topics.js.map +1 -0
- package/dist/prod/redis.js +2 -0
- package/dist/prod/redis.js.map +1 -0
- package/dist/prod/server/http.js +2 -0
- package/dist/prod/server/http.js.map +1 -0
- package/dist/prod/server/hub.js +2 -0
- package/dist/prod/server/hub.js.map +1 -0
- package/dist/prod/server/index.js +2 -0
- package/dist/prod/server/index.js.map +1 -0
- package/dist/prod/server/limits.js +2 -0
- package/dist/prod/server/limits.js.map +1 -0
- package/dist/prod/server/rules.js +2 -0
- package/dist/prod/server/rules.js.map +1 -0
- package/dist/prod/testing.js +2 -0
- package/dist/prod/testing.js.map +1 -0
- package/package.json +105 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The codes an error of this package carries. They are the same strings the wire protocol uses in `err` frames.
|
|
4
|
+
* @since 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
export type ErrorCode = "bad_frame" | "bad_channel" | "unauthorized" | "forbidden" | "not_subscribed" | "too_large" | "rate_limited" | "limit" | "timeout" | "unavailable" | "no_handler" | "handler_error" | "closed" | "queue_full" | "conflict";
|
|
7
|
+
/**
|
|
8
|
+
* An error with a protocol code. A request handler that throws one answers the client with its code and message; any
|
|
9
|
+
* other error answers `handler_error` with no message, so internals never reach a client.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import { RealtimeError } from "@shadow-js/realtime";
|
|
13
|
+
*
|
|
14
|
+
* const refusal = new RealtimeError("forbidden", "only moderators can pin messages");
|
|
15
|
+
* refusal.code; // "forbidden"
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
*/
|
|
18
|
+
export declare class RealtimeError extends Error {
|
|
19
|
+
readonly code: ErrorCode;
|
|
20
|
+
constructor(code: ErrorCode, message?: string, options?: {
|
|
21
|
+
readonly cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* An error with a protocol code. A request handler that throws one answers the client with its code and message; any
|
|
4
|
+
* other error answers `handler_error` with no message, so internals never reach a client.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { RealtimeError } from "@shadow-js/realtime";
|
|
8
|
+
*
|
|
9
|
+
* const refusal = new RealtimeError("forbidden", "only moderators can pin messages");
|
|
10
|
+
* refusal.code; // "forbidden"
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
var RealtimeError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message, options) {
|
|
16
|
+
super(message ?? code, options);
|
|
17
|
+
this.name = "RealtimeError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { RealtimeError };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/errors.ts"],"sourcesContent":["/**\n * The codes an error of this package carries. They are the same strings the wire protocol uses in `err` frames.\n * @since 1.0.0\n */\nexport type ErrorCode =\n | \"bad_frame\"\n | \"bad_channel\"\n | \"unauthorized\"\n | \"forbidden\"\n | \"not_subscribed\"\n | \"too_large\"\n | \"rate_limited\"\n | \"limit\"\n | \"timeout\"\n | \"unavailable\"\n | \"no_handler\"\n | \"handler_error\"\n | \"closed\"\n | \"queue_full\"\n | \"conflict\";\n\n/**\n * An error with a protocol code. A request handler that throws one answers the client with its code and message; any\n * other error answers `handler_error` with no message, so internals never reach a client.\n *\n * @example\n * import { RealtimeError } from \"@shadow-js/realtime\";\n *\n * const refusal = new RealtimeError(\"forbidden\", \"only moderators can pin messages\");\n * refusal.code; // \"forbidden\"\n * @since 1.0.0\n */\nexport class RealtimeError extends Error {\n readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message?: string, options?: { readonly cause?: unknown }) {\n super(message ?? code, options);\n this.name = \"RealtimeError\";\n this.code = code;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAgCA,IAAa,gBAAb,cAAmC,MAAM;CACvC;CAEA,YAAY,MAAiB,SAAkB,SAAwC;EACrF,MAAM,WAAW,MAAM,OAAO;EAC9B,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Member } from "./protocol.js";
|
|
2
|
+
import { AnyMessage, Channel, ChannelDef, ChannelOptions, ChannelSchema, ChannelStatus, MessageMeta, PresenceOf, Realtime, RequestData, RequestReply, RequestsOf, Status } from "./client/types.js";
|
|
3
|
+
import { Accessor } from "@shadow-js/core";
|
|
4
|
+
//#region src/hooks.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What {@link useChannel} returns: the reactive state of a channel and the calls on it.
|
|
7
|
+
* @since 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
export interface ChannelHandle<Def extends ChannelDef> {
|
|
10
|
+
/** The channel's status, as a signal. */
|
|
11
|
+
readonly status: Accessor<ChannelStatus>;
|
|
12
|
+
/** The latest message of any event, as a signal (undefined until one arrives). */
|
|
13
|
+
readonly lastMessage: Accessor<AnyMessage<Def> | undefined>;
|
|
14
|
+
/** Publishes a message; see `Channel.publish`. */
|
|
15
|
+
readonly publish: Channel<Def>["publish"];
|
|
16
|
+
/** Sends a request to the server handler; see `Channel.request`. */
|
|
17
|
+
readonly request: <E extends keyof RequestsOf<Def> & string>(event: E, data: RequestData<Def, E>, options?: {
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
}) => Promise<RequestReply<Def, E>>;
|
|
20
|
+
/** The channel itself, for `onGap`, `presence` and the other calls. */
|
|
21
|
+
readonly channel: Channel<Def>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Opens a channel for the owning component and releases it when the component is disposed: `status` and `lastMessage` are
|
|
25
|
+
* signals, `publish` and `request` act on the channel. The channel subscribes while the component lives; other components may
|
|
26
|
+
* share it. During the server render nothing connects.
|
|
27
|
+
*
|
|
28
|
+
* @param rt The client from `createRealtime`.
|
|
29
|
+
* @param name The channel name (fixed for the life of the component; key the component to change it).
|
|
30
|
+
* @param options Echo, presence and replay, applied when the channel is first created.
|
|
31
|
+
* @returns The reactive handle.
|
|
32
|
+
* @example
|
|
33
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
34
|
+
* import { useChannel } from "@shadow-js/realtime/hooks";
|
|
35
|
+
*
|
|
36
|
+
* type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };
|
|
37
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
38
|
+
*
|
|
39
|
+
* function Lobby() {
|
|
40
|
+
* const lobby = useChannel(rt, "chat:lobby");
|
|
41
|
+
* return <button onClick={() => lobby.publish("message", { text: "hi" })}>{lobby.status()}</button>;
|
|
42
|
+
* }
|
|
43
|
+
* @since 1.0.0
|
|
44
|
+
*/
|
|
45
|
+
export declare function useChannel<Schema extends ChannelSchema, Name extends keyof Schema & string>(rt: Realtime<Schema>, name: Name, options?: ChannelOptions<PresenceOf<Schema[Name]>>): ChannelHandle<Schema[Name]>;
|
|
46
|
+
/**
|
|
47
|
+
* Calls `handler` for each message of `event` on `channel` while the owning component lives.
|
|
48
|
+
*
|
|
49
|
+
* @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.
|
|
50
|
+
* @param event The event name.
|
|
51
|
+
* @param handler Called with the data and the message meta.
|
|
52
|
+
* @example
|
|
53
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
54
|
+
* import { useChannel, useChannelEvent } from "@shadow-js/realtime/hooks";
|
|
55
|
+
*
|
|
56
|
+
* type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };
|
|
57
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
58
|
+
*
|
|
59
|
+
* function Log() {
|
|
60
|
+
* const { channel } = useChannel(rt, "chat:lobby");
|
|
61
|
+
* useChannelEvent(channel, "message", (data) => console.log(data.text));
|
|
62
|
+
* return <p>listening</p>;
|
|
63
|
+
* }
|
|
64
|
+
* @since 1.0.0
|
|
65
|
+
*/
|
|
66
|
+
export declare function useChannelEvent<Def extends ChannelDef, E extends keyof Def["events"] & string>(channel: Channel<Def>, event: E, handler: (data: Def["events"][E], meta: MessageMeta) => void): void;
|
|
67
|
+
/**
|
|
68
|
+
* The presence set of `channel` as a signal, and a setter that joins it (or changes this client's state) from the owning
|
|
69
|
+
* component. Passing `undefined` leaves. The component leaves the set when it is disposed.
|
|
70
|
+
*
|
|
71
|
+
* @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.
|
|
72
|
+
* @returns The members and `setPresence`.
|
|
73
|
+
* @example
|
|
74
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
75
|
+
* import { useChannel, useChannelPresence } from "@shadow-js/realtime/hooks";
|
|
76
|
+
*
|
|
77
|
+
* type Schema = {
|
|
78
|
+
* [name: `chat:${string}`]: { events: { message: string }; presence: { name: string } };
|
|
79
|
+
* };
|
|
80
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
81
|
+
*
|
|
82
|
+
* function Who() {
|
|
83
|
+
* const { channel } = useChannel(rt, "chat:lobby");
|
|
84
|
+
* const [members, setPresence] = useChannelPresence(channel);
|
|
85
|
+
* setPresence({ name: "ann" });
|
|
86
|
+
* return <p>{members().length} here</p>;
|
|
87
|
+
* }
|
|
88
|
+
* @since 1.0.0
|
|
89
|
+
*/
|
|
90
|
+
export declare function useChannelPresence<Def extends ChannelDef>(channel: Channel<Def>): readonly [Accessor<readonly Member<PresenceOf<Def>>[]>, (state: PresenceOf<Def> | undefined) => void];
|
|
91
|
+
/**
|
|
92
|
+
* The connection status of `rt` as a signal.
|
|
93
|
+
*
|
|
94
|
+
* @param rt The client from `createRealtime`.
|
|
95
|
+
* @returns The status accessor.
|
|
96
|
+
* @example
|
|
97
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
98
|
+
* import { useRealtimeStatus } from "@shadow-js/realtime/hooks";
|
|
99
|
+
*
|
|
100
|
+
* const rt = createRealtime({ url: "/rt" });
|
|
101
|
+
*
|
|
102
|
+
* function Badge() {
|
|
103
|
+
* const status = useRealtimeStatus(rt);
|
|
104
|
+
* return <span>{status()}</span>;
|
|
105
|
+
* }
|
|
106
|
+
* @since 1.0.0
|
|
107
|
+
*/
|
|
108
|
+
export declare function useRealtimeStatus<Schema extends ChannelSchema>(rt: Realtime<Schema>): Accessor<Status>;
|
|
109
|
+
//#endregion
|
|
110
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { RealtimeError } from "./errors.js";
|
|
2
|
+
import { isServer, onCleanup, useSignal } from "@shadow-js/core";
|
|
3
|
+
//#region src/hooks.ts
|
|
4
|
+
/** A channel for the server render: it never connects, listens or publishes. */
|
|
5
|
+
function inertChannel(name) {
|
|
6
|
+
const refuse = () => Promise.reject(new RealtimeError("closed", "a realtime channel is not available during the server render"));
|
|
7
|
+
const none = () => () => void 0;
|
|
8
|
+
return {
|
|
9
|
+
name,
|
|
10
|
+
status: "idle",
|
|
11
|
+
lastSeq: 0,
|
|
12
|
+
on: none,
|
|
13
|
+
onAny: none,
|
|
14
|
+
onGap: none,
|
|
15
|
+
onStatus: none,
|
|
16
|
+
publish: refuse,
|
|
17
|
+
request: refuse,
|
|
18
|
+
presence: {
|
|
19
|
+
members: () => [],
|
|
20
|
+
set: () => void 0,
|
|
21
|
+
leave: () => void 0,
|
|
22
|
+
on: none
|
|
23
|
+
},
|
|
24
|
+
close: () => void 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Opens a channel for the owning component and releases it when the component is disposed: `status` and `lastMessage` are
|
|
29
|
+
* signals, `publish` and `request` act on the channel. The channel subscribes while the component lives; other components may
|
|
30
|
+
* share it. During the server render nothing connects.
|
|
31
|
+
*
|
|
32
|
+
* @param rt The client from `createRealtime`.
|
|
33
|
+
* @param name The channel name (fixed for the life of the component; key the component to change it).
|
|
34
|
+
* @param options Echo, presence and replay, applied when the channel is first created.
|
|
35
|
+
* @returns The reactive handle.
|
|
36
|
+
* @example
|
|
37
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
38
|
+
* import { useChannel } from "@shadow-js/realtime/hooks";
|
|
39
|
+
*
|
|
40
|
+
* type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };
|
|
41
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
42
|
+
*
|
|
43
|
+
* function Lobby() {
|
|
44
|
+
* const lobby = useChannel(rt, "chat:lobby");
|
|
45
|
+
* return <button onClick={() => lobby.publish("message", { text: "hi" })}>{lobby.status()}</button>;
|
|
46
|
+
* }
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
*/
|
|
49
|
+
function useChannel(rt, name, options) {
|
|
50
|
+
const channel = isServer ? inertChannel(name) : rt.channel(name, options);
|
|
51
|
+
const [status, setStatus] = useSignal(channel.status);
|
|
52
|
+
const [lastMessage, setLastMessage] = useSignal(void 0);
|
|
53
|
+
if (!isServer) {
|
|
54
|
+
const stopStatus = channel.onStatus((next) => setStatus(next));
|
|
55
|
+
const stopMessages = channel.onAny((message) => setLastMessage(() => message));
|
|
56
|
+
onCleanup(() => {
|
|
57
|
+
stopStatus();
|
|
58
|
+
stopMessages();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
status,
|
|
63
|
+
lastMessage,
|
|
64
|
+
publish: (event, data, publishOptions) => channel.publish(event, data, publishOptions),
|
|
65
|
+
request: (event, data, requestOptions) => channel.request(event, data, requestOptions),
|
|
66
|
+
channel
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Calls `handler` for each message of `event` on `channel` while the owning component lives.
|
|
71
|
+
*
|
|
72
|
+
* @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.
|
|
73
|
+
* @param event The event name.
|
|
74
|
+
* @param handler Called with the data and the message meta.
|
|
75
|
+
* @example
|
|
76
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
77
|
+
* import { useChannel, useChannelEvent } from "@shadow-js/realtime/hooks";
|
|
78
|
+
*
|
|
79
|
+
* type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };
|
|
80
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
81
|
+
*
|
|
82
|
+
* function Log() {
|
|
83
|
+
* const { channel } = useChannel(rt, "chat:lobby");
|
|
84
|
+
* useChannelEvent(channel, "message", (data) => console.log(data.text));
|
|
85
|
+
* return <p>listening</p>;
|
|
86
|
+
* }
|
|
87
|
+
* @since 1.0.0
|
|
88
|
+
*/
|
|
89
|
+
function useChannelEvent(channel, event, handler) {
|
|
90
|
+
if (isServer) return;
|
|
91
|
+
onCleanup(channel.on(event, handler));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The presence set of `channel` as a signal, and a setter that joins it (or changes this client's state) from the owning
|
|
95
|
+
* component. Passing `undefined` leaves. The component leaves the set when it is disposed.
|
|
96
|
+
*
|
|
97
|
+
* @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.
|
|
98
|
+
* @returns The members and `setPresence`.
|
|
99
|
+
* @example
|
|
100
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
101
|
+
* import { useChannel, useChannelPresence } from "@shadow-js/realtime/hooks";
|
|
102
|
+
*
|
|
103
|
+
* type Schema = {
|
|
104
|
+
* [name: `chat:${string}`]: { events: { message: string }; presence: { name: string } };
|
|
105
|
+
* };
|
|
106
|
+
* const rt = createRealtime<Schema>({ url: "/rt" });
|
|
107
|
+
*
|
|
108
|
+
* function Who() {
|
|
109
|
+
* const { channel } = useChannel(rt, "chat:lobby");
|
|
110
|
+
* const [members, setPresence] = useChannelPresence(channel);
|
|
111
|
+
* setPresence({ name: "ann" });
|
|
112
|
+
* return <p>{members().length} here</p>;
|
|
113
|
+
* }
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
*/
|
|
116
|
+
function useChannelPresence(channel) {
|
|
117
|
+
const [members, setMembers] = useSignal(channel.presence.members());
|
|
118
|
+
let joined = false;
|
|
119
|
+
if (!isServer) {
|
|
120
|
+
const stop = channel.presence.on((list) => setMembers(() => list));
|
|
121
|
+
onCleanup(() => {
|
|
122
|
+
stop();
|
|
123
|
+
if (joined) channel.presence.leave();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return [members, (state) => {
|
|
127
|
+
if (isServer) return;
|
|
128
|
+
if (state === void 0) {
|
|
129
|
+
joined = false;
|
|
130
|
+
channel.presence.leave();
|
|
131
|
+
} else {
|
|
132
|
+
joined = true;
|
|
133
|
+
channel.presence.set(state);
|
|
134
|
+
}
|
|
135
|
+
}];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The connection status of `rt` as a signal.
|
|
139
|
+
*
|
|
140
|
+
* @param rt The client from `createRealtime`.
|
|
141
|
+
* @returns The status accessor.
|
|
142
|
+
* @example
|
|
143
|
+
* import { createRealtime } from "@shadow-js/realtime/client";
|
|
144
|
+
* import { useRealtimeStatus } from "@shadow-js/realtime/hooks";
|
|
145
|
+
*
|
|
146
|
+
* const rt = createRealtime({ url: "/rt" });
|
|
147
|
+
*
|
|
148
|
+
* function Badge() {
|
|
149
|
+
* const status = useRealtimeStatus(rt);
|
|
150
|
+
* return <span>{status()}</span>;
|
|
151
|
+
* }
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
*/
|
|
154
|
+
function useRealtimeStatus(rt) {
|
|
155
|
+
const [status, setStatus] = useSignal(rt.status);
|
|
156
|
+
if (!isServer) onCleanup(rt.onStatus((next) => setStatus(next)));
|
|
157
|
+
return status;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { useChannel, useChannelEvent, useChannelPresence, useRealtimeStatus };
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../../src/hooks.ts"],"sourcesContent":["import type { Accessor } from \"@shadow-js/core\";\nimport { isServer, onCleanup, useSignal } from \"@shadow-js/core\";\nimport type {\n AnyMessage,\n Channel,\n ChannelDef,\n ChannelOptions,\n ChannelSchema,\n ChannelStatus,\n MessageMeta,\n PresenceOf,\n Realtime,\n RequestData,\n RequestReply,\n RequestsOf,\n Status,\n} from \"./client/types\";\nimport { RealtimeError } from \"./errors\";\nimport type { Member } from \"./protocol\";\n\n/**\n * What {@link useChannel} returns: the reactive state of a channel and the calls on it.\n * @since 1.0.0\n */\nexport interface ChannelHandle<Def extends ChannelDef> {\n /** The channel's status, as a signal. */\n readonly status: Accessor<ChannelStatus>;\n /** The latest message of any event, as a signal (undefined until one arrives). */\n readonly lastMessage: Accessor<AnyMessage<Def> | undefined>;\n /** Publishes a message; see `Channel.publish`. */\n readonly publish: Channel<Def>[\"publish\"];\n /** Sends a request to the server handler; see `Channel.request`. */\n readonly request: <E extends keyof RequestsOf<Def> & string>(\n event: E,\n data: RequestData<Def, E>,\n options?: { readonly timeoutMs?: number },\n ) => Promise<RequestReply<Def, E>>;\n /** The channel itself, for `onGap`, `presence` and the other calls. */\n readonly channel: Channel<Def>;\n}\n\n/** A channel for the server render: it never connects, listens or publishes. */\nfunction inertChannel<Def extends ChannelDef>(name: string): Channel<Def> {\n const refuse = (): Promise<never> =>\n Promise.reject(\n new RealtimeError(\"closed\", \"a realtime channel is not available during the server render\"),\n );\n const none = (): (() => void) => () => undefined;\n return {\n name,\n status: \"idle\",\n lastSeq: 0,\n on: none,\n onAny: none,\n onGap: none,\n onStatus: none,\n publish: refuse,\n request: refuse,\n presence: { members: () => [], set: () => undefined, leave: () => undefined, on: none },\n close: () => undefined,\n };\n}\n\n/**\n * Opens a channel for the owning component and releases it when the component is disposed: `status` and `lastMessage` are\n * signals, `publish` and `request` act on the channel. The channel subscribes while the component lives; other components may\n * share it. During the server render nothing connects.\n *\n * @param rt The client from `createRealtime`.\n * @param name The channel name (fixed for the life of the component; key the component to change it).\n * @param options Echo, presence and replay, applied when the channel is first created.\n * @returns The reactive handle.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Lobby() {\n * const lobby = useChannel(rt, \"chat:lobby\");\n * return <button onClick={() => lobby.publish(\"message\", { text: \"hi\" })}>{lobby.status()}</button>;\n * }\n * @since 1.0.0\n */\nexport function useChannel<Schema extends ChannelSchema, Name extends keyof Schema & string>(\n rt: Realtime<Schema>,\n name: Name,\n options?: ChannelOptions<PresenceOf<Schema[Name]>>,\n): ChannelHandle<Schema[Name]> {\n const channel: Channel<Schema[Name]> = isServer\n ? inertChannel<Schema[Name]>(name)\n : rt.channel(name, options);\n const [status, setStatus] = useSignal<ChannelStatus>(channel.status);\n const [lastMessage, setLastMessage] = useSignal<AnyMessage<Schema[Name]> | undefined>(undefined);\n if (!isServer) {\n const stopStatus = channel.onStatus((next) => setStatus(next));\n const stopMessages = channel.onAny((message) => setLastMessage(() => message));\n onCleanup(() => {\n stopStatus();\n stopMessages();\n });\n }\n return {\n status,\n lastMessage,\n publish: (event, data, publishOptions) => channel.publish(event, data, publishOptions),\n request: (event, data, requestOptions) => channel.request(event, data, requestOptions),\n channel,\n };\n}\n\n/**\n * Calls `handler` for each message of `event` on `channel` while the owning component lives.\n *\n * @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.\n * @param event The event name.\n * @param handler Called with the data and the message meta.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel, useChannelEvent } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = { [name: `chat:${string}`]: { events: { message: { text: string } } } };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Log() {\n * const { channel } = useChannel(rt, \"chat:lobby\");\n * useChannelEvent(channel, \"message\", (data) => console.log(data.text));\n * return <p>listening</p>;\n * }\n * @since 1.0.0\n */\nexport function useChannelEvent<Def extends ChannelDef, E extends keyof Def[\"events\"] & string>(\n channel: Channel<Def>,\n event: E,\n handler: (data: Def[\"events\"][E], meta: MessageMeta) => void,\n): void {\n if (isServer) return;\n onCleanup(channel.on(event, handler));\n}\n\n/**\n * The presence set of `channel` as a signal, and a setter that joins it (or changes this client's state) from the owning\n * component. Passing `undefined` leaves. The component leaves the set when it is disposed.\n *\n * @param channel A channel from `useChannel(...).channel` or `rt.channel(...)`.\n * @returns The members and `setPresence`.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useChannel, useChannelPresence } from \"@shadow-js/realtime/hooks\";\n *\n * type Schema = {\n * [name: `chat:${string}`]: { events: { message: string }; presence: { name: string } };\n * };\n * const rt = createRealtime<Schema>({ url: \"/rt\" });\n *\n * function Who() {\n * const { channel } = useChannel(rt, \"chat:lobby\");\n * const [members, setPresence] = useChannelPresence(channel);\n * setPresence({ name: \"ann\" });\n * return <p>{members().length} here</p>;\n * }\n * @since 1.0.0\n */\nexport function useChannelPresence<Def extends ChannelDef>(\n channel: Channel<Def>,\n): readonly [\n Accessor<readonly Member<PresenceOf<Def>>[]>,\n (state: PresenceOf<Def> | undefined) => void,\n] {\n const [members, setMembers] = useSignal<readonly Member<PresenceOf<Def>>[]>(\n channel.presence.members(),\n );\n let joined = false;\n if (!isServer) {\n const stop = channel.presence.on((list) => setMembers(() => list));\n onCleanup(() => {\n stop();\n if (joined) channel.presence.leave();\n });\n }\n return [\n members,\n (state) => {\n if (isServer) return;\n if (state === undefined) {\n joined = false;\n channel.presence.leave();\n } else {\n joined = true;\n channel.presence.set(state);\n }\n },\n ];\n}\n\n/**\n * The connection status of `rt` as a signal.\n *\n * @param rt The client from `createRealtime`.\n * @returns The status accessor.\n * @example\n * import { createRealtime } from \"@shadow-js/realtime/client\";\n * import { useRealtimeStatus } from \"@shadow-js/realtime/hooks\";\n *\n * const rt = createRealtime({ url: \"/rt\" });\n *\n * function Badge() {\n * const status = useRealtimeStatus(rt);\n * return <span>{status()}</span>;\n * }\n * @since 1.0.0\n */\nexport function useRealtimeStatus<Schema extends ChannelSchema>(\n rt: Realtime<Schema>,\n): Accessor<Status> {\n const [status, setStatus] = useSignal<Status>(rt.status);\n if (!isServer) onCleanup(rt.onStatus((next) => setStatus(next)));\n return status;\n}\n"],"mappings":";;;;AA0CA,SAAS,aAAqC,MAA4B;CACxE,MAAM,eACJ,QAAQ,OACN,IAAI,cAAc,UAAU,8DAA8D,CAC5F;CACF,MAAM,mBAAiC,KAAA;CACvC,OAAO;EACL;EACA,QAAQ;EACR,SAAS;EACT,IAAI;EACJ,OAAO;EACP,OAAO;EACP,UAAU;EACV,SAAS;EACT,SAAS;EACT,UAAU;GAAE,eAAe,CAAC;GAAG,WAAW,KAAA;GAAW,aAAa,KAAA;GAAW,IAAI;EAAK;EACtF,aAAa,KAAA;CACf;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,WACd,IACA,MACA,SAC6B;CAC7B,MAAM,UAAiC,WACnC,aAA2B,IAAI,IAC/B,GAAG,QAAQ,MAAM,OAAO;CAC5B,MAAM,CAAC,QAAQ,aAAa,UAAyB,QAAQ,MAAM;CACnE,MAAM,CAAC,aAAa,kBAAkB,UAAgD,KAAA,CAAS;CAC/F,IAAI,CAAC,UAAU;EACb,MAAM,aAAa,QAAQ,UAAU,SAAS,UAAU,IAAI,CAAC;EAC7D,MAAM,eAAe,QAAQ,OAAO,YAAY,qBAAqB,OAAO,CAAC;EAC7E,gBAAgB;GACd,WAAW;GACX,aAAa;EACf,CAAC;CACH;CACA,OAAO;EACL;EACA;EACA,UAAU,OAAO,MAAM,mBAAmB,QAAQ,QAAQ,OAAO,MAAM,cAAc;EACrF,UAAU,OAAO,MAAM,mBAAmB,QAAQ,QAAQ,OAAO,MAAM,cAAc;EACrF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,gBACd,SACA,OACA,SACM;CACN,IAAI,UAAU;CACd,UAAU,QAAQ,GAAG,OAAO,OAAO,CAAC;AACtC;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,mBACd,SAIA;CACA,MAAM,CAAC,SAAS,cAAc,UAC5B,QAAQ,SAAS,QAAQ,CAC3B;CACA,IAAI,SAAS;CACb,IAAI,CAAC,UAAU;EACb,MAAM,OAAO,QAAQ,SAAS,IAAI,SAAS,iBAAiB,IAAI,CAAC;EACjE,gBAAgB;GACd,KAAK;GACL,IAAI,QAAQ,QAAQ,SAAS,MAAM;EACrC,CAAC;CACH;CACA,OAAO,CACL,UACC,UAAU;EACT,IAAI,UAAU;EACd,IAAI,UAAU,KAAA,GAAW;GACvB,SAAS;GACT,QAAQ,SAAS,MAAM;EACzB,OAAO;GACL,SAAS;GACT,QAAQ,SAAS,IAAI,KAAK;EAC5B;CACF,CACF;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,kBACd,IACkB;CAClB,MAAM,CAAC,QAAQ,aAAa,UAAkB,GAAG,MAAM;CACvD,IAAI,CAAC,UAAU,UAAU,GAAG,UAAU,SAAS,UAAU,IAAI,CAAC,CAAC;CAC/D,OAAO;AACT"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ErrorCode, RealtimeError } from "./errors.js";
|
|
2
|
+
import { ClientFrame, DecodedClientFrame, Json, Member, PROTOCOL_VERSION, PresenceEvent, ServerFrame, StoredEntry, StoredMessage, decodeClientFrame, decodeServerFrame, isChannelName, isEventName } from "./protocol.js";
|
|
3
|
+
import { Adapter, HistoryResult, PresenceRef, PublishOptions, PublishResult, Unsubscribe } from "./adapter.js";
|
|
4
|
+
export { type Adapter, type ClientFrame, type DecodedClientFrame, type ErrorCode, type HistoryResult, type Json, type Member, PROTOCOL_VERSION, type PresenceEvent, type PresenceRef, type PublishOptions, type PublishResult, RealtimeError, type ServerFrame, type StoredEntry, type StoredMessage, type Unsubscribe, decodeClientFrame, decodeServerFrame, isChannelName, isEventName };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Adapter } from "./adapter.js";
|
|
2
|
+
//#region src/memory.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Options of {@link memoryAdapter}.
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
export interface MemoryAdapterOptions {
|
|
8
|
+
/** The clock, in epoch milliseconds. Default: `Date.now`. */
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
/**
|
|
11
|
+
* Milliseconds a channel with no listener and no retained message is remembered. After that its head is forgotten and its
|
|
12
|
+
* numbering restarts, which a resuming client sees as `reset`. Default: one hour.
|
|
13
|
+
*/
|
|
14
|
+
readonly idleMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
|
|
18
|
+
* of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis
|
|
19
|
+
* or Cloudflare adapter when more than one process serves clients.
|
|
20
|
+
*
|
|
21
|
+
* @param options The clock.
|
|
22
|
+
* @returns The adapter.
|
|
23
|
+
* @example
|
|
24
|
+
* import { createHub, memoryAdapter } from "@shadow-js/realtime/server";
|
|
25
|
+
*
|
|
26
|
+
* const adapter = memoryAdapter();
|
|
27
|
+
* const hub = createHub({ adapter });
|
|
28
|
+
* void hub.close();
|
|
29
|
+
* @since 1.0.0
|
|
30
|
+
*/
|
|
31
|
+
export declare function memoryAdapter(options?: MemoryAdapterOptions): Adapter;
|
|
32
|
+
//#endregion
|
|
33
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
//#region src/memory.ts
|
|
2
|
+
/**
|
|
3
|
+
* The single-process adapter, and the default of `createHub`. Messages, history, idempotency keys and presence live in memory
|
|
4
|
+
* of this process; several hubs created over one adapter object share them, which is how tests exercise two servers. Use the Redis
|
|
5
|
+
* or Cloudflare adapter when more than one process serves clients.
|
|
6
|
+
*
|
|
7
|
+
* @param options The clock.
|
|
8
|
+
* @returns The adapter.
|
|
9
|
+
* @example
|
|
10
|
+
* import { createHub, memoryAdapter } from "@shadow-js/realtime/server";
|
|
11
|
+
*
|
|
12
|
+
* const adapter = memoryAdapter();
|
|
13
|
+
* const hub = createHub({ adapter });
|
|
14
|
+
* void hub.close();
|
|
15
|
+
* @since 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
function memoryAdapter(options = {}) {
|
|
18
|
+
const now = options.now ?? Date.now;
|
|
19
|
+
const idleMs = options.idleMs ?? 36e5;
|
|
20
|
+
let sweepAt = 1024;
|
|
21
|
+
const channels = /* @__PURE__ */ new Map();
|
|
22
|
+
const presence = /* @__PURE__ */ new Map();
|
|
23
|
+
const presenceListeners = /* @__PURE__ */ new Map();
|
|
24
|
+
const presenceQueue = [];
|
|
25
|
+
let presenceDraining = false;
|
|
26
|
+
let closed = false;
|
|
27
|
+
/** Forgets channels nobody listens to and nothing retains; runs when the table has doubled since the last sweep. */
|
|
28
|
+
function sweep() {
|
|
29
|
+
const cutoff = now() - idleMs;
|
|
30
|
+
for (const [name, state] of channels) if (state.listeners.size === 0 && state.lastActive < cutoff) {
|
|
31
|
+
trim(state, state.log.length, idleMs);
|
|
32
|
+
if (state.log.length === 0) channels.delete(name);
|
|
33
|
+
}
|
|
34
|
+
sweepAt = Math.max(1024, channels.size * 2);
|
|
35
|
+
}
|
|
36
|
+
function channelOf(name) {
|
|
37
|
+
let state = channels.get(name);
|
|
38
|
+
if (state === void 0) {
|
|
39
|
+
if (channels.size >= sweepAt) sweep();
|
|
40
|
+
state = {
|
|
41
|
+
lastActive: now(),
|
|
42
|
+
seq: 0,
|
|
43
|
+
log: [],
|
|
44
|
+
keys: /* @__PURE__ */ new Map(),
|
|
45
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
46
|
+
pending: [],
|
|
47
|
+
draining: false
|
|
48
|
+
};
|
|
49
|
+
channels.set(name, state);
|
|
50
|
+
}
|
|
51
|
+
state.lastActive = now();
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
/** Delivers queued messages in order; a listener that publishes in turn queues behind the message being delivered. */
|
|
55
|
+
function drain(state) {
|
|
56
|
+
if (state.draining) return;
|
|
57
|
+
state.draining = true;
|
|
58
|
+
try {
|
|
59
|
+
for (let next = state.pending.shift(); next !== void 0; next = state.pending.shift()) for (const listener of [...state.listeners]) try {
|
|
60
|
+
listener(next);
|
|
61
|
+
} catch {}
|
|
62
|
+
} finally {
|
|
63
|
+
state.draining = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function emitPresence(event) {
|
|
67
|
+
presenceQueue.push(event);
|
|
68
|
+
if (presenceDraining) return;
|
|
69
|
+
presenceDraining = true;
|
|
70
|
+
try {
|
|
71
|
+
for (let next = presenceQueue.shift(); next !== void 0; next = presenceQueue.shift()) for (const listener of [...presenceListeners.get(next.channel) ?? []]) try {
|
|
72
|
+
listener(next);
|
|
73
|
+
} catch {}
|
|
74
|
+
} finally {
|
|
75
|
+
presenceDraining = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function trim(state, history, ttl) {
|
|
79
|
+
const cutoff = now() - ttl;
|
|
80
|
+
let drop = Math.max(0, state.log.length - history);
|
|
81
|
+
while (drop < state.log.length && (state.log[drop]?.at ?? Infinity) < cutoff) drop += 1;
|
|
82
|
+
if (drop > 0) state.log = state.log.slice(drop);
|
|
83
|
+
}
|
|
84
|
+
function live(name) {
|
|
85
|
+
let set = presence.get(name);
|
|
86
|
+
if (set === void 0) {
|
|
87
|
+
set = /* @__PURE__ */ new Map();
|
|
88
|
+
presence.set(name, set);
|
|
89
|
+
}
|
|
90
|
+
return set;
|
|
91
|
+
}
|
|
92
|
+
function check() {
|
|
93
|
+
if (closed) throw new Error("the adapter is closed.");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
async publish(channel, entry, publish) {
|
|
97
|
+
check();
|
|
98
|
+
const state = channelOf(channel);
|
|
99
|
+
const at = now();
|
|
100
|
+
if (publish.key !== void 0) {
|
|
101
|
+
const known = state.keys.get(publish.key);
|
|
102
|
+
if (known !== void 0 && known.expires > at) return {
|
|
103
|
+
seq: known.seq,
|
|
104
|
+
duplicate: true
|
|
105
|
+
};
|
|
106
|
+
if (state.keys.size >= 4096) {
|
|
107
|
+
for (const [key, value] of state.keys) if (value.expires <= at) state.keys.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
state.seq += 1;
|
|
111
|
+
const message = {
|
|
112
|
+
...entry,
|
|
113
|
+
seq: state.seq,
|
|
114
|
+
at
|
|
115
|
+
};
|
|
116
|
+
if (publish.key !== void 0) state.keys.set(publish.key, {
|
|
117
|
+
seq: state.seq,
|
|
118
|
+
expires: at + publish.idempotencyTtlMs
|
|
119
|
+
});
|
|
120
|
+
if (publish.history > 0) {
|
|
121
|
+
state.log.push(message);
|
|
122
|
+
trim(state, publish.history, publish.historyTtlMs);
|
|
123
|
+
} else state.log = [];
|
|
124
|
+
state.pending.push(message);
|
|
125
|
+
drain(state);
|
|
126
|
+
return {
|
|
127
|
+
seq: message.seq,
|
|
128
|
+
duplicate: false
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
async subscribe(channel, listener) {
|
|
132
|
+
check();
|
|
133
|
+
const state = channelOf(channel);
|
|
134
|
+
state.listeners.add(listener);
|
|
135
|
+
return async () => {
|
|
136
|
+
state.listeners.delete(listener);
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async history(channel, after, limit) {
|
|
140
|
+
check();
|
|
141
|
+
const state = channels.get(channel);
|
|
142
|
+
if (state === void 0) return {
|
|
143
|
+
messages: [],
|
|
144
|
+
head: 0,
|
|
145
|
+
oldest: 0
|
|
146
|
+
};
|
|
147
|
+
const retained = state.log;
|
|
148
|
+
const messages = [];
|
|
149
|
+
for (const message of retained) {
|
|
150
|
+
if (messages.length >= limit) break;
|
|
151
|
+
if (message.seq > after) messages.push(message);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
messages,
|
|
155
|
+
head: state.seq,
|
|
156
|
+
oldest: retained[0]?.seq ?? 0
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
async presenceSet(channel, member, ttlMs) {
|
|
160
|
+
check();
|
|
161
|
+
const set = live(channel);
|
|
162
|
+
const known = set.get(member.id);
|
|
163
|
+
if (known !== void 0 && known.member.v > member.v) return;
|
|
164
|
+
set.set(member.id, {
|
|
165
|
+
member,
|
|
166
|
+
expires: now() + ttlMs
|
|
167
|
+
});
|
|
168
|
+
if (known === void 0) emitPresence({
|
|
169
|
+
channel,
|
|
170
|
+
op: "join",
|
|
171
|
+
member
|
|
172
|
+
});
|
|
173
|
+
else if (known.member.v < member.v) emitPresence({
|
|
174
|
+
channel,
|
|
175
|
+
op: "set",
|
|
176
|
+
member
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
async presenceRemove(channel, id) {
|
|
180
|
+
check();
|
|
181
|
+
const set = presence.get(channel);
|
|
182
|
+
const known = set?.get(id);
|
|
183
|
+
if (set === void 0 || known === void 0) return false;
|
|
184
|
+
set.delete(id);
|
|
185
|
+
if (set.size === 0) presence.delete(channel);
|
|
186
|
+
emitPresence({
|
|
187
|
+
channel,
|
|
188
|
+
op: "leave",
|
|
189
|
+
member: known.member
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
async presenceList(channel) {
|
|
194
|
+
check();
|
|
195
|
+
const at = now();
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const entry of presence.get(channel)?.values() ?? []) if (entry.expires > at) out.push(entry.member);
|
|
198
|
+
return out;
|
|
199
|
+
},
|
|
200
|
+
async presenceTouch(entries, ttlMs) {
|
|
201
|
+
check();
|
|
202
|
+
const expires = now() + ttlMs;
|
|
203
|
+
const missing = [];
|
|
204
|
+
for (const ref of entries) {
|
|
205
|
+
const entry = presence.get(ref.channel)?.get(ref.id);
|
|
206
|
+
if (entry === void 0) missing.push(ref);
|
|
207
|
+
else entry.expires = expires;
|
|
208
|
+
}
|
|
209
|
+
return missing;
|
|
210
|
+
},
|
|
211
|
+
async presenceReap(channel) {
|
|
212
|
+
check();
|
|
213
|
+
const set = presence.get(channel);
|
|
214
|
+
if (set === void 0) return [];
|
|
215
|
+
const at = now();
|
|
216
|
+
const removed = [];
|
|
217
|
+
for (const [id, entry] of [...set]) {
|
|
218
|
+
if (entry.expires > at) continue;
|
|
219
|
+
set.delete(id);
|
|
220
|
+
removed.push(entry.member);
|
|
221
|
+
emitPresence({
|
|
222
|
+
channel,
|
|
223
|
+
op: "leave",
|
|
224
|
+
member: entry.member
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (set.size === 0) presence.delete(channel);
|
|
228
|
+
return removed;
|
|
229
|
+
},
|
|
230
|
+
async presenceSubscribe(channel, listener) {
|
|
231
|
+
check();
|
|
232
|
+
let set = presenceListeners.get(channel);
|
|
233
|
+
if (set === void 0) {
|
|
234
|
+
set = /* @__PURE__ */ new Set();
|
|
235
|
+
presenceListeners.set(channel, set);
|
|
236
|
+
}
|
|
237
|
+
set.add(listener);
|
|
238
|
+
const owner = set;
|
|
239
|
+
return async () => {
|
|
240
|
+
owner.delete(listener);
|
|
241
|
+
if (owner.size === 0 && presenceListeners.get(channel) === owner) presenceListeners.delete(channel);
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
async close() {
|
|
245
|
+
closed = true;
|
|
246
|
+
channels.clear();
|
|
247
|
+
presence.clear();
|
|
248
|
+
presenceListeners.clear();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { memoryAdapter };
|
|
254
|
+
|
|
255
|
+
//# sourceMappingURL=memory.js.map
|