@foony/chat 0.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.
- package/LICENSE +215 -0
- package/README.md +67 -0
- package/lib/chatClient.d.ts +22 -0
- package/lib/chatClient.d.ts.map +1 -0
- package/lib/chatClient.js +28 -0
- package/lib/chatClient.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/messages.d.ts +45 -0
- package/lib/messages.d.ts.map +1 -0
- package/lib/messages.js +146 -0
- package/lib/messages.js.map +1 -0
- package/lib/occupancy.d.ts +30 -0
- package/lib/occupancy.d.ts.map +1 -0
- package/lib/occupancy.js +65 -0
- package/lib/occupancy.js.map +1 -0
- package/lib/presence.d.ts +0 -0
- package/lib/presence.d.ts.map +1 -0
- package/lib/presence.js +0 -0
- package/lib/presence.js.map +1 -0
- package/lib/protocol.d.ts +52 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +72 -0
- package/lib/protocol.js.map +1 -0
- package/lib/reactions.d.ts +26 -0
- package/lib/reactions.d.ts.map +1 -0
- package/lib/reactions.js +56 -0
- package/lib/reactions.js.map +1 -0
- package/lib/reconciler.d.ts +29 -0
- package/lib/reconciler.d.ts.map +1 -0
- package/lib/reconciler.js +149 -0
- package/lib/reconciler.js.map +1 -0
- package/lib/room.d.ts +53 -0
- package/lib/room.d.ts.map +1 -0
- package/lib/room.js +81 -0
- package/lib/room.js.map +1 -0
- package/lib/rooms.d.ts +20 -0
- package/lib/rooms.d.ts.map +1 -0
- package/lib/rooms.js +38 -0
- package/lib/rooms.js.map +1 -0
- package/lib/types.d.ts +139 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/typing.d.ts +43 -0
- package/lib/typing.d.ts.map +1 -0
- package/lib/typing.js +108 -0
- package/lib/typing.js.map +1 -0
- package/lib/util.d.ts +9 -0
- package/lib/util.d.ts.map +1 -0
- package/lib/util.js +12 -0
- package/lib/util.js.map +1 -0
- package/lib-cjs/chatClient.js +32 -0
- package/lib-cjs/chatClient.js.map +1 -0
- package/lib-cjs/index.js +30 -0
- package/lib-cjs/index.js.map +1 -0
- package/lib-cjs/messages.js +150 -0
- package/lib-cjs/messages.js.map +1 -0
- package/lib-cjs/occupancy.js +69 -0
- package/lib-cjs/occupancy.js.map +1 -0
- package/lib-cjs/package.json +3 -0
- package/lib-cjs/presence.js +0 -0
- package/lib-cjs/presence.js.map +1 -0
- package/lib-cjs/protocol.js +79 -0
- package/lib-cjs/protocol.js.map +1 -0
- package/lib-cjs/reactions.js +60 -0
- package/lib-cjs/reactions.js.map +1 -0
- package/lib-cjs/reconciler.js +153 -0
- package/lib-cjs/reconciler.js.map +1 -0
- package/lib-cjs/room.js +85 -0
- package/lib-cjs/room.js.map +1 -0
- package/lib-cjs/rooms.js +42 -0
- package/lib-cjs/rooms.js.map +1 -0
- package/lib-cjs/types.js +10 -0
- package/lib-cjs/types.js.map +1 -0
- package/lib-cjs/typing.js +112 -0
- package/lib-cjs/typing.js.map +1 -0
- package/lib-cjs/util.js +15 -0
- package/lib-cjs/util.js.map +1 -0
- package/package.json +56 -0
- package/src/chatClient.ts +31 -0
- package/src/index.ts +32 -0
- package/src/messages.ts +159 -0
- package/src/occupancy.ts +78 -0
- package/src/presence.ts +0 -0
- package/src/protocol.ts +90 -0
- package/src/reactions.ts +64 -0
- package/src/reconciler.ts +169 -0
- package/src/room.ts +97 -0
- package/src/rooms.ts +42 -0
- package/src/types.ts +141 -0
- package/src/typing.ts +123 -0
- package/src/util.ts +12 -0
package/lib/room.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chat room: one realtime channel (`chat:<name>`) carrying messages,
|
|
3
|
+
* presence, typing, reactions and occupancy. Lifecycle (`attach`/`detach`/
|
|
4
|
+
* status) delegates to the underlying channel rather than duplicating a state
|
|
5
|
+
* machine.
|
|
6
|
+
*/
|
|
7
|
+
import { Messages } from './messages.js';
|
|
8
|
+
import { Occupancy } from './occupancy.js';
|
|
9
|
+
import { Presence } from './presence.js';
|
|
10
|
+
import { Reactions } from './reactions.js';
|
|
11
|
+
import { Typing } from './typing.js';
|
|
12
|
+
/** A chat room and its features. Obtain via `chatClient.rooms.get(name)`. */
|
|
13
|
+
export class Room {
|
|
14
|
+
name;
|
|
15
|
+
channel;
|
|
16
|
+
/** Messages: send, edit, delete, subscribe, history. */
|
|
17
|
+
messages;
|
|
18
|
+
/** Presence: enter/update/leave plus a local member snapshot. */
|
|
19
|
+
presence;
|
|
20
|
+
/** Typing indicators. */
|
|
21
|
+
typing;
|
|
22
|
+
/** Ephemeral room-level reactions. */
|
|
23
|
+
reactions;
|
|
24
|
+
/** Occupancy counts derived from presence. */
|
|
25
|
+
occupancy;
|
|
26
|
+
discontinuityListeners = new Set();
|
|
27
|
+
hadBeenAttached = false;
|
|
28
|
+
constructor(
|
|
29
|
+
/** Room name (without the `chat:` channel prefix). */
|
|
30
|
+
name, channel, getClientId, options) {
|
|
31
|
+
this.name = name;
|
|
32
|
+
this.channel = channel;
|
|
33
|
+
this.messages = new Messages(channel, name, getClientId);
|
|
34
|
+
this.presence = new Presence(channel);
|
|
35
|
+
this.typing = new Typing(channel, getClientId, options?.typing?.heartbeatThrottleMs);
|
|
36
|
+
this.reactions = new Reactions(channel, getClientId);
|
|
37
|
+
this.occupancy = new Occupancy(this.presence, options?.occupancy?.debounceMs);
|
|
38
|
+
channel.on((change) => this.onChannelState(change));
|
|
39
|
+
}
|
|
40
|
+
/** Current room status. */
|
|
41
|
+
get status() {
|
|
42
|
+
return this.channel.state;
|
|
43
|
+
}
|
|
44
|
+
/** Ensure the room is attached so messages and presence flow. */
|
|
45
|
+
async attach() {
|
|
46
|
+
await this.channel.attach();
|
|
47
|
+
}
|
|
48
|
+
/** Detach from the room (stop receiving). Listeners are preserved. */
|
|
49
|
+
async detach() {
|
|
50
|
+
await this.channel.detach();
|
|
51
|
+
}
|
|
52
|
+
/** Subscribe to room status changes. Returns an unsubscribe function. */
|
|
53
|
+
onStatusChange(listener) {
|
|
54
|
+
return this.channel.on(listener);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to continuity gaps — best-effort: fires when the room re-attaches
|
|
58
|
+
* without a resume, so the app can backfill via `messages.history()`.
|
|
59
|
+
*/
|
|
60
|
+
onDiscontinuity(listener) {
|
|
61
|
+
this.discontinuityListeners.add(listener);
|
|
62
|
+
return () => {
|
|
63
|
+
this.discontinuityListeners.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
onChannelState(change) {
|
|
67
|
+
if (change.current === 'attached') {
|
|
68
|
+
if (this.hadBeenAttached && !change.resumed) {
|
|
69
|
+
for (const listener of [...this.discontinuityListeners]) {
|
|
70
|
+
listener(change.reason);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.hadBeenAttached = true;
|
|
74
|
+
}
|
|
75
|
+
else if (change.current === 'detached') {
|
|
76
|
+
// A deliberate detach/re-attach is not a continuity gap.
|
|
77
|
+
this.hadBeenAttached = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=room.js.map
|
package/lib/room.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room.js","sourceRoot":"","sources":["../src/room.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AASrC,6EAA6E;AAC7E,MAAM,OAAO,IAAI;IAiBJ;IACQ;IAjBnB,wDAAwD;IAC/C,QAAQ,CAAW;IAC5B,iEAAiE;IACxD,QAAQ,CAAW;IAC5B,yBAAyB;IAChB,MAAM,CAAS;IACxB,sCAAsC;IAC7B,SAAS,CAAY;IAC9B,8CAA8C;IACrC,SAAS,CAAY;IAEb,sBAAsB,GAAG,IAAI,GAAG,EAAyB,CAAC;IACnE,eAAe,GAAG,KAAK,CAAC;IAEhC;IACE,sDAAsD;IAC7C,IAAY,EACJ,OAAgB,EACjC,WAAgC,EAChC,OAAqB;QAHZ,SAAI,GAAJ,IAAI,CAAQ;QACJ,YAAO,GAAP,OAAO,CAAS;QAIjC,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QACzD,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;QACrF,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QACrD,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9E,OAAO,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,2BAA2B;IAC3B,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;IAC5B,CAAC;IAED,iEAAiE;IACjE,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,sEAAsE;IACtE,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,yEAAyE;IACzE,cAAc,CAAC,QAA8C;QAC3D,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,QAA+B;QAC7C,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/C,CAAC,CAAC;IACJ,CAAC;IAEO,cAAc,CAAC,MAA0B;QAC/C,IAAI,MAAM,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YAClC,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC5C,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,sBAAsB,CAAC,EAAE,CAAC;oBACxD,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC9B,CAAC;aAAM,IAAI,MAAM,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YACzC,yDAAyD;YACzD,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC/B,CAAC;IACH,CAAC;CACF"}
|
package/lib/rooms.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room registry. Mirrors `Realtime.channels` — `get(name)` returns a stable
|
|
3
|
+
* {@link Room} per name (creating its `chat:<name>` channel on first use), and
|
|
4
|
+
* `release(name)` detaches and drops it.
|
|
5
|
+
*/
|
|
6
|
+
import type { Realtime } from '@foony/realtime';
|
|
7
|
+
import { Room } from './room.js';
|
|
8
|
+
import type { RoomOptions } from './types.js';
|
|
9
|
+
/** Factory and cache for {@link Room} instances on a {@link ChatClient}. */
|
|
10
|
+
export declare class Rooms {
|
|
11
|
+
private readonly realtime;
|
|
12
|
+
private readonly getClientId;
|
|
13
|
+
private readonly byName;
|
|
14
|
+
constructor(realtime: Realtime, getClientId: () => string | null);
|
|
15
|
+
/** Get (or create) the room named `name`. Stable instance per name. */
|
|
16
|
+
get(name: string, options?: RoomOptions): Room;
|
|
17
|
+
/** Detach and forget the room named `name`. No-op if not present. */
|
|
18
|
+
release(name: string): void;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=rooms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rooms.d.ts","sourceRoot":"","sources":["../src/rooms.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,4EAA4E;AAC5E,qBAAa,KAAK;IAId,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAJ9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;gBAG/B,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI;IAGnD,uEAAuE;IACvE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI;IAU9C,qEAAqE;IACrE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAS5B"}
|
package/lib/rooms.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room registry. Mirrors `Realtime.channels` — `get(name)` returns a stable
|
|
3
|
+
* {@link Room} per name (creating its `chat:<name>` channel on first use), and
|
|
4
|
+
* `release(name)` detaches and drops it.
|
|
5
|
+
*/
|
|
6
|
+
import { roomChannelName } from './protocol.js';
|
|
7
|
+
import { Room } from './room.js';
|
|
8
|
+
/** Factory and cache for {@link Room} instances on a {@link ChatClient}. */
|
|
9
|
+
export class Rooms {
|
|
10
|
+
realtime;
|
|
11
|
+
getClientId;
|
|
12
|
+
byName = new Map();
|
|
13
|
+
constructor(realtime, getClientId) {
|
|
14
|
+
this.realtime = realtime;
|
|
15
|
+
this.getClientId = getClientId;
|
|
16
|
+
}
|
|
17
|
+
/** Get (or create) the room named `name`. Stable instance per name. */
|
|
18
|
+
get(name, options) {
|
|
19
|
+
let existing = this.byName.get(name);
|
|
20
|
+
if (!existing) {
|
|
21
|
+
const channel = this.realtime.channels.get(roomChannelName(name));
|
|
22
|
+
existing = new Room(name, channel, this.getClientId, options);
|
|
23
|
+
this.byName.set(name, existing);
|
|
24
|
+
}
|
|
25
|
+
return existing;
|
|
26
|
+
}
|
|
27
|
+
/** Detach and forget the room named `name`. No-op if not present. */
|
|
28
|
+
release(name) {
|
|
29
|
+
const room = this.byName.get(name);
|
|
30
|
+
if (!room) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.byName.delete(name);
|
|
34
|
+
room.detach().catch(() => { });
|
|
35
|
+
this.realtime.channels.release(roomChannelName(name));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=rooms.js.map
|
package/lib/rooms.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rooms.js","sourceRoot":"","sources":["../src/rooms.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,4EAA4E;AAC5E,MAAM,OAAO,KAAK;IAIG;IACA;IAJF,MAAM,GAAG,IAAI,GAAG,EAAgB,CAAC;IAElD,YACmB,QAAkB,EAClB,WAAgC;QADhC,aAAQ,GAAR,QAAQ,CAAU;QAClB,gBAAW,GAAX,WAAW,CAAqB;IAChD,CAAC;IAEJ,uEAAuE;IACvE,GAAG,CAAC,IAAY,EAAE,OAAqB;QACrC,IAAI,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;YAClE,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YAC9D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,qEAAqE;IACrE,OAAO,CAAC,IAAY;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC9B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,CAAC;CACF"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the @foony/chat API.
|
|
3
|
+
*
|
|
4
|
+
* Foony-native shapes inspired by Ably Chat: a {@link Message} is keyed by a
|
|
5
|
+
* stable `id` (so edit/delete reference it directly), and presence/typing/
|
|
6
|
+
* reactions/occupancy mirror the concepts without copying Ably's exact names.
|
|
7
|
+
*/
|
|
8
|
+
/** Latest action that produced a message's current state. */
|
|
9
|
+
export type MessageAction = 'create' | 'update' | 'delete';
|
|
10
|
+
/**
|
|
11
|
+
* A materialized chat message. Edits replace `text`/`metadata`/`headers`;
|
|
12
|
+
* deletes blank them and set {@link Message.deleted}. Reconcile by `id`.
|
|
13
|
+
*/
|
|
14
|
+
export type Message = {
|
|
15
|
+
/** Stable, sender-assigned message id. Pass it to `update`/`delete`. */
|
|
16
|
+
readonly id: string;
|
|
17
|
+
/** Client id of the author. */
|
|
18
|
+
readonly clientId: string;
|
|
19
|
+
/** Room this message belongs to. */
|
|
20
|
+
readonly roomName: string;
|
|
21
|
+
/** Message body; `''` once the message is deleted. */
|
|
22
|
+
readonly text: string;
|
|
23
|
+
/** Arbitrary application metadata; `{}` once deleted. */
|
|
24
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
25
|
+
/** Arbitrary application headers; `{}` once deleted. */
|
|
26
|
+
readonly headers: Readonly<Record<string, unknown>>;
|
|
27
|
+
/** When the message was created (server publish time). */
|
|
28
|
+
readonly createdAt: Date;
|
|
29
|
+
/** When the message last changed; equals `createdAt` until edited or deleted. */
|
|
30
|
+
readonly updatedAt: Date;
|
|
31
|
+
/** The latest applied action. */
|
|
32
|
+
readonly action: MessageAction;
|
|
33
|
+
/** True once a delete has been applied. */
|
|
34
|
+
readonly deleted: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Event delivered to message subscribers as the room's history materializes. */
|
|
37
|
+
export type ChatMessageEvent = {
|
|
38
|
+
readonly type: 'created';
|
|
39
|
+
readonly message: Message;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: 'updated';
|
|
42
|
+
readonly message: Message;
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: 'deleted';
|
|
45
|
+
readonly message: Message;
|
|
46
|
+
};
|
|
47
|
+
/** Fields a caller may set when sending a new message. */
|
|
48
|
+
export type SendMessageParams = {
|
|
49
|
+
/** Message body. */
|
|
50
|
+
readonly text: string;
|
|
51
|
+
/** Optional application metadata. */
|
|
52
|
+
readonly metadata?: Record<string, unknown>;
|
|
53
|
+
/** Optional application headers. */
|
|
54
|
+
readonly headers?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
/** Fields a caller may change when editing a message. Omitted fields are cleared. */
|
|
57
|
+
export type UpdateMessageParams = {
|
|
58
|
+
/** New message body. */
|
|
59
|
+
readonly text: string;
|
|
60
|
+
/** Replacement metadata (defaults to `{}`). */
|
|
61
|
+
readonly metadata?: Record<string, unknown>;
|
|
62
|
+
/** Replacement headers (defaults to `{}`). */
|
|
63
|
+
readonly headers?: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
/** A page of materialized messages returned by `messages.history`. */
|
|
66
|
+
export type MessagePage = {
|
|
67
|
+
/** Messages in the page, oldest-first. */
|
|
68
|
+
readonly messages: readonly Message[];
|
|
69
|
+
/** True when older messages remain beyond this page. */
|
|
70
|
+
readonly hasMore: boolean;
|
|
71
|
+
/** Cursor (oldest message id in the page) to pass back for the next page. */
|
|
72
|
+
readonly nextCursor?: string;
|
|
73
|
+
};
|
|
74
|
+
/** A single presence member in a room. */
|
|
75
|
+
export type PresenceMember = {
|
|
76
|
+
/** Member's client id. */
|
|
77
|
+
readonly clientId: string;
|
|
78
|
+
/** Member's connection id (a user on two devices appears as two members). */
|
|
79
|
+
readonly connectionId: string;
|
|
80
|
+
/** Presence payload supplied on enter/update, if any. */
|
|
81
|
+
readonly data?: unknown;
|
|
82
|
+
/** When this member's presence last changed. */
|
|
83
|
+
readonly updatedAt: Date;
|
|
84
|
+
};
|
|
85
|
+
/** Normalized presence transition delivered to presence subscribers. */
|
|
86
|
+
export type PresenceEvent = {
|
|
87
|
+
/** Which transition occurred. */
|
|
88
|
+
readonly type: 'enter' | 'update' | 'leave';
|
|
89
|
+
/** The member the transition is about. */
|
|
90
|
+
readonly member: PresenceMember;
|
|
91
|
+
};
|
|
92
|
+
/** Snapshot of who is currently typing in a room (excludes the local client). */
|
|
93
|
+
export type TypingEvent = {
|
|
94
|
+
/** Client ids currently typing. */
|
|
95
|
+
readonly currentlyTyping: ReadonlySet<string>;
|
|
96
|
+
/** The change that produced this event. */
|
|
97
|
+
readonly change: {
|
|
98
|
+
readonly type: 'started' | 'stopped';
|
|
99
|
+
readonly clientId: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
/** An ephemeral, room-level reaction (fire-and-forget; not stored). */
|
|
103
|
+
export type RoomReaction = {
|
|
104
|
+
/** Reaction name, e.g. an emoji or short code. */
|
|
105
|
+
readonly name: string;
|
|
106
|
+
/** Client id that sent the reaction. */
|
|
107
|
+
readonly clientId: string;
|
|
108
|
+
/** Optional reaction metadata. */
|
|
109
|
+
readonly metadata?: unknown;
|
|
110
|
+
/** When the reaction was sent (server publish time). */
|
|
111
|
+
readonly createdAt: Date;
|
|
112
|
+
/** True when the local client sent this reaction. */
|
|
113
|
+
readonly isSelf: boolean;
|
|
114
|
+
};
|
|
115
|
+
/** Room occupancy counts derived from the presence set. */
|
|
116
|
+
export type Occupancy = {
|
|
117
|
+
/** Distinct (clientId, connectionId) pairs present — i.e. connections in presence. */
|
|
118
|
+
readonly connections: number;
|
|
119
|
+
/** Distinct client ids present. */
|
|
120
|
+
readonly presenceMembers: number;
|
|
121
|
+
};
|
|
122
|
+
/** Per-room configuration. */
|
|
123
|
+
export type RoomOptions = {
|
|
124
|
+
/** Typing config. */
|
|
125
|
+
readonly typing?: {
|
|
126
|
+
/**
|
|
127
|
+
* How often a continuously-typing client re-broadcasts that it is typing,
|
|
128
|
+
* and the basis for receiver-side expiry. Must be uniform across clients
|
|
129
|
+
* in a room. Defaults to 10000ms.
|
|
130
|
+
*/
|
|
131
|
+
readonly heartbeatThrottleMs?: number;
|
|
132
|
+
};
|
|
133
|
+
/** Occupancy config. */
|
|
134
|
+
readonly occupancy?: {
|
|
135
|
+
/** Debounce window for occupancy change events. Defaults to 1000ms. */
|
|
136
|
+
readonly debounceMs?: number;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the @foony/chat API.
|
|
3
|
+
*
|
|
4
|
+
* Foony-native shapes inspired by Ably Chat: a {@link Message} is keyed by a
|
|
5
|
+
* stable `id` (so edit/delete reference it directly), and presence/typing/
|
|
6
|
+
* reactions/occupancy mirror the concepts without copying Ably's exact names.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
package/lib/typing.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-room typing indicators — ephemeral, pure client-side logic over the
|
|
3
|
+
* channel's pub/sub (no persistence, no edge support needed).
|
|
4
|
+
*
|
|
5
|
+
* Sender: the first `keystroke()` broadcasts `started` immediately; further
|
|
6
|
+
* keystrokes within `heartbeatThrottleMs` are no-ops, and one after the window
|
|
7
|
+
* re-broadcasts. `stop()` broadcasts `stopped`. As long as a client keeps
|
|
8
|
+
* typing it keeps heartbeating; when it stops, receivers expire it.
|
|
9
|
+
*
|
|
10
|
+
* Receiver: tracks who is typing and auto-expires a typer after
|
|
11
|
+
* `heartbeatThrottleMs + GRACE_MS` with no heartbeat, emitting a synthetic
|
|
12
|
+
* `stopped`. The local client is excluded from the typing set.
|
|
13
|
+
*/
|
|
14
|
+
import type { Channel, UnsubscribeFn } from '@foony/realtime';
|
|
15
|
+
import type { TypingEvent } from './types.js';
|
|
16
|
+
/** Listener invoked whenever the set of typers changes. */
|
|
17
|
+
export type TypingListener = (event: TypingEvent) => void;
|
|
18
|
+
/** The typing feature of a {@link Room}. */
|
|
19
|
+
export declare class Typing {
|
|
20
|
+
private readonly channel;
|
|
21
|
+
private readonly getClientId;
|
|
22
|
+
private readonly heartbeatThrottleMs;
|
|
23
|
+
private readonly listeners;
|
|
24
|
+
/** Active typers (excluding self) → expiry timer. */
|
|
25
|
+
private readonly typers;
|
|
26
|
+
/** When the local client last broadcast a heartbeat; -Infinity means "never / send next". */
|
|
27
|
+
private lastSentAt;
|
|
28
|
+
private channelUnsubscribe;
|
|
29
|
+
constructor(channel: Channel, getClientId: () => string | null, heartbeatThrottleMs?: number);
|
|
30
|
+
/** Signal the local client is typing. Throttled to one heartbeat per window. */
|
|
31
|
+
keystroke(): Promise<void>;
|
|
32
|
+
/** Signal the local client has stopped typing. */
|
|
33
|
+
stop(): Promise<void>;
|
|
34
|
+
/** Client ids currently typing (excludes the local client). */
|
|
35
|
+
get currentlyTyping(): ReadonlySet<string>;
|
|
36
|
+
/** Subscribe to typing changes. Attaches the channel on first listener. */
|
|
37
|
+
subscribe(listener: TypingListener): UnsubscribeFn;
|
|
38
|
+
private ensureChannelSubscription;
|
|
39
|
+
private markTyping;
|
|
40
|
+
private markStopped;
|
|
41
|
+
private emit;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=typing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing.d.ts","sourceRoot":"","sources":["../src/typing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAO1D,4CAA4C;AAC5C,qBAAa,MAAM;IAUf,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAV9B,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,qDAAqD;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoD;IAC3E,6FAA6F;IAC7F,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,kBAAkB,CAA8B;gBAGrC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,EACjD,mBAAmB,CAAC,EAAE,MAAM;IAK9B,gFAAgF;IAC1E,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAUhC,kDAAkD;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,+DAA+D;IAC/D,IAAI,eAAe,IAAI,WAAW,CAAC,MAAM,CAAC,CAEzC;IAED,2EAA2E;IAC3E,SAAS,CAAC,QAAQ,EAAE,cAAc,GAAG,aAAa;IAQlD,OAAO,CAAC,yBAAyB;IAkBjC,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,IAAI;CAMb"}
|
package/lib/typing.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-room typing indicators — ephemeral, pure client-side logic over the
|
|
3
|
+
* channel's pub/sub (no persistence, no edge support needed).
|
|
4
|
+
*
|
|
5
|
+
* Sender: the first `keystroke()` broadcasts `started` immediately; further
|
|
6
|
+
* keystrokes within `heartbeatThrottleMs` are no-ops, and one after the window
|
|
7
|
+
* re-broadcasts. `stop()` broadcasts `stopped`. As long as a client keeps
|
|
8
|
+
* typing it keeps heartbeating; when it stops, receivers expire it.
|
|
9
|
+
*
|
|
10
|
+
* Receiver: tracks who is typing and auto-expires a typer after
|
|
11
|
+
* `heartbeatThrottleMs + GRACE_MS` with no heartbeat, emitting a synthetic
|
|
12
|
+
* `stopped`. The local client is excluded from the typing set.
|
|
13
|
+
*/
|
|
14
|
+
import { parseTypingPayload, TYPING_EVENT } from './protocol.js';
|
|
15
|
+
/** Default heartbeat throttle; must be uniform across clients in a room. */
|
|
16
|
+
const DEFAULT_HEARTBEAT_THROTTLE_MS = 10_000;
|
|
17
|
+
/** Extra time a receiver waits past the throttle before expiring a typer. */
|
|
18
|
+
const GRACE_MS = 2_000;
|
|
19
|
+
/** The typing feature of a {@link Room}. */
|
|
20
|
+
export class Typing {
|
|
21
|
+
channel;
|
|
22
|
+
getClientId;
|
|
23
|
+
heartbeatThrottleMs;
|
|
24
|
+
listeners = new Set();
|
|
25
|
+
/** Active typers (excluding self) → expiry timer. */
|
|
26
|
+
typers = new Map();
|
|
27
|
+
/** When the local client last broadcast a heartbeat; -Infinity means "never / send next". */
|
|
28
|
+
lastSentAt = Number.NEGATIVE_INFINITY;
|
|
29
|
+
channelUnsubscribe = null;
|
|
30
|
+
constructor(channel, getClientId, heartbeatThrottleMs) {
|
|
31
|
+
this.channel = channel;
|
|
32
|
+
this.getClientId = getClientId;
|
|
33
|
+
this.heartbeatThrottleMs = heartbeatThrottleMs ?? DEFAULT_HEARTBEAT_THROTTLE_MS;
|
|
34
|
+
}
|
|
35
|
+
/** Signal the local client is typing. Throttled to one heartbeat per window. */
|
|
36
|
+
async keystroke() {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
if (now - this.lastSentAt < this.heartbeatThrottleMs) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.lastSentAt = now;
|
|
42
|
+
const payload = { state: 'started' };
|
|
43
|
+
await this.channel.publish(TYPING_EVENT, payload);
|
|
44
|
+
}
|
|
45
|
+
/** Signal the local client has stopped typing. */
|
|
46
|
+
async stop() {
|
|
47
|
+
this.lastSentAt = Number.NEGATIVE_INFINITY;
|
|
48
|
+
const payload = { state: 'stopped' };
|
|
49
|
+
await this.channel.publish(TYPING_EVENT, payload);
|
|
50
|
+
}
|
|
51
|
+
/** Client ids currently typing (excludes the local client). */
|
|
52
|
+
get currentlyTyping() {
|
|
53
|
+
return new Set(this.typers.keys());
|
|
54
|
+
}
|
|
55
|
+
/** Subscribe to typing changes. Attaches the channel on first listener. */
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
this.ensureChannelSubscription();
|
|
59
|
+
return () => {
|
|
60
|
+
this.listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
ensureChannelSubscription() {
|
|
64
|
+
if (this.channelUnsubscribe) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.channelUnsubscribe = this.channel.subscribe(TYPING_EVENT, (frame) => {
|
|
68
|
+
const payload = parseTypingPayload(frame.data);
|
|
69
|
+
const clientId = frame.clientId;
|
|
70
|
+
if (payload === null || clientId === undefined || clientId === this.getClientId()) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (payload.state === 'started') {
|
|
74
|
+
this.markTyping(clientId);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.markStopped(clientId);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
markTyping(clientId) {
|
|
82
|
+
const existing = this.typers.get(clientId);
|
|
83
|
+
if (existing) {
|
|
84
|
+
clearTimeout(existing);
|
|
85
|
+
}
|
|
86
|
+
const timer = setTimeout(() => this.markStopped(clientId), this.heartbeatThrottleMs + GRACE_MS);
|
|
87
|
+
this.typers.set(clientId, timer);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
this.emit({ type: 'started', clientId });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
markStopped(clientId) {
|
|
93
|
+
const existing = this.typers.get(clientId);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
clearTimeout(existing);
|
|
98
|
+
this.typers.delete(clientId);
|
|
99
|
+
this.emit({ type: 'stopped', clientId });
|
|
100
|
+
}
|
|
101
|
+
emit(change) {
|
|
102
|
+
const event = { currentlyTyping: new Set(this.typers.keys()), change };
|
|
103
|
+
for (const listener of [...this.listeners]) {
|
|
104
|
+
listener(event);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=typing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typing.js","sourceRoot":"","sources":["../src/typing.ts"],"names":[],"mappings":"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"}
|
package/lib/util.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Small internal helpers shared across chat features. */
|
|
2
|
+
/**
|
|
3
|
+
* A sender-assigned, roughly time-sortable message id: `<unixMillis>-<random>`.
|
|
4
|
+
* Mirrors the realtime SDK's transport id format, but the chat id is decoupled
|
|
5
|
+
* from transport — it lives in the payload so `send` can return it immediately
|
|
6
|
+
* and `update`/`delete` can reference it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function newMessageId(): string;
|
|
9
|
+
//# sourceMappingURL=util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAE1D;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAGrC"}
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Small internal helpers shared across chat features. */
|
|
2
|
+
/**
|
|
3
|
+
* A sender-assigned, roughly time-sortable message id: `<unixMillis>-<random>`.
|
|
4
|
+
* Mirrors the realtime SDK's transport id format, but the chat id is decoupled
|
|
5
|
+
* from transport — it lives in the payload so `send` can return it immediately
|
|
6
|
+
* and `update`/`delete` can reference it.
|
|
7
|
+
*/
|
|
8
|
+
export function newMessageId() {
|
|
9
|
+
const random = Math.floor(Math.random() * 0x1_0000_0000).toString(16).padStart(8, '0');
|
|
10
|
+
return `${Date.now()}-${random}`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=util.js.map
|
package/lib/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAE1D;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACvF,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ChatClient is the top-level entry point: wrap a connected (or connecting)
|
|
4
|
+
* {@link Realtime} client and use `chat.rooms.get(name)` to start chatting.
|
|
5
|
+
* The chat layer adds no new transport — it rides the existing connection.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ChatClient = void 0;
|
|
9
|
+
const rooms_js_1 = require("./rooms.js");
|
|
10
|
+
/** Chat client built on top of a {@link Realtime} instance. */
|
|
11
|
+
class ChatClient {
|
|
12
|
+
realtime;
|
|
13
|
+
/** Room registry and factory. */
|
|
14
|
+
rooms;
|
|
15
|
+
constructor(realtime) {
|
|
16
|
+
this.realtime = realtime;
|
|
17
|
+
this.rooms = new rooms_js_1.Rooms(realtime, () => this.clientId);
|
|
18
|
+
}
|
|
19
|
+
/** The underlying realtime connection (status, events). */
|
|
20
|
+
get connection() {
|
|
21
|
+
return this.realtime.connection;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The resolved client id for this connection, or null until the auth
|
|
25
|
+
* handshake completes. Used to attribute messages and detect self.
|
|
26
|
+
*/
|
|
27
|
+
get clientId() {
|
|
28
|
+
return this.realtime.getClientId();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.ChatClient = ChatClient;
|
|
32
|
+
//# sourceMappingURL=chatClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatClient.js","sourceRoot":"","sources":["../src/chatClient.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAGH,yCAAmC;AAEnC,+DAA+D;AAC/D,MAAa,UAAU;IAIQ;IAH7B,iCAAiC;IACxB,KAAK,CAAQ;IAEtB,YAA6B,QAAkB;QAAlB,aAAQ,GAAR,QAAQ,CAAU;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,gBAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED,2DAA2D;IAC3D,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,CAAC;CACF;AApBD,gCAoBC"}
|
package/lib-cjs/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Public entry point for @foony/chat.
|
|
4
|
+
*
|
|
5
|
+
* A chat layer over @foony/realtime: construct a `ChatClient` from a `Realtime`
|
|
6
|
+
* instance, then `chat.rooms.get(name)` for messages, presence, typing,
|
|
7
|
+
* reactions and occupancy. React bindings are intentionally out of this core
|
|
8
|
+
* package (planned as a separate `@foony/chat/react` subpath).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.MessageReconciler = exports.Occupancy = exports.Reactions = exports.Typing = exports.Presence = exports.Messages = exports.Room = exports.Rooms = exports.ChatClient = void 0;
|
|
12
|
+
var chatClient_js_1 = require("./chatClient.js");
|
|
13
|
+
Object.defineProperty(exports, "ChatClient", { enumerable: true, get: function () { return chatClient_js_1.ChatClient; } });
|
|
14
|
+
var rooms_js_1 = require("./rooms.js");
|
|
15
|
+
Object.defineProperty(exports, "Rooms", { enumerable: true, get: function () { return rooms_js_1.Rooms; } });
|
|
16
|
+
var room_js_1 = require("./room.js");
|
|
17
|
+
Object.defineProperty(exports, "Room", { enumerable: true, get: function () { return room_js_1.Room; } });
|
|
18
|
+
var messages_js_1 = require("./messages.js");
|
|
19
|
+
Object.defineProperty(exports, "Messages", { enumerable: true, get: function () { return messages_js_1.Messages; } });
|
|
20
|
+
var presence_js_1 = require("./presence.js");
|
|
21
|
+
Object.defineProperty(exports, "Presence", { enumerable: true, get: function () { return presence_js_1.Presence; } });
|
|
22
|
+
var typing_js_1 = require("./typing.js");
|
|
23
|
+
Object.defineProperty(exports, "Typing", { enumerable: true, get: function () { return typing_js_1.Typing; } });
|
|
24
|
+
var reactions_js_1 = require("./reactions.js");
|
|
25
|
+
Object.defineProperty(exports, "Reactions", { enumerable: true, get: function () { return reactions_js_1.Reactions; } });
|
|
26
|
+
var occupancy_js_1 = require("./occupancy.js");
|
|
27
|
+
Object.defineProperty(exports, "Occupancy", { enumerable: true, get: function () { return occupancy_js_1.Occupancy; } });
|
|
28
|
+
var reconciler_js_1 = require("./reconciler.js");
|
|
29
|
+
Object.defineProperty(exports, "MessageReconciler", { enumerable: true, get: function () { return reconciler_js_1.MessageReconciler; } });
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,iDAA6C;AAApC,2GAAA,UAAU,OAAA;AACnB,uCAAmC;AAA1B,iGAAA,KAAK,OAAA;AACd,qCAA8E;AAArE,+FAAA,IAAI,OAAA;AACb,6CAA+D;AAAtD,uGAAA,QAAQ,OAAA;AACjB,6CAAgE;AAAvD,uGAAA,QAAQ,OAAA;AACjB,yCAA0D;AAAjD,mGAAA,MAAM,OAAA;AACf,+CAAkE;AAAzD,yGAAA,SAAS,OAAA;AAClB,+CAAmE;AAA1D,yGAAA,SAAS,OAAA;AAClB,iDAAoD;AAA3C,kHAAA,iBAAiB,OAAA"}
|