@solarpunkltd/swarm-chat-js 6.2.8 → 7.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.
Files changed (51) hide show
  1. package/README.md +175 -196
  2. package/dist/chat.d.ts +106 -0
  3. package/dist/events.d.ts +80 -0
  4. package/dist/follower.d.ts +122 -0
  5. package/dist/history.d.ts +51 -0
  6. package/dist/index.cjs +1 -0
  7. package/dist/index.d.ts +7 -8
  8. package/dist/index.js +576 -11
  9. package/dist/message/entry.d.ts +97 -0
  10. package/dist/message/index.d.ts +2 -0
  11. package/dist/message/message.d.ts +90 -0
  12. package/dist/message.cjs +1 -0
  13. package/dist/message.js +264 -0
  14. package/dist/sender.d.ts +62 -0
  15. package/dist/swarm.d.ts +74 -0
  16. package/package.json +48 -54
  17. package/dist/index.d.ts.map +0 -1
  18. package/dist/index.esm.js +0 -6578
  19. package/dist/interfaces/chat.d.ts +0 -41
  20. package/dist/interfaces/chat.d.ts.map +0 -1
  21. package/dist/interfaces/index.d.ts +0 -3
  22. package/dist/interfaces/index.d.ts.map +0 -1
  23. package/dist/interfaces/message.d.ts +0 -31
  24. package/dist/interfaces/message.d.ts.map +0 -1
  25. package/dist/lib/constants.d.ts +0 -10
  26. package/dist/lib/constants.d.ts.map +0 -1
  27. package/dist/lib/core.d.ts +0 -31
  28. package/dist/lib/core.d.ts.map +0 -1
  29. package/dist/lib/history.d.ts +0 -27
  30. package/dist/lib/history.d.ts.map +0 -1
  31. package/dist/lib/utils.d.ts +0 -32
  32. package/dist/lib/utils.d.ts.map +0 -1
  33. package/dist/scripts/mine.d.ts +0 -3
  34. package/dist/scripts/mine.d.ts.map +0 -1
  35. package/dist/swarm-chat-umd.js +0 -11
  36. package/dist/transports/MessageTransport.d.ts +0 -7
  37. package/dist/transports/MessageTransport.d.ts.map +0 -1
  38. package/dist/transports/PollingTransport.d.ts +0 -20
  39. package/dist/transports/PollingTransport.d.ts.map +0 -1
  40. package/dist/utils/bee.d.ts +0 -18
  41. package/dist/utils/bee.d.ts.map +0 -1
  42. package/dist/utils/common.d.ts +0 -5
  43. package/dist/utils/common.d.ts.map +0 -1
  44. package/dist/utils/error.d.ts +0 -8
  45. package/dist/utils/error.d.ts.map +0 -1
  46. package/dist/utils/eventEmitter.d.ts +0 -11
  47. package/dist/utils/eventEmitter.d.ts.map +0 -1
  48. package/dist/utils/logger.d.ts +0 -12
  49. package/dist/utils/logger.d.ts.map +0 -1
  50. package/dist/utils/validation.d.ts +0 -18
  51. package/dist/utils/validation.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,214 +1,193 @@
1
- # Swarm Chat JS Library 🐝💬
2
-
3
- The core client-side library for building decentralized chat applications over [Swarm](https://www.ethswarm.org/). This library provides the essential logic for sending, receiving, and managing chat messages within a Swarm ecosystem.
4
-
5
- **Important Note:** `swarm-chat-js` is designed to work in conjunction with a companion aggregator server. It is **not functional as a standalone library** for a complete chat system. The aggregator is responsible for collecting messages broadcast by users and consolidating them into a shared chat feed.
6
-
7
- The current reference implementation works with the [Solar-Punk-Ltd/swarm-chat-aggregator-js](https://github.com/Solar-Punk-Ltd/swarm-chat-aggregator-js).
8
-
9
- ---
10
-
11
- ## ⚙️ How It Works
12
-
13
- `swarm-chat-js` enables decentralized communication by leveraging Swarm's features like Feeds and GSOC.
14
-
15
- **Conceptual Overview:**
16
-
17
- Users broadcast their messages via updates to their personal Swarm feeds, with notifications sent to a pre-defined GSOC address. A dedicated **aggregator server** listens to this GSOC message. When new message notifications arrive, the aggregator fetches the message content, processes it, and then writes it to a common, persistent, protected Swarm feed (the "chat feed"). Client applications polling this aggregated chat feed to display messages to users (built into `swarm-chat-js`).
18
-
19
- **Message Flow:**
20
-
21
- 1. **User Sends Message:** A user types and sends a message using an application built with `swarm-chat-js`.
22
- - The message is written to the user's own Swarm feed.
23
- - An update is broadcast to a designated GSOC address.
24
- 2. **Aggregator Receives & Processes:** The aggregator server, subscribed to the GSOC address, receives the update.
25
- - It may perform validation or other processing steps.
26
- 3. **Aggregator Writes to Chat Feed:** The aggregator writes the processed message to the main, shared "chat feed".
27
- 4. **Client App Reads:** `swarm-chat-js` in other users' applications either:
28
- - **Polling mode**: Polls the main chat feed for new messages and displays them
29
- - **Waku mode**: Receives real-time message updates via the Waku network
30
-
31
- ---
32
-
33
- ## 📦 Installation
34
-
35
- You can install the library using npm or pnpm:
1
+ # Swarm Chat JS
2
+
3
+ A client library for chat over [Swarm](https://www.ethswarm.org/). A browser signs each message and sends it as one
4
+ GSOC write to a shared inbox. A chat server verifies it and publishes it to the chat's feed, and every reader follows
5
+ that feed. The browser talks only to a Bee node or gateway, never to a web server of its own.
6
+
7
+ The library needs its server,
8
+ [Solar-Punk-Ltd/swarm-chat-aggregator-js](https://github.com/Solar-Punk-Ltd/swarm-chat-aggregator-js), which listens on
9
+ the inbox and writes the feed. Version 7 of the library reads and writes the v7 message format only.
10
+
11
+ ## How it works
12
+
13
+ 1. **Sending.** The sender builds the message, signs it with the user's key, and writes it once to the inbox, a GSOC
14
+ address every chat shares. The message stays pending until the chat feed shows it. If it does not, the identical
15
+ bytes are written again every ten seconds, up to five times, and then it is failed with a manual retry.
16
+ 2. **Publishing.** The server checks each message's shape and signature and writes it to the chat's feed as the next
17
+ entry, at index 0, 1, 2 and on. After a message is published it saves a history file of the chat, and every entry
18
+ links to the newest file saved.
19
+ 3. **Opening.** A reader asks Bee for the feed's head once, shows the history file the head entry links to, and reads
20
+ the entries after it.
21
+ 4. **Following.** A reader polls explicit feed slots after the last one it saw, several in one poll. A slot not
22
+ written yet is the ordinary live state and is simply asked again. A slot that fails its checks is skipped and never
23
+ stops the chat. A slot no peer gives out while later slots exist is stepped past and read again on later polls, so a
24
+ message skipped arrives late rather than never. Gateway failures back off with jitter up to eight seconds.
25
+ 5. **Older messages.** Loading older messages is a click, which reads the history file before the oldest one shown.
26
+
27
+ ## Installation
36
28
 
37
29
  ```bash
38
- npm/pnpm install @solarpunkltd/swarm-chat-js
30
+ pnpm add @solarpunkltd/swarm-chat-js @ethersphere/bee-js
39
31
  ```
40
32
 
41
- ---
33
+ bee-js 13 is a peer dependency. The package runs in browsers and in Node 24 or later, as ESM or CommonJS.
42
34
 
43
- ## 🛠️ Core Concepts & API
35
+ ## Usage
44
36
 
45
- ### Imports
37
+ ```ts
38
+ import { EVENTS, MessageType, SwarmChat, type MessageData } from '@solarpunkltd/swarm-chat-js';
46
39
 
47
- ```typescript
48
- import { EVENTS, MessageData, MessageType, SwarmChat, ChatSettings } from '@solarpunkltd/swarm-chat-js';
49
- ```
50
-
51
- ### `ChatSettings` Interface
52
-
53
- This configuration object is crucial for initializing the `SwarmChat` instance.
54
-
55
- ```typescript
56
- export interface ChatSettings {
57
- user: {
58
- /** Private key of the chat user, used for signing updates to their own Swarm feed. */
59
- privateKey: string;
60
- /** Display name or nickname of the current user. */
61
- nickname: string;
62
- };
40
+ const chat = new SwarmChat({
41
+ user: { privateKey: userKey, nickname: 'alice' },
63
42
  infra: {
64
- /** URL of the Bee node used by the client to write to their own feed and to poll the aggregated chat feed. */
65
- beeUrl: string;
66
- /**
67
- * If true, a postage stamp (`stamp` property) must be provided for uploading messages.
68
- * The stamp does not necessarily need to be tied to the `beeUrl` node; it can be an independent stamp. (Enveloped stamp)
69
- */
70
- enveloped: boolean;
71
- /** Optional: Postage stamp ID. Required if `enveloped` is true, unless `beeUrl` points to a gateway with auto-stamping capabilities. */
72
- stamp?: string;
73
- /** The mined GSOC topic string where users broadcast updates about their new messages. */
74
- gsocTopic: string;
75
- /** The mined GSOC resource ID (address) associated with the `gsocTopic`. */
76
- gsocResourceId: string;
77
- /** The topic of the aggregated chat feed, written by the aggregator server. */
78
- chatTopic: string;
79
- /** The public address (Swarm feed address) of the aggregated chat feed, written by the aggregator. */
80
- chatAddress: string;
81
- /** Enable real-time messaging via Waku network. When true, messages are received instantly instead of polling. */
82
- waku: boolean;
83
- };
84
- }
43
+ beeUrl: 'https://gateway.example.com',
44
+ gsocTopic: inboxIdentifier,
45
+ gsocResourceId: minedInboxKey,
46
+ chatTopic: 'chat-my-stream',
47
+ chatAddress: serverFeedOwner,
48
+ pollingInterval: 500,
49
+ },
50
+ });
51
+
52
+ const { on } = chat.getEmitter();
53
+ on(EVENTS.MESSAGE_RECEIVED, (message: MessageData) => show(message));
54
+ on(EVENTS.MESSAGE_REQUEST_ERROR, (message: MessageData) => offerRetry(message));
55
+ on(EVENTS.STATUS, (status) => showConnection(status));
56
+
57
+ await chat.start();
58
+ await chat.sendMessage('hello', MessageType.TEXT);
59
+ await chat.sendMessage('👍', MessageType.REACTION, someMessageId);
60
+
61
+ // Later, and in any order, as often as needed.
62
+ await chat.stop();
85
63
  ```
86
64
 
87
- ### Events (`EVENTS`)
88
-
89
- The library emits several events that your application can subscribe to for reacting to different stages of the chat lifecycle. Use `SwarmChat.getEmitter().on(EVENT_NAME, callback)` to subscribe.
90
-
91
- - `EVENTS.LOADING_INIT`: ('loadingInit')
92
- Fired when the chat library begins its initialization process.
93
- - `EVENTS.LOADING_PREVIOUS_MESSAGES`: ('loadingPreviousMessages')
94
- Fired when the library is actively loading previous messages from the chat feed.
95
- - `EVENTS.MESSAGE_RECEIVED`: ('messageReceived')
96
- A new message has been successfully received from the aggregated chat feed and processed by the client.
97
- - `EVENTS.MESSAGE_REQUEST_INITIATED`: ('messageRequestInitiated')
98
- The current user has initiated the process of sending a new message.
99
- - `EVENTS.MESSAGE_REQUEST_UPLOADED`: ('messageRequestUploaded')
100
- The current user's message has been successfully uploaded to their own feed. (Note: The broadcast to the GSOC for aggregator pickup can still be pending or fail after this event).
101
- - `EVENTS.MESSAGE_REQUEST_ERROR`: ('messageRequestError')
102
- An error occurred during the message sending process (either uploading to the user's feed or broadcasting via GSOC).
103
- - `EVENTS.CRITICAL_ERROR`: ('criticalError')
104
- The library has encountered a critical, potentially unrecoverable error.
105
-
106
- ### Main `SwarmChat` Methods
107
-
108
- The `SwarmChat` class instance provides the following core methods:
109
-
110
- - `start()`: Initializes and starts the chat service, including setting up listeners and beginning to poll for messages.
111
- - `stop()`: Stops the chat service, clears intervals, and cleans up resources.
112
- - `getEmitter()`: Returns an event emitter instance, allowing your application to subscribe to the `EVENTS` listed above.
113
- - `sendMessage(message: string, type: MessageType, targetMessageId?: string, id?: string)`: Initiates the process of sending a new chat message from the current user. Supports different message types including text, threads, and reactions.
114
- - `fetchPreviousMessages()`: Manually triggers the fetching of older messages from the aggregated chat feed.
115
- - `hasPreviousMessages()`: Returns a boolean indicating whether there are previous messages available to fetch (determined by checking if more than one message state reference exists).
116
- - `retrySendMessage(message: MessageData)`: Attempts to resend a message that previously encountered an error during the initial request phase (e.g., failed to write to the user's own feed).
117
- - `retryBroadcastUserMessage(message: MessageData)`: Attempts to re-broadcast a message update via GSOC if the message was successfully uploaded to the user's feed but the GSOC broadcast might have failed or needs retrying.
118
-
119
- ### Message Types
120
-
121
- The library supports three types of messages through the `MessageType` enum:
122
-
123
- - `MessageType.TEXT`: Regular chat messages
124
- - `MessageType.THREAD`: Reply messages that reference a parent message via `targetMessageId`
125
- - `MessageType.REACTION`: Emoji reactions to existing messages, also using `targetMessageId` to reference the target message
126
-
127
- ### Message State Management
128
-
129
- The library includes robust message state handling with:
130
-
131
- - **Automatic retry logic**: Failed message state references are automatically retried with exponential backoff
132
- - **Ref banning**: Persistently failing references are banned after maximum retry attempts to prevent infinite loops
133
- - **Efficient caching**: Message state data is cached to avoid redundant network requests
134
-
135
- ---
136
-
137
- ## 🚀 Usage Example (React)
138
-
139
- Here's an example of how `swarm-chat-js` can be integrated into a React application using a custom hook (`useSwarmChat`). This hook encapsulates chat logic, state management, and event handling.
140
-
141
- ### Complete Implementation
142
-
143
- For a full working example, check out our React integration:
144
-
145
- **📖 [View Complete useSwarmChat Hook Implementation](https://github.com/Solar-Punk-Ltd/swarm-chat-react-example/blob/master/src/hooks/useSwarmChat.tsx)**
146
-
147
- ### Basic Usage
148
-
149
- ```typescript
150
- import { useSwarmChat } from './hooks/useSwarmChat';
151
- import { MessageType } from '@solarpunkltd/swarm-chat-js';
152
-
153
- function ChatComponent() {
154
- const { messages, isLoading, sendMessage, hasPreviousMessages, loadPreviousMessages } = useSwarmChat(chatSettings);
155
-
156
- const handleSendMessage = (text: string) => {
157
- sendMessage(text, MessageType.TEXT);
158
- };
159
-
160
- const handleReaction = (targetMessageId: string, emoji: string) => {
161
- sendMessage(emoji, MessageType.REACTION, targetMessageId);
162
- };
163
-
164
- const handleReply = (targetMessageId: string, replyText: string) => {
165
- sendMessage(replyText, MessageType.THREAD, targetMessageId);
166
- };
167
-
168
- return (
169
- <div className="chat-container">
170
- {/* Your chat UI implementation */}
171
- {hasPreviousMessages() && <button onClick={loadPreviousMessages}>Load Previous Messages</button>}
172
- {/* Message list, input field, etc. */}
173
- </div>
174
- );
175
- }
65
+ ## Settings
66
+
67
+ | Setting | Meaning |
68
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `user.privateKey` | Signs every message. A viewer that only reads may pass any key. |
70
+ | `user.nickname` | The name on every message, 1 to 20 characters. Nothing can be sent without one. |
71
+ | `infra.beeUrl` | The Bee node or gateway every read and write goes through. |
72
+ | `infra.stamp` | The batch the inbox writes are stamped with. Left out for a gateway that stamps writes itself. |
73
+ | `infra.gsocTopic` | The inbox's identifier string, the same for every chat. |
74
+ | `infra.gsocResourceId` | The mined key every sender signs inbox writes with. The server's operator mines it. |
75
+ | `infra.chatTopic` | The chat's topic, which every message carries and the feed is named by. |
76
+ | `infra.chatAddress` | The server's feed owner address. |
77
+ | `infra.pollingInterval` | How often a reader polls at the live edge, 1,000 ms by default. |
78
+ | `infra.socReadTimeout` | One feed slot read, 5,000 ms by default. |
79
+ | `infra.feedReadTimeout` | The head lookup and a history file download, 15,000 ms by default. A head lookup that runs out of time reads the chat from slot 0. |
80
+ | `infra.gsocWriteTimeout` | One inbox write, 10,000 ms by default. |
81
+
82
+ ## Methods
83
+
84
+ | Method | What it does |
85
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | `start()` | Opens the chat and follows it. Resolves once it is open, or once `stop` is called. Opening is retried for as long as the chat runs. |
87
+ | `stop()` | Stops every poll, resend and retry. Listeners stay, so a later `start` reports to them again. |
88
+ | `getEmitter()` | The events below, with `on` and `off`. |
89
+ | `sendMessage(text, type, targetMessageId?)` | Sends a text or a thread reply and resolves to the pending message. A reaction resolves to `null`: taps on one reaction inside a second cancel in pairs, and only an odd count is sent. Rejects with a `ChatMessageError`, sending nothing, for a message the server would refuse. |
90
+ | `retrySendMessage(message)` | Writes a failed or pending message again with its identical bytes. |
91
+ | `fetchPreviousMessages()` | Shows the history file before the oldest one shown, and resolves to its messages. |
92
+ | `hasPreviousMessages()` | Whether there is an older file to load. |
93
+ | `orderMessages(messages)` | Published messages in chat order, then pending ones by time. |
94
+ | `getStatus()` | `live`, `reconnecting`, `stalled`, or `null` before the first read. |
95
+ | `getAddress()` | The address of the key, which is every sent message's `address`. |
96
+
97
+ ## Events
98
+
99
+ | Event | Carries | When |
100
+ | --------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
101
+ | `LOADING_INIT` | `boolean` | `true` when opening starts, `false` once the chat is open. |
102
+ | `LOADING_PREVIOUS_MESSAGES` | `boolean` | Around a load of older messages. |
103
+ | `MESSAGE_RECEIVED` | `MessageData` | A message the feed or a history file shows. For a sender, this is what "sent" means. |
104
+ | `MESSAGE_REQUEST_INITIATED` | `MessageData` | A message signed and about to be written, with `index` -1. |
105
+ | `MESSAGE_REQUEST_UPLOADED` | `MessageData` | Its first write the node accepted. It is still pending. |
106
+ | `MESSAGE_REQUEST_ERROR` | `MessageData` | Its resends ran out unseen. |
107
+ | `STATUS` | `FeedStatus` | `live`: the gateway answers. `reconnecting`: it does not. `stalled`: a message known to exist has not loaded for eight seconds. |
108
+ | `MESSAGE_SKIPPED` | `SkippedMessage` | A feed slot or history row not shown, and why. |
109
+ | `ERROR` | `unknown` | A failure the chat recovers from on its own. |
110
+ | `CRITICAL_ERROR` | `unknown` | Opening failed three times in a row. It keeps trying. |
111
+
112
+ **The order of a sent message's events.** `MESSAGE_REQUEST_INITIATED` comes first, then `MESSAGE_REQUEST_UPLOADED`
113
+ once a write is accepted, then `MESSAGE_RECEIVED` when the feed shows it. The resends and the give-up wait while the
114
+ status is not `live`, so a message is not failed only because this viewer could not see the feed. A message can still
115
+ be failed and then shown, for example when the server published it after the last resend, so `MESSAGE_RECEIVED` may
116
+ follow `MESSAGE_REQUEST_ERROR` for the same `id`, and a viewer settles on the received one.
117
+
118
+ A `MessageData` has `id`, `type`, `message` (the text or emoji), `username`, `address`, `timestamp` (the server's
119
+ receive time, or the sender's clock while pending), `targetMessageId` for a reply or reaction, `chatTopic`,
120
+ `signature`, `index` (the feed index, -1 while pending) and `sentAt` (the sender's clock, which nothing checks).
121
+
122
+ ## The message, for servers and tools
123
+
124
+ `@solarpunkltd/swarm-chat-js/message` is the one place the message is built, signed and checked, and the server imports
125
+ it rather than keeping a copy. It loads none of the reader.
126
+
127
+ ```ts
128
+ import { parseChatMessage, MAX_MESSAGE_BYTES } from '@solarpunkltd/swarm-chat-js/message';
129
+
130
+ const check = parseChatMessage(payload);
131
+ if (check.ok) publish(check.message);
132
+ else count(check.reason);
176
133
  ```
177
134
 
178
- ---
179
-
180
- ## ⛏️ Helper Scripts
181
-
182
- ### Mine GSOC Address
183
-
184
- The library provides a helper script to mine a GSOC address and topic. This is typically used when setting up your aggregator server.
185
-
186
- **Usage:**
135
+ A message is a JSON object of at most 2,048 bytes of UTF-8:
136
+
137
+ | Field | Meaning |
138
+ | -------- | ------------------------------------------------------------------------------------------------------------ |
139
+ | `v` | `7` |
140
+ | `topic` | The chat's topic, 1 to 128 characters |
141
+ | `id` | A random id, 32 lowercase hex characters |
142
+ | `type` | `text`, `thread` (a reply, `target` is the parent) or `reaction` (`target` is the message, `text` the emoji) |
143
+ | `target` | The id it refers to, or `""` for a text |
144
+ | `text` | 1 to 500 characters |
145
+ | `name` | 1 to 20 characters |
146
+ | `addr` | The sender's address, 40 lowercase hex characters without `0x` |
147
+ | `ts` | The sender's clock in milliseconds |
148
+ | `sig` | 130 hex characters |
149
+
150
+ Characters are counted as Unicode code points, so an emoji counts once, and `countCharacters` counts the same way for
151
+ a composer. The signed bytes are the UTF-8 of `JSON.stringify([7, topic, id, type, target, text, name, addr, ts])`,
152
+ signed with bee-js's `PrivateKey.sign` and checked with `Signature.recoverPublicKey`, both over the raw bytes.
153
+
154
+ | Export | Use |
155
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
156
+ | `parseChatMessage(bytes)` | The server's check of a raw payload: byte cap, UTF-8, JSON, shape, signature. Never throws. |
157
+ | `checkChatMessage(value)` | The same for a parsed object. |
158
+ | `createChatMessage(key, draft)` | Builds and signs, throwing a `ChatMessageError` for anything the check would refuse. |
159
+ | `chatMessageSchema`, `hasValidSignature`, `signedBytes`, `encodeChatMessage` | The parts. |
160
+ | `feedEntrySchema`, `parseFeedEntry` | The feed entry, `{v, seq, at, msg, history}`. |
161
+ | `historyFileSchema`, `parseHistoryFile` | The history file, `{v, topic, fromSeq, toSeq, messages, prev}`. |
162
+
163
+ ## Changes from 6.x
164
+
165
+ - Messages are signed over every field and verified by the server. 6.x signed four fields with a double prefix, and its
166
+ check passed forged messages.
167
+ - One inbox write per message. The write to the sender's own feed is gone, and so are `userTopic`, `additionalProps`,
168
+ and the `enveloped` setting.
169
+ - A bad message or history row is skipped, never a freeze. A slot no peer gives out is stepped past and read again.
170
+ - No signature check per message on the read path, which cost 6.x about 36 ms of main thread per message on every open.
171
+ - `sendMessage` takes three arguments, and a reaction resolves to `null`. `retrySendMessage` covers what
172
+ `retryBroadcastUserMessage` did.
173
+ - `stop` keeps listeners. New events: `STATUS`, `MESSAGE_SKIPPED` and `ERROR`. `CRITICAL_ERROR` means three failed
174
+ openings in a row, and opening keeps trying.
175
+ - `MessageData.index` is the feed index, `timestamp` is the server's receive time, and `sentAt` is new.
176
+ - The GSOC mining script moved to the server repository, whose operator mines the inbox key.
177
+ - The package is ESM first with a CommonJS build. The UMD bundle and the Node polyfills are gone.
178
+
179
+ ## Development
180
+
181
+ Node 24 and pnpm 12, through `packageManager`.
187
182
 
188
183
  ```bash
189
- npm run mine -- <bee-address> <topic-name>
184
+ pnpm install
185
+ pnpm test
190
186
  ```
191
187
 
192
- ---
193
-
194
- ## ⚠️ Limitations
195
-
196
- - **Polling Mechanism:** The current version of `swarm-chat-js` relies on polling the aggregator's chat feed to fetch new messages. This approach can be resource-heavy.
197
-
198
- ---
199
-
200
- ## 💡 Future Development
201
-
202
- - **Push-Based Event System:** We are actively designing a new architecture to transition from the polling mechanism to a more efficient, push-based event system for message delivery. This will significantly reduce node load and improve real-time message propagation.
203
- - **Performance Optimizations:** Further improvements to message state handling and caching mechanisms.
204
-
205
- ---
206
-
207
- ## 📚 Further Reading & Resources
188
+ `lint` (oxlint, type-aware), `format` and `format:check` (oxfmt), `typecheck`, `test` (vitest) and `build` (vite, with
189
+ types from `tsc`) are the scripts CI runs. `pnpm pack` builds before it packs.
208
190
 
209
- - [What are Feeds? (Official Swarm Documentation)](https://docs.ethswarm.org/docs/develop/tools-and-features/feeds#what-are-feeds)
210
- - [GSOC Introduction (Official Swarm Documentation)](https://docs.ethswarm.org/docs/develop/tools-and-features/gsoc/#introduction)
211
- - [Example Aggregator: Solar-Punk-Ltd/swarm-chat-aggregator-js](https://github.com/Solar-Punk-Ltd/swarm-chat-aggregator-js)
212
- - [Example React client](https://github.com/Solar-Punk-Ltd/swarm-chat-react-example)
191
+ ## License
213
192
 
214
- ---
193
+ Apache-2.0
package/dist/chat.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { ChatEmitter, type MessageData } from './events.js';
2
+ import { FeedStatus, type FollowerSettings } from './follower.js';
3
+ import { MessageType } from './message/index.js';
4
+ import { type GsocWrite, type SenderSettings } from './sender.js';
5
+ import { type ChatSource } from './swarm.js';
6
+ export interface ChatSettings {
7
+ user: {
8
+ /** Signs every message. A viewer that only reads may pass any key, since nothing is sent without a nickname. */
9
+ privateKey: string;
10
+ /** The name shown on every message, 1 to 20 characters. */
11
+ nickname: string;
12
+ };
13
+ infra: {
14
+ /** The Bee node or gateway every read and write goes through. */
15
+ beeUrl: string;
16
+ /** The batch the inbox writes are stamped with. Left out for a gateway that stamps writes itself. */
17
+ stamp?: string;
18
+ /** The inbox's identifier string, one for every chat, as the server listens on it. */
19
+ gsocTopic: string;
20
+ /** The mined key every sender signs inbox writes with, which puts them in the server node's neighbourhood. */
21
+ gsocResourceId: string;
22
+ /** The chat's topic, which every message carries and the server's feed is named by. */
23
+ chatTopic: string;
24
+ /** The server's feed owner address. */
25
+ chatAddress: string;
26
+ /** How often the reader polls at the live edge, 1,000 ms by default. */
27
+ pollingInterval?: number;
28
+ /**
29
+ * The head lookup and a history file download, 15,000 ms by default. An idle Bee answers the head lookup of a
30
+ * 1,000 message chat in about five seconds, so this is three times that. A lookup slower than this is a loaded
31
+ * gateway, and the chat is then read from slot 0 rather than waiting on it.
32
+ */
33
+ feedReadTimeout?: number;
34
+ /** One inbox write, 10,000 ms by default. */
35
+ gsocWriteTimeout?: number;
36
+ /** One feed slot read, 5,000 ms by default. */
37
+ socReadTimeout?: number;
38
+ };
39
+ }
40
+ /** What a test hands in instead of the network, and the tuning a test shortens. */
41
+ export interface ChatParts {
42
+ source: ChatSource;
43
+ write: GsocWrite;
44
+ follower: Partial<FollowerSettings>;
45
+ sender: Partial<SenderSettings>;
46
+ /** The waits between opening attempts, the last repeated. */
47
+ openRetryMs: readonly number[];
48
+ }
49
+ /**
50
+ * One chat: opens it from its newest history file, follows its feed, and sends messages to the server's inbox. Its
51
+ * events carry everything a viewer shows, and `start` and `stop` may be called in any order and any number of times.
52
+ */
53
+ export declare class SwarmChat {
54
+ private readonly emitter;
55
+ private readonly key;
56
+ private readonly topic;
57
+ private readonly nickname;
58
+ private readonly source;
59
+ private readonly write;
60
+ private readonly senderSettings;
61
+ private readonly openRetryMs;
62
+ private readonly follower;
63
+ private readonly history;
64
+ private sender;
65
+ private readonly seen;
66
+ private status;
67
+ private generation;
68
+ private running;
69
+ private starting;
70
+ private openTimer;
71
+ private wakeOpen;
72
+ constructor(settings: ChatSettings, parts?: Partial<ChatParts>);
73
+ getEmitter(): ChatEmitter;
74
+ /** The reader's state, or null before the chat has been read. */
75
+ getStatus(): FeedStatus | null;
76
+ /** This key's address, the `address` of every message it sends. */
77
+ getAddress(): string;
78
+ /**
79
+ * Opens the chat and starts following it. Resolves once it is open, or once `stop` is called first. Opening is
80
+ * retried with backoff for as long as the chat runs: every failure is an ERROR event, and three in a row a
81
+ * CRITICAL_ERROR.
82
+ */
83
+ start(): Promise<void>;
84
+ /** Stops every poll, resend and retry. Listeners stay, so a later `start` reports to them again. */
85
+ stop(): Promise<void>;
86
+ /**
87
+ * Sends a text or a thread reply, or taps a reaction. A text and a reply resolve to the pending message, which
88
+ * MESSAGE_RECEIVED later shows as published. A reaction resolves to null: taps inside a second cancel in pairs,
89
+ * and only an odd count is sent. Rejects with a ChatMessageError, sending nothing, for a message the server would
90
+ * refuse, such as one with no nickname.
91
+ */
92
+ sendMessage(text: string, type: MessageType, targetMessageId?: string): Promise<MessageData | null>;
93
+ /** Sends a failed or pending message again, with its identical bytes. Returns whether it was still held. */
94
+ retrySendMessage(message: MessageData | string): boolean;
95
+ /** Loads the history file before the oldest one shown, and shows its messages. */
96
+ fetchPreviousMessages(): Promise<MessageData[]>;
97
+ hasPreviousMessages(): boolean;
98
+ /** Published messages in chat order, then pending ones by the sender's clock. */
99
+ orderMessages<T extends Pick<MessageData, 'index' | 'timestamp'>>(messages: T[]): T[];
100
+ private open;
101
+ private waitToOpen;
102
+ private makeSender;
103
+ private show;
104
+ private showRows;
105
+ private setStatus;
106
+ }
@@ -0,0 +1,80 @@
1
+ import type { FeedStatus, SkipReason } from './follower.js';
2
+ import type { ChatMessage, FeedEntry, HistoryRow, MessageType } from './message/index.js';
3
+ export declare const EVENTS: {
4
+ /** `true` when opening starts, `false` once the chat is open and being followed. */
5
+ readonly LOADING_INIT: 'loadingInit';
6
+ /** `true` while "load older" reads a history file, then `false`. */
7
+ readonly LOADING_PREVIOUS_MESSAGES: 'loadingPreviousMessages';
8
+ /** A message the chat feed shows, a MessageData. Also how the sender learns its message went out. */
9
+ readonly MESSAGE_RECEIVED: 'messageReceived';
10
+ /** A message built, signed and about to be written, a MessageData with index -1. */
11
+ readonly MESSAGE_REQUEST_INITIATED: 'messageRequestInitiated';
12
+ /** The first write of a message that the node accepted. It is still pending until MESSAGE_RECEIVED. */
13
+ readonly MESSAGE_REQUEST_UPLOADED: 'messageRequestUploaded';
14
+ /**
15
+ * A message whose resends ran out unseen while the reader was live. `retrySendMessage` sends it again. A
16
+ * MESSAGE_RECEIVED for the same id may still follow, if the server published it late.
17
+ */
18
+ readonly MESSAGE_REQUEST_ERROR: 'messageRequestError';
19
+ /** Opening has failed three times in a row. It keeps trying, and a later LOADING_INIT `false` means it got through. */
20
+ readonly CRITICAL_ERROR: 'criticalError';
21
+ /** The reader's state, a FeedStatus: live, reconnecting or stalled. */
22
+ readonly STATUS: 'status';
23
+ /** A feed slot or history row not shown, a SkippedMessage. */
24
+ readonly MESSAGE_SKIPPED: 'messageSkipped';
25
+ /** Any failure the chat recovers from on its own, for logging. */
26
+ readonly ERROR: 'error';
27
+ };
28
+ export type ChatEvent = (typeof EVENTS)[keyof typeof EVENTS];
29
+ /** A message as the viewer shows it. The field names are 6.x's, so a viewer built on 6.x changes little. */
30
+ export interface MessageData {
31
+ id: string;
32
+ type: MessageType;
33
+ /** The text, or the emoji of a reaction. */
34
+ message: string;
35
+ username: string;
36
+ address: string;
37
+ /** The server's receive time once published, the sender's clock while pending. What the viewer shows and sorts by. */
38
+ timestamp: number;
39
+ /** The message a thread reply or a reaction refers to. */
40
+ targetMessageId?: string;
41
+ chatTopic: string;
42
+ signature: string;
43
+ /** The message's place in the chat, its feed index, or -1 while it is pending. */
44
+ index: number;
45
+ /** The sender's own clock, which nothing checks. */
46
+ sentAt: number;
47
+ }
48
+ export interface SkippedMessage {
49
+ /** The feed index, or -1 for rows of a history file. */
50
+ index: number;
51
+ reason: SkipReason | 'history-row';
52
+ detail: string;
53
+ }
54
+ export declare function pendingMessageData(message: ChatMessage): MessageData;
55
+ export declare function publishedMessageData(entry: FeedEntry | HistoryRow): MessageData;
56
+ export interface ChatEventPayloads {
57
+ [EVENTS.LOADING_INIT]: boolean;
58
+ [EVENTS.LOADING_PREVIOUS_MESSAGES]: boolean;
59
+ [EVENTS.MESSAGE_RECEIVED]: MessageData;
60
+ [EVENTS.MESSAGE_REQUEST_INITIATED]: MessageData;
61
+ [EVENTS.MESSAGE_REQUEST_UPLOADED]: MessageData;
62
+ [EVENTS.MESSAGE_REQUEST_ERROR]: MessageData;
63
+ [EVENTS.CRITICAL_ERROR]: unknown;
64
+ [EVENTS.STATUS]: FeedStatus;
65
+ [EVENTS.MESSAGE_SKIPPED]: SkippedMessage;
66
+ [EVENTS.ERROR]: unknown;
67
+ }
68
+ type Listener<T> = (data: T) => void;
69
+ /**
70
+ * The chat's events. `on` and `off` are bound, so a caller may take them apart from the emitter. A listener that
71
+ * throws is reported on the console and never stops the listeners after it or the chat behind it.
72
+ */
73
+ export declare class ChatEmitter {
74
+ private readonly listeners;
75
+ on: <E extends ChatEvent>(event: E, listener: Listener<ChatEventPayloads[E]>) => void;
76
+ off: <E extends ChatEvent>(event: E, listener: Listener<ChatEventPayloads[E]>) => void;
77
+ cleanAll: () => void;
78
+ emit<E extends ChatEvent>(event: E, data: ChatEventPayloads[E]): void;
79
+ }
80
+ export {};