@solarpunkltd/swarm-chat-js 7.2.0 → 8.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/README.md +119 -24
- package/dist/chat.d.ts +10 -3
- package/dist/events.d.ts +9 -5
- package/dist/follower.d.ts +17 -5
- package/dist/index.cjs +1 -1
- package/dist/index.js +149 -110
- package/dist/message/binding.d.ts +48 -0
- package/dist/message/canonicalSignature.d.ts +6 -0
- package/dist/message/entry.d.ts +25 -6
- package/dist/message/index.d.ts +2 -0
- package/dist/message/message.d.ts +60 -14
- package/dist/message/ownFeed.d.ts +9 -0
- package/dist/message-Bmwh3SCs.cjs +3 -0
- package/dist/message-Dbh8qrpg.js +455 -0
- package/dist/message.cjs +1 -1
- package/dist/message.js +2 -308
- package/dist/notes.d.ts +8 -1
- package/dist/pacer.d.ts +31 -0
- package/dist/sender.d.ts +11 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ that feed. The browser talks only to a Bee node or gateway, never to a web serve
|
|
|
6
6
|
|
|
7
7
|
The library needs its server,
|
|
8
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
|
|
9
|
+
the inbox and writes the feed. Version 8 of the library reads and writes the v8 message format only, and a v7 message
|
|
10
|
+
fails its shape check.
|
|
10
11
|
|
|
11
12
|
## How it works
|
|
12
13
|
|
|
@@ -25,9 +26,11 @@ the inbox and writes the feed. Version 7 of the library reads and writes the v7
|
|
|
25
26
|
feed entries up to the newest one a note named, never further. So it never asks Bee for an address before it is
|
|
26
27
|
written. Bee answers such a request by skipping its peers for that address for a minute, which made new messages
|
|
27
28
|
arrive about forty seconds late while readers polled the next slot. An entry a note named that does not load is
|
|
28
|
-
read again
|
|
29
|
-
|
|
30
|
-
writes
|
|
29
|
+
read again later, so a message arrives late rather than never. A slot that fails its checks is skipped and never
|
|
30
|
+
stops the chat. Gateway failures back off with jitter up to eight seconds. A reader that finds no note, which is a
|
|
31
|
+
server that writes none or a chat it has not opened yet, polls the next slot, and is led by notes from the first
|
|
32
|
+
one it finds. Any address that came back missing, a feed slot or a note, is asked for again no sooner than twenty
|
|
33
|
+
seconds later, three times a minute at most, which keeps Bee from skipping its peers for it.
|
|
31
34
|
5. **Older messages.** Loading older messages is a click, which reads the history file before the oldest one shown.
|
|
32
35
|
|
|
33
36
|
## Installation
|
|
@@ -41,12 +44,13 @@ bee-js 13 is a peer dependency. The package runs in browsers and in Node 24 or l
|
|
|
41
44
|
## Usage
|
|
42
45
|
|
|
43
46
|
```ts
|
|
44
|
-
import { EVENTS, MessageType, SwarmChat, type MessageData } from '@solarpunkltd/swarm-chat-js';
|
|
47
|
+
import { EVENTS, MessageType, MessageVia, SwarmChat, type MessageData } from '@solarpunkltd/swarm-chat-js';
|
|
45
48
|
|
|
46
49
|
const chat = new SwarmChat({
|
|
47
50
|
user: { privateKey: userKey, nickname: 'alice' },
|
|
48
51
|
infra: {
|
|
49
52
|
beeUrl: 'https://gateway.example.com',
|
|
53
|
+
via: MessageVia.GATEWAY,
|
|
50
54
|
gsocTopic: inboxIdentifier,
|
|
51
55
|
gsocResourceId: minedInboxKey,
|
|
52
56
|
chatTopic: 'chat-my-stream',
|
|
@@ -74,14 +78,16 @@ await chat.stop();
|
|
|
74
78
|
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
79
|
| `user.privateKey` | Signs every message. A viewer that only reads may pass any key. |
|
|
76
80
|
| `user.nickname` | The name on every message, 1 to 20 characters. Nothing can be sent without one. |
|
|
81
|
+
| `user.binding` | Optional. A signed-in viewer's Swarm ID chat binding, `{ msg, sig }`, for the chat address of `user.privateKey`. Every message then carries it. |
|
|
77
82
|
| `infra.beeUrl` | The Bee node or gateway the chat is read through, and written through when `infra.writeUrl` is not set. |
|
|
78
83
|
| `infra.writeUrl` | Optional. The Bee node or gateway the inbox writes go through, such as a write gateway that stamps them, when they go somewhere other than the reads. |
|
|
79
84
|
| `infra.stamp` | The batch the inbox writes are stamped with. Left out for a gateway that stamps writes itself. |
|
|
85
|
+
| `infra.via` | Required. Who pays for the inbox writes, `MessageVia.GATEWAY` or `MessageVia.OWN_STAMP`, as every message says in `via`. |
|
|
80
86
|
| `infra.gsocTopic` | The inbox's identifier string, the same for every chat. |
|
|
81
87
|
| `infra.gsocResourceId` | The mined key every sender signs inbox writes with. The server's operator mines it. |
|
|
82
88
|
| `infra.chatTopic` | The chat's topic, which every message carries and the feed is named by. |
|
|
83
89
|
| `infra.chatAddress` | The server's feed owner address. |
|
|
84
|
-
| `infra.pollingInterval` |
|
|
90
|
+
| `infra.pollingInterval` | The shortest wait between two polls, and the first wait after the gateway failed, 1,000 ms by default. |
|
|
85
91
|
| `infra.socReadTimeout` | One feed slot read, 5,000 ms by default. |
|
|
86
92
|
| `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. |
|
|
87
93
|
| `infra.gsocWriteTimeout` | One inbox write, 10,000 ms by default. |
|
|
@@ -125,8 +131,10 @@ be failed and then shown, for example when the server published it after the las
|
|
|
125
131
|
follow `MESSAGE_REQUEST_ERROR` for the same `id`, and a viewer settles on the received one.
|
|
126
132
|
|
|
127
133
|
A `MessageData` has `id`, `type`, `message` (the text or emoji), `username`, `address`, `timestamp` (the server's
|
|
128
|
-
receive time, or the sender's clock while pending), `targetMessageId` for a reply or reaction, `
|
|
129
|
-
|
|
134
|
+
receive time, or the sender's clock while pending), `targetMessageId` for a reply or reaction, `index` (the feed index,
|
|
135
|
+
-1 while pending) and `signedMessage`, the message exactly as its sender signed it. Everything else the message says,
|
|
136
|
+
its topic, its signature, the sender's clock, `via`, `own` and the binding, is read from `signedMessage`, and a page
|
|
137
|
+
that shows who sent it hands `signedMessage` to `verifyChatMessage`. See [Verifying](#verifying).
|
|
130
138
|
|
|
131
139
|
## The message, for servers and tools
|
|
132
140
|
|
|
@@ -136,38 +144,104 @@ it rather than keeping a copy. It loads none of the reader.
|
|
|
136
144
|
```ts
|
|
137
145
|
import { parseChatMessage, MAX_MESSAGE_BYTES } from '@solarpunkltd/swarm-chat-js/message';
|
|
138
146
|
|
|
139
|
-
const check = parseChatMessage(payload);
|
|
147
|
+
const check = parseChatMessage(payload, { origin: 'https://example.com' });
|
|
140
148
|
if (check.ok) publish(check.message);
|
|
141
149
|
else count(check.reason);
|
|
142
150
|
```
|
|
143
151
|
|
|
144
|
-
A message is a JSON object of at most
|
|
152
|
+
A message is a JSON object of at most 3,072 bytes of UTF-8, its fields in this order:
|
|
145
153
|
|
|
146
154
|
| Field | Meaning |
|
|
147
155
|
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
148
|
-
| `v` | `
|
|
156
|
+
| `v` | `8` |
|
|
149
157
|
| `topic` | The chat's topic, 1 to 128 characters |
|
|
150
158
|
| `id` | A random id, 32 lowercase hex characters |
|
|
151
159
|
| `type` | `text`, `thread` (a reply, `target` is the parent) or `reaction` (`target` is the message, `text` the emoji) |
|
|
152
160
|
| `target` | The id it refers to, or `""` for a text |
|
|
153
161
|
| `text` | 1 to 500 characters |
|
|
154
162
|
| `name` | 1 to 20 characters |
|
|
155
|
-
| `addr` | The sender's address, 40 lowercase hex characters without `0x`
|
|
163
|
+
| `addr` | The sender's chat address, 40 lowercase hex characters without `0x` |
|
|
156
164
|
| `ts` | The sender's clock in milliseconds |
|
|
157
|
-
| `
|
|
165
|
+
| `via` | Who paid for the inbox write: `gateway` (the gateway's stamp) or `own-stamp` (the sender's own) |
|
|
166
|
+
| `own` | `true` when the sender also keeps a copy of the message in its own feed |
|
|
167
|
+
| `idb` | Only for a signed-in sender: its Swarm ID chat binding, `{ msg, sig }` |
|
|
168
|
+
| `sig` | 130 lowercase hex characters of r, s and v, with v 27 or 28 and s in the lower half |
|
|
158
169
|
|
|
159
170
|
Characters are counted as Unicode code points, so an emoji counts once, and `countCharacters` counts the same way for
|
|
160
|
-
a composer. The signed bytes are the UTF-8 of
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
a composer. The signed bytes are the UTF-8 of
|
|
172
|
+
`JSON.stringify([8, topic, id, type, target, text, name, addr, ts, via, own, idb?.msg ?? '', idb?.sig ?? ''])`, signed
|
|
173
|
+
with bee-js's `PrivateKey.sign` and checked with `Signature.recoverPublicKey`, both over the raw bytes. So `via`, `own`
|
|
174
|
+
and the binding cannot be changed or taken off without breaking the signature. Every signature has a second valid
|
|
175
|
+
spelling, s replaced by n - s with v flipped, and bee-js reads any v but 27 as 28, so the check refuses all but the
|
|
176
|
+
low s spelling with v 27 or 28, the one bee-js signs. One message has one valid signature.
|
|
177
|
+
|
|
178
|
+
### The Swarm ID chat binding
|
|
179
|
+
|
|
180
|
+
A Swarm ID account gives each site its own chat key, and nothing in that key says which account it came from. The fork
|
|
181
|
+
derives it from the site's app secret, itself HMAC-SHA256 of the site's origin under the account key: the chat key is
|
|
182
|
+
HMAC-SHA256 of `swarm-id chat key v1` under the app secret, asked again under `swarm-id chat key v1/1`, `/2` and on
|
|
183
|
+
while the result is not a valid secp256k1 private key. This library never derives it. It only checks the address a
|
|
184
|
+
binding names. The binding is the account's statement, signed with the account's own key, that one chat address on one
|
|
185
|
+
site is its own. The Swarm ID fork signs it, and this library reads it exactly as the fork's `verifyChatBinding` does,
|
|
186
|
+
without importing the fork. `msg` is four lines joined by `\n`:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
Swarm ID chat binding v1
|
|
190
|
+
Site: https://example.com
|
|
191
|
+
Chat address: 0x<addr>
|
|
192
|
+
Date: 2026-10-14T12:00:00.000Z
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The site is a bare https origin as a browser reports it (plain http only for `localhost`, `127.0.0.1` and `[::1]`, and
|
|
196
|
+
only with `allowLocalHttp`), the chat address is `0x` and 40 lowercase hex characters, and the date is ISO 8601 UTC with
|
|
197
|
+
milliseconds. `sig` is EIP-191 `personal_sign` over `msg`, written as the fork writes it: `0x` and 130 lowercase hex
|
|
198
|
+
characters of r, s and v, v 27 or 28 and s in the lower half. Recovering it gives the account's address.
|
|
199
|
+
|
|
200
|
+
### Verifying
|
|
201
|
+
|
|
202
|
+
`verifyChatMessage(value, { origin, allowLocalHttp? })` checks a message's shape and its signature, and then, when it
|
|
203
|
+
carries a binding, that the binding's text is those four lines, its chat address is the message's `addr`, its signature
|
|
204
|
+
recovers an account, and its site is `origin`. It returns `{ ok: true, message, identity }`, `identity` being
|
|
205
|
+
`{ account, site, date }` for a signed-in sender and `null` for a guest. A message whose binding fails is refused with
|
|
206
|
+
the reason `binding` and is never shown as a guest.
|
|
207
|
+
|
|
208
|
+
`origin` is required, the bare origin of the site whose chat this is, such as `https://example.com`. A binding signed
|
|
209
|
+
for one site therefore never passes in another site's chat because a caller left the origin out. A JavaScript caller
|
|
210
|
+
that passes none anyway gets every bound message refused as `binding`. The server passes its configured site origin,
|
|
211
|
+
and a page passes its own.
|
|
212
|
+
|
|
213
|
+
**What `identity.account` proves, and what it does not.** It proves only that whoever controls that Swarm ID account
|
|
214
|
+
bound this chat key for this site, on the date the binding names. It is not a verified person, and it is not an
|
|
215
|
+
account anyone has allowed. One person can hold many accounts, an account can be made in a minute, and a lost or shared
|
|
216
|
+
account key binds just as well. A page that wants more, such as a badge for known people or a list of accounts allowed
|
|
217
|
+
to post, keeps that list itself and compares `identity.account` with it.
|
|
218
|
+
|
|
219
|
+
The server calls it, through `parseChatMessage`, on every payload. The reader in this library still checks no
|
|
220
|
+
signature, since that cost 6.x about 36 ms of main thread per message, so a page that shows identities runs
|
|
221
|
+
`verifyChatMessage` in a Web Worker on each event's `MessageData.signedMessage`, which is the feed
|
|
222
|
+
entry's `msg` or, while pending, the message as it was signed. It never throws, and it touches no DOM and no Node API.
|
|
223
|
+
|
|
224
|
+
### The own feed
|
|
225
|
+
|
|
226
|
+
A sender that also keeps a copy of each message it sends says so with `own: true`. The copies live in a feed owned by
|
|
227
|
+
the sender's chat address `addr`, under the topic `ownFeedTopic(chatTopic)`, which is `swarm-chat-own-feed-v1:` and the
|
|
228
|
+
chat's topic, turned into a feed topic with `Topic.fromString` as the chat's own feed is. This library does not write
|
|
229
|
+
that feed yet, so `SwarmChat` always sends `own: false`.
|
|
230
|
+
|
|
231
|
+
| Export | Use |
|
|
232
|
+
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
233
|
+
| `parseChatMessage(bytes, { origin })` | The server's check of a raw payload: byte cap, UTF-8, JSON, then `verifyChatMessage`. Never throws. |
|
|
234
|
+
| `verifyChatMessage(value, { origin })` | Shape, signature and binding of a parsed object, with the sender's identity. Never throws. |
|
|
235
|
+
| `createChatMessage(key, draft)` | Builds and signs, throwing a `ChatMessageError` for anything the check would refuse. `via` is required. |
|
|
236
|
+
| `MessageType`, `MessageVia` | The values of `type` and `via`. |
|
|
237
|
+
| `checkChatBinding(binding, addr, options?)`, `chatBindingSchema` | The binding check on its own and its shape. Its `origin` is optional, for a sender checking its own binding. |
|
|
238
|
+
| `ownFeedTopic(chatTopic)`, `OWN_FEED_TOPIC_PREFIX` | The topic of a sender's own feed of copies. |
|
|
239
|
+
| `chatMessageSchema`, `hasValidSignature`, `signedBytes`, `encodeChatMessage` | The parts. |
|
|
240
|
+
| `feedEntrySchema`, `parseFeedEntry` | The feed entry, `{v, seq, at, msg, history}`. |
|
|
241
|
+
| `historyFileSchema`, `parseHistoryFile` | The history file, `{v, topic, fromSeq, toSeq, messages, prev}`. |
|
|
242
|
+
|
|
243
|
+
The feed entry and the history file carry `v` 8 like the message. A feed entry, at most 4,096 bytes, holds a message at
|
|
244
|
+
the byte cap inside its largest wrapper.
|
|
171
245
|
|
|
172
246
|
## Slot notes, for servers and tools
|
|
173
247
|
|
|
@@ -195,6 +269,27 @@ notes are found it trusts its own clock, moving reads earlier only when that bou
|
|
|
195
269
|
no note is found for longer than a heartbeat, it waits out the whole bound from then on, which is never early. A
|
|
196
270
|
clock that runs behind sees every stamp as old as it expects, so its messages arrive as late as it runs behind.
|
|
197
271
|
|
|
272
|
+
## Changes in 8.0
|
|
273
|
+
|
|
274
|
+
- The v8 message adds `via`, `own` and, for a signed-in sender, `idb`, its Swarm ID chat binding, all signed. A v7
|
|
275
|
+
message, entry or history file fails its shape check, and nothing reads both.
|
|
276
|
+
- `verifyChatMessage` replaces `checkChatMessage` and also checks the binding, returning the sender's `identity`.
|
|
277
|
+
Both it and `parseChatMessage` require `{ origin }`, the site whose chat this is. A failed binding is refused with the
|
|
278
|
+
new reason `binding`.
|
|
279
|
+
- `MAX_MESSAGE_BYTES` is 3,072, so a long text fits beside a binding.
|
|
280
|
+
- `createChatMessage` requires `via` and takes `own` and `idb`. `SwarmChat` requires `infra.via` and takes
|
|
281
|
+
`user.binding`.
|
|
282
|
+
- `ownFeedTopic` names the feed where a sender keeps its copies.
|
|
283
|
+
- `MessageData.signedMessage` carries the whole signed message, for a page to verify. `chatTopic`, `signature` and
|
|
284
|
+
`sentAt` are gone from `MessageData`, since `signedMessage` carries them as `topic`, `sig` and `ts`.
|
|
285
|
+
- The binding is recovered through `@ethersphere/core-sdk`, which bee-js 13 already installs, now a dependency here.
|
|
286
|
+
|
|
287
|
+
## Changes in 7.3
|
|
288
|
+
|
|
289
|
+
- A reader asks Bee for an address that came back missing no more than three times a minute, a feed slot and a note
|
|
290
|
+
alike. Polling the next slot every second, as a reader that found no note did, kept that slot on Bee's skip list,
|
|
291
|
+
so the first messages in a quiet room arrived 40 to 55 s late. A slot a note names is still read at once.
|
|
292
|
+
|
|
198
293
|
## Changes in 7.2
|
|
199
294
|
|
|
200
295
|
- Readers follow a chat by its slot notes and no longer ask for the next feed slot before it exists, so a new message
|
package/dist/chat.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChatEmitter, type MessageData } from './events.js';
|
|
2
2
|
import { FeedStatus, type FollowerSettings } from './follower.js';
|
|
3
|
-
import { MessageType } from './message/index.js';
|
|
3
|
+
import { MessageType, type ChatBinding, type MessageVia } from './message/index.js';
|
|
4
4
|
import { type NoteSettings } from './notes.js';
|
|
5
5
|
import { type GsocWrite, type SenderSettings } from './sender.js';
|
|
6
6
|
import { type ChatSource } from './swarm.js';
|
|
@@ -10,6 +10,11 @@ export interface ChatSettings {
|
|
|
10
10
|
privateKey: string;
|
|
11
11
|
/** The name shown on every message, 1 to 20 characters. */
|
|
12
12
|
nickname: string;
|
|
13
|
+
/**
|
|
14
|
+
* The Swarm ID chat binding of a signed-in viewer, whose chat address must be `privateKey`'s. Every message then
|
|
15
|
+
* carries it, and a reader that verifies it shows the account. Left out for a guest.
|
|
16
|
+
*/
|
|
17
|
+
binding?: ChatBinding;
|
|
13
18
|
};
|
|
14
19
|
infra: {
|
|
15
20
|
/** The Bee node or gateway the chat is read through, and written through too when `writeUrl` is not set. */
|
|
@@ -21,6 +26,8 @@ export interface ChatSettings {
|
|
|
21
26
|
writeUrl?: string;
|
|
22
27
|
/** The batch the inbox writes are stamped with. Left out for a gateway that stamps writes itself. */
|
|
23
28
|
stamp?: string;
|
|
29
|
+
/** Who pays for the inbox writes, the gateway's stamp or the sender's own, as every message says in `via`. */
|
|
30
|
+
via: MessageVia;
|
|
24
31
|
/** The inbox's identifier string, one for every chat, as the server listens on it. */
|
|
25
32
|
gsocTopic: string;
|
|
26
33
|
/** The mined key every sender signs inbox writes with, which puts them in the server node's neighbourhood. */
|
|
@@ -29,7 +36,7 @@ export interface ChatSettings {
|
|
|
29
36
|
chatTopic: string;
|
|
30
37
|
/** The server's feed owner address. */
|
|
31
38
|
chatAddress: string;
|
|
32
|
-
/**
|
|
39
|
+
/** The shortest wait between two polls, and the first wait after the gateway failed, 1,000 ms by default. */
|
|
33
40
|
pollingInterval?: number;
|
|
34
41
|
/**
|
|
35
42
|
* The head lookup and a history file download, 15,000 ms by default. An idle Bee answers the head lookup of a
|
|
@@ -65,7 +72,7 @@ export declare class SwarmChat {
|
|
|
65
72
|
private readonly emitter;
|
|
66
73
|
private readonly key;
|
|
67
74
|
private readonly topic;
|
|
68
|
-
private readonly
|
|
75
|
+
private readonly profile;
|
|
69
76
|
private readonly source;
|
|
70
77
|
private readonly write;
|
|
71
78
|
private readonly senderSettings;
|
package/dist/events.d.ts
CHANGED
|
@@ -26,7 +26,10 @@ export declare const EVENTS: {
|
|
|
26
26
|
readonly ERROR: 'error';
|
|
27
27
|
};
|
|
28
28
|
export type ChatEvent = (typeof EVENTS)[keyof typeof EVENTS];
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* A message as the viewer shows it. The field names are 6.x's, so a viewer built on 6.x changes little. The fields a
|
|
31
|
+
* viewer renders are kept beside `signedMessage`, and everything else the message says is read from it.
|
|
32
|
+
*/
|
|
30
33
|
export interface MessageData {
|
|
31
34
|
id: string;
|
|
32
35
|
type: MessageType;
|
|
@@ -38,12 +41,13 @@ export interface MessageData {
|
|
|
38
41
|
timestamp: number;
|
|
39
42
|
/** The message a thread reply or a reaction refers to. */
|
|
40
43
|
targetMessageId?: string;
|
|
41
|
-
chatTopic: string;
|
|
42
|
-
signature: string;
|
|
43
44
|
/** The message's place in the chat, its feed index, or -1 while it is pending. */
|
|
44
45
|
index: number;
|
|
45
|
-
/**
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The message exactly as its sender signed it. The reader checks no signature, so a page that shows who sent it
|
|
48
|
+
* hands this to `verifyChatMessage`, in a Web Worker.
|
|
49
|
+
*/
|
|
50
|
+
signedMessage: ChatMessage;
|
|
47
51
|
}
|
|
48
52
|
export interface SkippedMessage {
|
|
49
53
|
/** The feed index, or -1 for rows of a history file. */
|
package/dist/follower.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export type FeedStatus = (typeof FeedStatus)[keyof typeof FeedStatus];
|
|
|
13
13
|
/** Why a slot was not shown: its payload failed a check, or it was stepped past and the retry list was full. */
|
|
14
14
|
export type SkipReason = ReadRefusal | 'unreachable';
|
|
15
15
|
export interface FollowerSettings {
|
|
16
|
+
/** The shortest wait between two polls, and the first wait after the gateway failed. */
|
|
16
17
|
pollIntervalMs: number;
|
|
18
|
+
/** The shortest wait between two reads of a feed slot that came back missing, see `pacer.ts` for why. */
|
|
19
|
+
missingReadSpacingMs: number;
|
|
17
20
|
/** Slots one poll may walk before handing back, which only a viewer catching up on a backlog reaches. */
|
|
18
21
|
maxSlotsPerPoll: number;
|
|
19
22
|
/** Polls the next slot may be refused before asking whether anything lies behind it. */
|
|
@@ -62,7 +65,9 @@ export interface FollowerEvents {
|
|
|
62
65
|
* goes to the retry list. Silence longer than the heartbeat makes the follower stop trusting its own clock.
|
|
63
66
|
*
|
|
64
67
|
* Polling, when no note has been found, which is how a server without notes is followed: reads after the last slot
|
|
65
|
-
* seen, several in one poll, until one is refused. A refused next slot is the live edge and is
|
|
68
|
+
* seen, several in one poll, until one is refused. A refused next slot is the live edge and is read again, but no
|
|
69
|
+
* sooner than `missingReadSpacingMs` after it was refused, since reading a missing address every second is what keeps
|
|
70
|
+
* Bee skipping its peers for it once it is written. The same spacing holds for every slot read again. When it
|
|
66
71
|
* keeps being refused, the follower looks a few slots past it, and a later slot that answers means the refused one is
|
|
67
72
|
* a message no peer is giving out right now. That one is stepped past and kept on a short list to read again, so a
|
|
68
73
|
* message skipped arrives late rather than never. A follower that can read notes keeps asking for them while it polls
|
|
@@ -88,6 +93,7 @@ export declare class FeedFollower {
|
|
|
88
93
|
private running;
|
|
89
94
|
private status;
|
|
90
95
|
private readonly settings;
|
|
96
|
+
private readonly pacer;
|
|
91
97
|
constructor(source: ChatSource, topic: string, events: FollowerEvents, settings?: Partial<FollowerSettings>, notes?: NoteReader | null);
|
|
92
98
|
/** The index of the next slot this follower will ask for. */
|
|
93
99
|
get nextIndex(): number;
|
|
@@ -103,7 +109,11 @@ export declare class FeedFollower {
|
|
|
103
109
|
stop(): void;
|
|
104
110
|
private schedule;
|
|
105
111
|
private poll;
|
|
106
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The wait before the next read: until the next note is due, the next slot may be polled, or a slot on the retry
|
|
114
|
+
* list may be read again, whichever comes first. A slot the pacer does not hold back, one served unreadable, waits
|
|
115
|
+
* the poll interval.
|
|
116
|
+
*/
|
|
107
117
|
private untilNextRead;
|
|
108
118
|
/**
|
|
109
119
|
* Asks for the notes of every time slot that has ended since the last ask. One slot is read on its own. Several,
|
|
@@ -113,6 +123,8 @@ export declare class FeedFollower {
|
|
|
113
123
|
private readDueNotes;
|
|
114
124
|
/** A slot's payload, null when refused, or 'unreadable' when it was served and failed its own check. */
|
|
115
125
|
private read;
|
|
126
|
+
/** As `read`, for a slot no note has named, or 'held' without asking Bee while its last miss is too recent. */
|
|
127
|
+
private readUnnamed;
|
|
116
128
|
/**
|
|
117
129
|
* Reads forward from the next slot. Returns how many slots it showed. Throws when the gateway fails, and when the
|
|
118
130
|
* next slot has come back unreadable on several polls in a row with nothing behind it, which is the gateway answering
|
|
@@ -139,9 +151,9 @@ export declare class FeedFollower {
|
|
|
139
151
|
private probePast;
|
|
140
152
|
private remember;
|
|
141
153
|
/**
|
|
142
|
-
* Reads the stepped-past slots tried least recently, and lets go of any unread for
|
|
143
|
-
* its entries in the order they were last tried, so a slot that never loads moves to
|
|
144
|
-
* ones behind it from being read.
|
|
154
|
+
* Reads the stepped-past slots tried least recently whose last miss is old enough, and lets go of any unread for
|
|
155
|
+
* `missedExpiryMs`. The map keeps its entries in the order they were last tried, so a slot that never loads moves to
|
|
156
|
+
* the back and cannot keep the ones behind it from being read.
|
|
145
157
|
*/
|
|
146
158
|
private readMissed;
|
|
147
159
|
private accept;
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./message.cjs");let t=require("@ethersphere/bee-js");var n={LOADING_INIT:`loadingInit`,LOADING_PREVIOUS_MESSAGES:`loadingPreviousMessages`,MESSAGE_RECEIVED:`messageReceived`,MESSAGE_REQUEST_INITIATED:`messageRequestInitiated`,MESSAGE_REQUEST_UPLOADED:`messageRequestUploaded`,MESSAGE_REQUEST_ERROR:`messageRequestError`,CRITICAL_ERROR:`criticalError`,STATUS:`status`,MESSAGE_SKIPPED:`messageSkipped`,ERROR:`error`};function r(e){return a(e,-1,e.ts)}function i(e){return a(e.msg,e.seq,e.at)}function a(e,t,n){return{id:e.id,type:e.type,message:e.text,username:e.name,address:e.addr,timestamp:n,...e.target?{targetMessageId:e.target}:{},chatTopic:e.topic,signature:e.sig,index:t,sentAt:e.ts}}var o=class{constructor(){this.listeners=new Map,this.on=(e,t)=>{this.listeners.set(e,[...this.listeners.get(e)??[],t])},this.off=(e,t)=>{this.listeners.set(e,(this.listeners.get(e)??[]).filter(e=>e!==t))},this.cleanAll=()=>{this.listeners.clear()}}emit(e,t){for(let n of this.listeners.get(e)??[])try{n(t)}catch(t){console.error(`a listener for ${e} threw`,t)}}},s=class extends Error{constructor(e){super(`the feed head lookup did not answer in time`,e),this.name=`HeadLookupTimeoutError`}},c=class extends Error{constructor(e,t){super(`feed slot ${e} was served but could not be read`,t),this.index=e,this.name=`UnreadableSlotError`}};function l(e){return e instanceof t.BeeResponseError&&(e.status===404||e.status===500)}async function u(e,n){try{return await n()}catch(n){if(l(n))return null;throw n instanceof t.BeeResponseError?n:new c(e,{cause:n})}}function d(n,r,i,a,o=e.DEFAULT_NOTE_SLOT_MS){let c=t.Topic.fromString(i),d=e=>n.feed.makeReader(c,r,f(e));return{readNote:t=>u(t,async()=>(await n.soc.makeReader(r,f(a.slotReadMs)).download(e.noteIdentifier(i,o,t))).payload.toUint8Array()),readSlot:e=>u(e,async()=>(await d(a.slotReadMs).downloadPayload({index:e})).payload.toUint8Array()),async readHead(){let e=f(a.feedReadMs);try{let{payload:t,feedIndex:i}=await n.feed.makeReader(c,r,e).downloadPayload();return{index:Number(i.toBigInt()),payload:t.toUint8Array()}}catch(t){if(l(t))return null;throw e.signal?.aborted?new s({cause:t}):t}},async readFile(e){return(await n.data.download(e,void 0,f(a.feedReadMs))).toUint8Array()}}}function f(e){return{signal:AbortSignal.timeout(e)}}var p=`0123456789abcdef`.repeat(4);function m(e,n,r,i,a){let o=new t.PrivateKey(r),s=t.Identifier.fromString(i);return async t=>{await e.messaging.gsocSend(n,o,s,t,{deferred:!1},f(a.writeMs))}}var h={LIVE:`live`,RECONNECTING:`reconnecting`,STALLED:`stalled`},g={pollIntervalMs:1e3,maxSlotsPerPoll:16,unservedPollsBeforeProbe:3,unreadablePollsBeforeFailure:3,probeIntervalCapMs:6e4,probeDistances:[1,2,4,8],missedLimit:32,missedReadsPerPoll:4,missedExpiryMs:3e5,backoffCapMs:8e3,stallAfterMs:8e3,random:Math.random,now:Date.now},_=class extends Error{constructor(e){super(`the gateway keeps answering feed slot ${e} with something that is not a chunk`),this.index=e,this.name=`UnreadableGatewayError`}},v=class{constructor(e,t,n,r={},i=null){this.source=e,this.topic=t,this.events=n,this.notes=i,this.next=0,this.ceiling=null,this.nextNoteSlot=0,this.lastNoteFoundAt=0,this.unservedPolls=0,this.nextProbeAt=0,this.unreadablePolls=0,this.gatewayFailures=0,this.missed=new Map,this.timer=null,this.generation=0,this.running=!1,this.status=null,this.settings={...g,...r}}get nextIndex(){return this.next}get missedIndices(){return[...this.missed.keys()]}get ledByNotes(){return this.ceiling!==null}start(e,t=null){this.stop(),this.next=e,this.ceiling=t?.newest??null,this.nextNoteSlot=t?.nextNoteSlot??(this.notes?this.notes.lastReadableSlot()+1:0),this.lastNoteFoundAt=this.settings.now(),this.resetRefusals(),this.unreadablePolls=0,this.gatewayFailures=0,this.missed.clear(),this.status=null,this.running=!0,this.schedule(0)}stop(){this.running=!1,this.generation++,this.timer&&=(clearTimeout(this.timer),null)}schedule(e){let t=this.generation;this.timer=setTimeout(()=>{this.timer=null,this.poll(t)},e)}async poll(e){let t=0,n=!1;try{this.notes!==null&&await this.readDueNotes(this.notes,e),t=await this.walk(e),await this.readMissed(e)}catch(t){n=!0,e===this.generation&&this.gatewayFailed(t)}if(e===this.generation&&this.running){if(n){this.schedule(this.backoffMs());return}this.gatewayAnswered(),this.schedule(t>=this.settings.maxSlotsPerPoll?0:this.untilNextRead())}}untilNextRead(){return this.ceiling===null||this.notes===null?this.settings.pollIntervalMs:Math.max(0,this.notes.readableAt(this.nextNoteSlot)-this.settings.now())}async readDueNotes(e,t){let n=this.settings.now(),r=e.lastReadableSlot(n);if(r<this.nextNoteSlot)return;let i=Math.max(this.nextNoteSlot,r-e.lookBackSlots+1),a=await e.scan(r,i);if(t===this.generation){if(this.nextNoteSlot=r+1,a){this.lastNoteFoundAt=n,this.ceiling=Math.max(this.ceiling??-1,a.note.newest,this.next-1);return}n-this.lastNoteFoundAt>=e.silenceMs&&(this.lastNoteFoundAt=n,e.clock.distrust(),this.nextNoteSlot=Math.min(this.nextNoteSlot,e.lastReadableSlot(n)+1))}}async read(e){try{return await this.source.readSlot(e)}catch(e){if(e instanceof c)return`unreadable`;throw e}}async walk(e){for(let t=0;t<this.settings.maxSlotsPerPoll;t++){let n=this.next;if(this.ceiling!==null&&n>this.ceiling)return t;let r=await this.read(n);if(e!==this.generation)return t;if(this.ceiling!==null&&(r===null||r===`unreadable`)){this.remember(n),this.next=n+1;continue}if(r===null||r===`unreadable`){if(t===0){if(this.unservedPolls++,this.unreadablePolls=r===`unreadable`?this.unreadablePolls+1:0,this.shouldProbe()&&await this.probePast(n,e)){this.unreadablePolls=0;continue}if(this.unreadablePolls>=this.settings.unreadablePollsBeforeFailure)throw new _(n)}return t}this.resetRefusals(),this.unreadablePolls=0,this.next=n+1,this.accept(n,r)}return this.settings.maxSlotsPerPoll}shouldProbe(){if(this.unservedPolls!==this.nextProbeAt)return!1;let{probeIntervalCapMs:e,pollIntervalMs:t}=this.settings,n=Math.max(1,Math.ceil(e/t));return this.nextProbeAt+=Math.min(this.nextProbeAt,n),!0}resetRefusals(){this.unservedPolls=0,this.nextProbeAt=this.settings.unservedPollsBeforeProbe}async probePast(e,t){let n=new Set([e]);for(let r of this.settings.probeDistances){let i=e+r,a=await this.read(i);if(t!==this.generation)return!1;if(a===null||a===`unreadable`){n.add(i);continue}this.resetRefusals(),this.remember(e);for(let r=e+1;r<i;r++){let e=n.has(r)?null:await this.read(r);if(t!==this.generation)return!1;e===null||e===`unreadable`?this.remember(r):this.accept(r,e)}return this.next=i+1,this.accept(i,a),!0}return!1}remember(e){if(!this.missed.has(e)){if(this.missed.size>=this.settings.missedLimit){this.events.skipped(e,`unreachable`,`stepped past with the retry list full`);return}this.missed.set(e,this.settings.now())}}async readMissed(e){let t=this.settings.now();for(let[e,n]of this.missed)t-n>=this.settings.missedExpiryMs&&(this.missed.delete(e),this.events.skipped(e,`unreachable`,`not served within the time a stepped-past slot is kept`));let n=[...this.missed.keys()].slice(0,this.settings.missedReadsPerPoll);for(let t of n){let n=await this.read(t);if(e!==this.generation)return;let r=this.missed.get(t);this.missed.delete(t),n!==null&&n!==`unreadable`?this.accept(t,n):r!==void 0&&this.missed.set(t,r)}}accept(t,n){let r=e.parseFeedEntry(n,t,this.topic);r.ok?(this.notes?.clock.observe(r.value.at,this.settings.now()),this.events.entry(r.value)):this.events.skipped(t,r.reason,r.detail)}gatewayFailed(e){this.gatewayFailures++,this.setStatus(h.RECONNECTING),this.events.error(e)}gatewayAnswered(){this.gatewayFailures=0;let e=this.settings.now(),t=[...this.missed.values()].some(t=>e-t>=this.settings.stallAfterMs);this.setStatus(t?h.STALLED:h.LIVE)}backoffMs(){let{pollIntervalMs:e,backoffCapMs:t,random:n}=this.settings,r=Math.min(t,e*2**this.gatewayFailures);return r/2+n()*r/2}setStatus(e){e!==this.status&&(this.status=e,this.events.status(e))}},y=16,b=class{constructor(e,t,n,r=null){this.source=e,this.topic=t,this.events=n,this.notes=r,this.older=null,this.loading=null}async open(){this.older=null;let e=await this.newestNote();if(e!==null)return this.openAt(e.newest,e);let t=await this.readHead();return t===null?{startAt:0,rows:[],notes:null}:this.openAt(t.index,null,t.payload)}async newestNote(){if(this.notes===null)return null;let e=this.notes.lastReadableSlot(),t=await this.notes.scan(e,e-this.notes.lookBackSlots+1);return t===null?null:{newest:t.note.newest,nextNoteSlot:e+1}}async openAt(e,t,n){let r=e<0?null:await this.newestLink(e,n);if(r===null)return{startAt:0,rows:[],notes:t};let i=await this.readFile(r);return i===`refused`?{startAt:0,rows:[],notes:t}:{startAt:r.toSeq+1,rows:i??[],notes:t}}async readHead(){try{return await this.source.readHead()}catch(e){if(e instanceof s)return this.events.error(e),null;throw e}}hasOlder(){return this.older!==null}loadOlder(){return this.loading??=this.loadOlderOnce().finally(()=>{this.loading=null}),this.loading}async loadOlderOnce(){let t=this.older;if(t===null)return[];let n=await this.source.readFile(t.ref),r=e.parseHistoryFile(n,this.topic,t);if(!r.ok)throw this.older=null,Error(`history file ${t.ref} refused (${r.reason}): ${r.detail}`);return this.report(r.value.skipped),this.older=r.value.prev,r.value.rows}async newestLink(t,n){let r=n===void 0?t:t-1;if(n!==void 0){let r=e.parseFeedEntry(n,t,this.topic);if(r.ok)return r.value.history}for(let n=r;n>=Math.max(0,t-y);n--){let t=await this.source.readSlot(n).catch(e=>{if(e instanceof c)return null;throw e});if(t===null)continue;let r=e.parseFeedEntry(t,n,this.topic);if(r.ok)return r.value.history}return null}async readFile(t){try{let n=e.parseHistoryFile(await this.source.readFile(t.ref),this.topic,t);return n.ok?(this.report(n.value.skipped),this.older=n.value.prev,n.value.rows):(this.events.error(Error(`history file ${t.ref} refused (${n.reason}): ${n.detail}`)),`refused`)}catch(e){return this.events.error(e),this.older=t,null}}report(e){e>0&&this.events.skipped(e)}},x={slotMs:e.DEFAULT_NOTE_SLOT_MS,heartbeatMs:e.DEFAULT_NOTE_HEARTBEAT_MS,marginMs:1e3,scanBatch:4,clockLimitMs:3e5};function S(e){return typeof e.readNote==`function`}var C=class{constructor(e){this.limitMs=e,this.aheadAtMost=1/0,this.trusted=!0}observe(e,t){this.aheadAtMost=Math.min(this.aheadAtMost,t-e)}distrust(){this.trusted=!1}get isTrusted(){return this.trusted}get ms(){let e=Number.isFinite(this.aheadAtMost),t=this.trusted||!e?Math.min(0,this.aheadAtMost):this.aheadAtMost;return Math.max(-this.limitMs,Math.min(this.limitMs,t))}},w=class{constructor(e,t,n){this.source=e,this.now=n,this.counts={found:0,empty:0},this.settings={...x,...t},this.clock=new C(this.settings.clockLimitMs)}lastReadableSlot(t=this.now()){return e.noteSlotOf(t-this.settings.marginMs-this.clock.ms,this.settings.slotMs)-1}readableAt(t){return e.noteSlotEnd(t,this.settings.slotMs)+this.settings.marginMs+this.clock.ms}get lookBackSlots(){return Math.ceil(this.settings.heartbeatMs/this.settings.slotMs)+2}get silenceMs(){return this.settings.heartbeatMs+2*this.settings.slotMs+this.settings.marginMs}async read(t){let n;try{n=await this.source.readNote(t)}catch(e){if(!(e instanceof c))throw e;n=null}let r=n===null?null:e.parseSlotNote(n);return r===null?(this.counts.empty++,null):(this.counts.found++,this.clock.observe(r.writtenAt,this.now()),r)}async scan(e,t){for(let n=e;n>=t;n-=this.settings.scanBatch){let e=[];for(let r=n;r>n-this.settings.scanBatch&&r>=t;r--)e.push(r);let r=await Promise.all(e.map(e=>this.read(e))),i=r.findIndex(e=>e!==null);if(i>=0)return{slot:e[i],note:r[i]}}return null}},T={resendAfterMs:1e4,maxResends:5,reactionWindowMs:1e3,isLive:()=>!0},E=class{constructor(e,t,n,r,i=T){this.key=e,this.name=t,this.write=n,this.events=r,this.settings=i,this.outgoing=new Map,this.reactions=new Map,this.stopped=!1}get address(){return this.key.publicKey().address().toHex()}send(t){let{message:n,bytes:r}=e.createChatMessage(this.key,{...t,name:this.name}),i={message:n,bytes:r,writes:0,written:!1,inFlight:!1,timer:null,lastError:null};return this.outgoing.set(n.id,i),this.events.pending(n),this.attempt(i),n}react(t,n,r){let i=JSON.stringify([t,n,r]),a=this.reactions.get(i);if(a){a.taps++;return}let o=setTimeout(()=>{let a=this.reactions.get(i);this.reactions.delete(i),a&&a.taps%2==1&&!this.stopped&&this.send({topic:t,type:e.MessageType.REACTION,target:n,text:r})},this.settings.reactionWindowMs);this.reactions.set(i,{taps:1,timer:o})}confirm(e){if(e.addr!==this.address)return;let t=this.outgoing.get(e.id);t&&(this.clearTimer(t),this.outgoing.delete(e.id),this.events.confirmed(t.message))}retry(e){let t=this.outgoing.get(e);return!t||this.stopped?!1:(this.clearTimer(t),t.writes=0,t.inFlight||this.attempt(t),!0)}isPending(e){return this.outgoing.has(e)}stop(){this.stopped=!0;for(let e of this.outgoing.values())this.clearTimer(e);for(let{timer:e}of this.reactions.values())clearTimeout(e);this.reactions.clear()}attempt(e){!this.stopped&&this.outgoing.has(e.message.id)&&(e.writes++,e.inFlight=!0,this.write(e.bytes).then(()=>{e.inFlight=!1,!e.written&&this.outgoing.has(e.message.id)&&(e.written=!0,this.events.written(e.message)),this.scheduleNext(e)},t=>{e.inFlight=!1,e.lastError=t,this.scheduleNext(e)}))}scheduleNext(e){!this.stopped&&this.outgoing.has(e.message.id)&&(e.timer=setTimeout(()=>{if(e.timer=null,!this.settings.isLive()){this.scheduleNext(e);return}if(e.writes>this.settings.maxResends){this.events.failed(e.message,e.lastError??Error(`never seen on the chat feed`));return}this.attempt(e)},this.settings.resendAfterMs))}clearTimer(e){e.timer&&=(clearTimeout(e.timer),null)}},D=[1e3,2e3,4e3,8e3],O=3;function k(e){return(e.startsWith(`0x`)?e.slice(2):e).toLowerCase()}var A=class{constructor(e,r={}){this.emitter=new o,this.seen=new Set,this.status=null,this.generation=0,this.running=!1,this.starting=null,this.openTimer=null,this.wakeOpen=null;let{user:i,infra:a}=e;this.key=new t.PrivateKey(k(i.privateKey)),this.topic=a.chatTopic,this.nickname=i.nickname,this.openRetryMs=r.openRetryMs??D,this.senderSettings={...T,isLive:()=>this.status===h.LIVE,...r.sender};let s={slotReadMs:a.socReadTimeout??5e3,feedReadMs:a.feedReadTimeout??15e3,writeMs:a.gsocWriteTimeout??1e4},c={...a.noteSlotMs===void 0?{}:{slotMs:a.noteSlotMs},...a.noteHeartbeatMs===void 0?{}:{heartbeatMs:a.noteHeartbeatMs},...r.notes};this.source=r.source??d(new t.Bee(a.beeUrl),k(a.chatAddress),a.chatTopic,s,c.slotMs),this.write=r.write??m(new t.Bee(a.writeUrl??a.beeUrl),a.stamp?k(a.stamp):p,k(a.gsocResourceId),a.gsocTopic,s);let l=r.follower?.now??(()=>Date.now()),u={pollIntervalMs:a.pollingInterval??1e3,...r.follower,now:l},f=this.source;this.notes=S(f)?new w(f,c,l):null,this.follower=new v(this.source,this.topic,{entry:e=>this.show(e),skipped:(e,t,r)=>this.emitter.emit(n.MESSAGE_SKIPPED,{index:e,reason:t,detail:r}),status:e=>this.setStatus(e),error:e=>this.emitter.emit(n.ERROR,e)},u,this.notes),this.history=new b(this.source,this.topic,{skipped:e=>this.emitter.emit(n.MESSAGE_SKIPPED,{index:-1,reason:`history-row`,detail:`${e} rows of a history file`}),error:e=>this.emitter.emit(n.ERROR,e)},this.notes),this.sender=this.makeSender()}getEmitter(){return this.emitter}getStatus(){return this.status}getAddress(){return this.key.publicKey().address().toHex()}start(){if(this.running)return this.starting??Promise.resolve();this.running=!0,this.generation++,this.seen.clear(),this.sender=this.makeSender();let e=this.open(this.generation).finally(()=>{this.starting===e&&(this.starting=null)});return this.starting=e,e}async stop(){this.running=!1,this.generation++,this.openTimer&&=(clearTimeout(this.openTimer),null),this.wakeOpen?.(),this.follower.stop(),this.sender.stop()}async sendMessage(t,n,i){if(n===e.MessageType.REACTION){if(!i)throw Error(`a reaction names the message it reacts to`);return this.sender.react(this.topic,i,t),null}return r(this.sender.send({topic:this.topic,type:n,target:i??``,text:t}))}retrySendMessage(e){return this.sender.retry(typeof e==`string`?e:e.id)}async fetchPreviousMessages(){this.emitter.emit(n.LOADING_PREVIOUS_MESSAGES,!0);try{let e=this.generation,t=await this.history.loadOlder();return e===this.generation?this.showRows(t):[]}finally{this.emitter.emit(n.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasOlder()}orderMessages(e){return[...e].sort((e,t)=>e.index>=0&&t.index>=0?e.index-t.index:e.index>=0||t.index>=0?e.index>=0?-1:1:e.timestamp-t.timestamp)}async open(e){this.emitter.emit(n.LOADING_INIT,!0);let t=0;for(;e===this.generation;)try{let t=await this.history.open();if(e!==this.generation)return;this.showRows(t.rows),this.follower.start(t.startAt,t.notes),this.emitter.emit(n.LOADING_INIT,!1);return}catch(r){if(e!==this.generation)return;t++,this.emitter.emit(n.ERROR,r),this.setStatus(h.RECONNECTING),t===O&&this.emitter.emit(n.CRITICAL_ERROR,r),await this.waitToOpen(this.openRetryMs[Math.min(t,this.openRetryMs.length)-1]??8e3)}}waitToOpen(e){return new Promise(t=>{this.wakeOpen=()=>{this.wakeOpen=null,t()},this.openTimer=setTimeout(()=>{this.openTimer=null,this.wakeOpen?.()},e)})}makeSender(){return new E(this.key,this.nickname,this.write,{pending:e=>this.emitter.emit(n.MESSAGE_REQUEST_INITIATED,r(e)),written:e=>this.emitter.emit(n.MESSAGE_REQUEST_UPLOADED,r(e)),failed:(e,t)=>{this.emitter.emit(n.ERROR,t),this.emitter.emit(n.MESSAGE_REQUEST_ERROR,r(e))},confirmed:()=>{}},this.senderSettings)}show(e){this.seen.has(e.seq)||(this.seen.add(e.seq),this.sender.confirm(e.msg),this.emitter.emit(n.MESSAGE_RECEIVED,i(e)))}showRows(e){let t=[];for(let r of e){if(this.seen.has(r.seq))continue;this.seen.add(r.seq),this.sender.confirm(r.msg);let e=i(r);t.push(e),this.emitter.emit(n.MESSAGE_RECEIVED,e)}return t}setStatus(e){e!==this.status&&(this.status=e,this.emitter.emit(n.STATUS,e))}};exports.ChatEmitter=o,exports.ChatMessageError=e.ChatMessageError,exports.DEFAULT_NOTE_HEARTBEAT_MS=e.DEFAULT_NOTE_HEARTBEAT_MS,exports.DEFAULT_NOTE_SLOT_MS=e.DEFAULT_NOTE_SLOT_MS,exports.EVENTS=n,exports.FeedStatus=h,exports.HeadLookupTimeoutError=s,exports.MAX_ENTRY_BYTES=e.MAX_ENTRY_BYTES,exports.MAX_MESSAGE_BYTES=e.MAX_MESSAGE_BYTES,exports.MAX_NAME_CHARACTERS=e.MAX_NAME_CHARACTERS,exports.MAX_NOTE_BYTES=e.MAX_NOTE_BYTES,exports.MAX_TEXT_CHARACTERS=e.MAX_TEXT_CHARACTERS,exports.MAX_TOPIC_CHARACTERS=e.MAX_TOPIC_CHARACTERS,exports.MESSAGE_VERSION=e.MESSAGE_VERSION,exports.MessageType=e.MessageType,exports.NOTE_VERSION=e.NOTE_VERSION,exports.SwarmChat=A,exports.UnreadableGatewayError=_,exports.UnreadableSlotError=c,exports.chatMessageSchema=e.chatMessageSchema,exports.checkChatMessage=e.checkChatMessage,exports.countCharacters=e.countCharacters,exports.createChatMessage=e.createChatMessage,exports.encodeChatMessage=e.encodeChatMessage,exports.encodeSlotNote=e.encodeSlotNote,exports.feedEntrySchema=e.feedEntrySchema,exports.hasValidSignature=e.hasValidSignature,exports.historyFileSchema=e.historyFileSchema,exports.historyLinkSchema=e.historyLinkSchema,exports.historyRowSchema=e.historyRowSchema,exports.newMessageId=e.newMessageId,exports.noteAddress=e.noteAddress,exports.noteIdentifier=e.noteIdentifier,exports.noteIdentifierText=e.noteIdentifierText,exports.noteSlotEnd=e.noteSlotEnd,exports.noteSlotOf=e.noteSlotOf,exports.parseChatMessage=e.parseChatMessage,exports.parseFeedEntry=e.parseFeedEntry,exports.parseHistoryFile=e.parseHistoryFile,exports.parseSlotNote=e.parseSlotNote,exports.signedBytes=e.signedBytes,exports.slotNoteSchema=e.slotNoteSchema;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./message-Bmwh3SCs.cjs");let t=require("@ethersphere/bee-js");var n={LOADING_INIT:`loadingInit`,LOADING_PREVIOUS_MESSAGES:`loadingPreviousMessages`,MESSAGE_RECEIVED:`messageReceived`,MESSAGE_REQUEST_INITIATED:`messageRequestInitiated`,MESSAGE_REQUEST_UPLOADED:`messageRequestUploaded`,MESSAGE_REQUEST_ERROR:`messageRequestError`,CRITICAL_ERROR:`criticalError`,STATUS:`status`,MESSAGE_SKIPPED:`messageSkipped`,ERROR:`error`};function r(e){return a(e,-1,e.ts)}function i(e){return a(e.msg,e.seq,e.at)}function a(e,t,n){return{id:e.id,type:e.type,message:e.text,username:e.name,address:e.addr,timestamp:n,...e.target?{targetMessageId:e.target}:{},index:t,signedMessage:e}}var o=class{constructor(){this.listeners=new Map,this.on=(e,t)=>{this.listeners.set(e,[...this.listeners.get(e)??[],t])},this.off=(e,t)=>{this.listeners.set(e,(this.listeners.get(e)??[]).filter(e=>e!==t))},this.cleanAll=()=>{this.listeners.clear()}}emit(e,t){for(let n of this.listeners.get(e)??[])try{n(t)}catch(t){console.error(`a listener for ${e} threw`,t)}}},s=2e4,c=class{constructor(e,t){this.spacingMs=e,this.now=t,this.missedAt=new Map}readableAt(e,t=this.now()){let n=this.missedAt.get(e);return n===void 0||this.expired(n,t)?-1/0:n+this.spacingMs}mayRead(e,t=this.now()){return t>=this.readableAt(e,t)}missed(e,t=this.now()){for(let[e,n]of this.missedAt)this.expired(n,t)&&this.missedAt.delete(e);this.missedAt.set(e,t)}found(e){this.missedAt.delete(e)}clear(){this.missedAt.clear()}expired(e,t){return e>t||t-e>=this.spacingMs}},l=class extends Error{constructor(e){super(`the feed head lookup did not answer in time`,e),this.name=`HeadLookupTimeoutError`}},u=class extends Error{constructor(e,t){super(`feed slot ${e} was served but could not be read`,t),this.index=e,this.name=`UnreadableSlotError`}};function d(e){return e instanceof t.BeeResponseError&&(e.status===404||e.status===500)}async function f(e,n){try{return await n()}catch(n){if(d(n))return null;throw n instanceof t.BeeResponseError?n:new u(e,{cause:n})}}function p(n,r,i,a,o=e.n){let s=t.Topic.fromString(i),c=e=>n.feed.makeReader(s,r,m(e));return{readNote:t=>f(t,async()=>(await n.soc.makeReader(r,m(a.slotReadMs)).download(e.s(i,o,t))).payload.toUint8Array()),readSlot:e=>f(e,async()=>(await c(a.slotReadMs).downloadPayload({index:e})).payload.toUint8Array()),async readHead(){let e=m(a.feedReadMs);try{let{payload:t,feedIndex:i}=await n.feed.makeReader(s,r,e).downloadPayload();return{index:Number(i.toBigInt()),payload:t.toUint8Array()}}catch(t){if(d(t))return null;throw e.signal?.aborted?new l({cause:t}):t}},async readFile(e){return(await n.data.download(e,void 0,m(a.feedReadMs))).toUint8Array()}}}function m(e){return{signal:AbortSignal.timeout(e)}}var h=`0123456789abcdef`.repeat(4);function g(e,n,r,i,a){let o=new t.PrivateKey(r),s=t.Identifier.fromString(i);return async t=>{await e.messaging.gsocSend(n,o,s,t,{deferred:!1},m(a.writeMs))}}var _={LIVE:`live`,RECONNECTING:`reconnecting`,STALLED:`stalled`},v={pollIntervalMs:1e3,missingReadSpacingMs:s,maxSlotsPerPoll:16,unservedPollsBeforeProbe:3,unreadablePollsBeforeFailure:3,probeIntervalCapMs:6e4,probeDistances:[1,2,4,8],missedLimit:32,missedReadsPerPoll:4,missedExpiryMs:3e5,backoffCapMs:8e3,stallAfterMs:8e3,random:Math.random,now:Date.now},y=class extends Error{constructor(e){super(`the gateway keeps answering feed slot ${e} with something that is not a chunk`),this.index=e,this.name=`UnreadableGatewayError`}},b=class{constructor(e,t,n,r={},i=null){this.source=e,this.topic=t,this.events=n,this.notes=i,this.next=0,this.ceiling=null,this.nextNoteSlot=0,this.lastNoteFoundAt=0,this.unservedPolls=0,this.nextProbeAt=0,this.unreadablePolls=0,this.gatewayFailures=0,this.missed=new Map,this.timer=null,this.generation=0,this.running=!1,this.status=null,this.settings={...v,...r},this.pacer=new c(this.settings.missingReadSpacingMs,this.settings.now)}get nextIndex(){return this.next}get missedIndices(){return[...this.missed.keys()]}get ledByNotes(){return this.ceiling!==null}start(e,t=null){this.stop(),this.next=e,this.ceiling=t?.newest??null,this.nextNoteSlot=t?.nextNoteSlot??(this.notes?this.notes.lastReadableSlot()+1:0),this.lastNoteFoundAt=this.settings.now(),this.resetRefusals(),this.unreadablePolls=0,this.gatewayFailures=0,this.missed.clear(),this.pacer.clear(),this.status=null,this.running=!0,this.schedule(0)}stop(){this.running=!1,this.generation++,this.timer&&=(clearTimeout(this.timer),null)}schedule(e){let t=this.generation;this.timer=setTimeout(()=>{this.timer=null,this.poll(t)},e)}async poll(e){let t=0,n=!1;try{this.notes!==null&&await this.readDueNotes(this.notes,e),t=await this.walk(e),await this.readMissed(e)}catch(t){n=!0,e===this.generation&&this.gatewayFailed(t)}if(e===this.generation&&this.running){if(n){this.schedule(this.backoffMs());return}this.gatewayAnswered(),this.schedule(t>=this.settings.maxSlotsPerPoll?0:this.untilNextRead())}}untilNextRead(){let e=this.settings.now(),t=t=>{let n=this.pacer.readableAt(t);return n>e?n:e+this.settings.pollIntervalMs},n=[...this.missed.keys()].map(t);return this.notes!==null&&n.push(this.notes.readableAt(this.nextNoteSlot)),this.ceiling===null&&n.push(t(this.next)),Math.max(0,Math.min(...n)-e)}async readDueNotes(e,t){let n=this.settings.now(),r=e.lastReadableSlot(n);if(r<this.nextNoteSlot)return;let i=Math.max(this.nextNoteSlot,r-e.lookBackSlots+1),a=await e.scan(r,i);if(t===this.generation){if(this.nextNoteSlot=r+1,a){this.lastNoteFoundAt=n,this.ceiling=Math.max(this.ceiling??-1,a.note.newest,this.next-1);return}n-this.lastNoteFoundAt>=e.silenceMs&&(this.lastNoteFoundAt=n,e.clock.distrust(),this.nextNoteSlot=Math.min(this.nextNoteSlot,e.lastReadableSlot(n)+1))}}async read(e){let t;try{t=await this.source.readSlot(e)}catch(e){if(e instanceof u)return`unreadable`;throw e}return t===null?this.pacer.missed(e):this.pacer.found(e),t}async readUnnamed(e){return this.pacer.mayRead(e)?this.read(e):`held`}async walk(e){for(let t=0;t<this.settings.maxSlotsPerPoll;t++){let n=this.next;if(this.ceiling!==null&&n>this.ceiling)return t;let r=this.ceiling===null?await this.readUnnamed(n):await this.read(n);if(e!==this.generation||r===`held`)return t;if(this.ceiling!==null&&(r===null||r===`unreadable`)){this.remember(n),this.next=n+1;continue}if(r===null||r===`unreadable`){if(t===0){if(this.unservedPolls++,this.unreadablePolls=r===`unreadable`?this.unreadablePolls+1:0,this.shouldProbe()&&await this.probePast(n,e)){this.unreadablePolls=0;continue}if(this.unreadablePolls>=this.settings.unreadablePollsBeforeFailure)throw new y(n)}return t}this.resetRefusals(),this.unreadablePolls=0,this.next=n+1,this.accept(n,r)}return this.settings.maxSlotsPerPoll}shouldProbe(){if(this.unservedPolls!==this.nextProbeAt)return!1;let{probeIntervalCapMs:e,pollIntervalMs:t,missingReadSpacingMs:n}=this.settings,r=Math.max(1,Math.ceil(e/Math.max(t,n)));return this.nextProbeAt+=Math.min(this.nextProbeAt,r),!0}resetRefusals(){this.unservedPolls=0,this.nextProbeAt=this.settings.unservedPollsBeforeProbe}async probePast(e,t){let n=new Set([e]);for(let r of this.settings.probeDistances){let i=e+r,a=await this.readUnnamed(i);if(t!==this.generation)return!1;if(a===null||a===`unreadable`||a===`held`){n.add(i);continue}this.resetRefusals(),this.remember(e);for(let r=e+1;r<i;r++){let e=n.has(r)?null:await this.readUnnamed(r);if(t!==this.generation)return!1;e===null||e===`unreadable`||e===`held`?this.remember(r):this.accept(r,e)}return this.next=i+1,this.accept(i,a),!0}return!1}remember(e){if(!this.missed.has(e)){if(this.missed.size>=this.settings.missedLimit){this.events.skipped(e,`unreachable`,`stepped past with the retry list full`);return}this.missed.set(e,this.settings.now())}}async readMissed(e){let t=this.settings.now();for(let[e,n]of this.missed)t-n>=this.settings.missedExpiryMs&&(this.missed.delete(e),this.events.skipped(e,`unreachable`,`not served within the time a stepped-past slot is kept`));let n=[...this.missed.keys()].filter(e=>this.pacer.mayRead(e,t)).slice(0,this.settings.missedReadsPerPoll);for(let t of n){let n=await this.read(t);if(e!==this.generation)return;let r=this.missed.get(t);this.missed.delete(t),n!==null&&n!==`unreadable`?this.accept(t,n):r!==void 0&&this.missed.set(t,r)}}accept(t,n){let r=e.v(n,t,this.topic);r.ok?(this.notes?.clock.observe(r.value.at,this.settings.now()),this.events.entry(r.value)):this.events.skipped(t,r.reason,r.detail)}gatewayFailed(e){this.gatewayFailures++,this.setStatus(_.RECONNECTING),this.events.error(e)}gatewayAnswered(){this.gatewayFailures=0;let e=this.settings.now(),t=[...this.missed.values()].some(t=>e-t>=this.settings.stallAfterMs);this.setStatus(t?_.STALLED:_.LIVE)}backoffMs(){let{pollIntervalMs:e,backoffCapMs:t,random:n}=this.settings,r=Math.min(t,e*2**this.gatewayFailures);return r/2+n()*r/2}setStatus(e){e!==this.status&&(this.status=e,this.events.status(e))}},x=16,S=class{constructor(e,t,n,r=null){this.source=e,this.topic=t,this.events=n,this.notes=r,this.older=null,this.loading=null}async open(){this.older=null;let e=await this.newestNote();if(e!==null)return this.openAt(e.newest,e);let t=await this.readHead();return t===null?{startAt:0,rows:[],notes:null}:this.openAt(t.index,null,t.payload)}async newestNote(){if(this.notes===null)return null;let e=this.notes.lastReadableSlot(),t=await this.notes.scan(e,e-this.notes.lookBackSlots+1);return t===null?null:{newest:t.note.newest,nextNoteSlot:e+1}}async openAt(e,t,n){let r=e<0?null:await this.newestLink(e,n);if(r===null)return{startAt:0,rows:[],notes:t};let i=await this.readFile(r);return i===`refused`?{startAt:0,rows:[],notes:t}:{startAt:r.toSeq+1,rows:i??[],notes:t}}async readHead(){try{return await this.source.readHead()}catch(e){if(e instanceof l)return this.events.error(e),null;throw e}}hasOlder(){return this.older!==null}loadOlder(){return this.loading??=this.loadOlderOnce().finally(()=>{this.loading=null}),this.loading}async loadOlderOnce(){let t=this.older;if(t===null)return[];let n=await this.source.readFile(t.ref),r=e.y(n,this.topic,t);if(!r.ok)throw this.older=null,Error(`history file ${t.ref} refused (${r.reason}): ${r.detail}`);return this.report(r.value.skipped),this.older=r.value.prev,r.value.rows}async newestLink(t,n){let r=n===void 0?t:t-1;if(n!==void 0){let r=e.v(n,t,this.topic);if(r.ok)return r.value.history}for(let n=r;n>=Math.max(0,t-x);n--){let t=await this.source.readSlot(n).catch(e=>{if(e instanceof u)return null;throw e});if(t===null)continue;let r=e.v(t,n,this.topic);if(r.ok)return r.value.history}return null}async readFile(t){try{let n=e.y(await this.source.readFile(t.ref),this.topic,t);return n.ok?(this.report(n.value.skipped),this.older=n.value.prev,n.value.rows):(this.events.error(Error(`history file ${t.ref} refused (${n.reason}): ${n.detail}`)),`refused`)}catch(e){return this.events.error(e),this.older=t,null}}report(e){e>0&&this.events.skipped(e)}},C={slotMs:e.n,heartbeatMs:e.t,marginMs:1e3,scanBatch:4,clockLimitMs:3e5,missingReadSpacingMs:s};function w(e){return typeof e.readNote==`function`}var T=class{constructor(e){this.limitMs=e,this.aheadAtMost=1/0,this.trusted=!0}observe(e,t){this.aheadAtMost=Math.min(this.aheadAtMost,t-e)}distrust(){this.trusted=!1}get isTrusted(){return this.trusted}get ms(){let e=Number.isFinite(this.aheadAtMost),t=this.trusted||!e?Math.min(0,this.aheadAtMost):this.aheadAtMost;return Math.max(-this.limitMs,Math.min(this.limitMs,t))}},E=class{constructor(e,t,n){this.source=e,this.now=n,this.counts={found:0,empty:0},this.settings={...C,...t},this.clock=new T(this.settings.clockLimitMs),this.pacer=new c(this.settings.missingReadSpacingMs,n)}lastReadableSlot(t=this.now()){return e.u(t-this.settings.marginMs-this.clock.ms,this.settings.slotMs)-1}readableAt(t){return e.l(t,this.settings.slotMs)+this.settings.marginMs+this.clock.ms}get lookBackSlots(){return Math.ceil(this.settings.heartbeatMs/this.settings.slotMs)+2}get silenceMs(){return this.settings.heartbeatMs+2*this.settings.slotMs+this.settings.marginMs}async read(t){if(!this.pacer.mayRead(t))return null;let n;try{n=await this.source.readNote(t),n===null&&this.pacer.missed(t)}catch(e){if(!(e instanceof u))throw e;n=null}let r=n===null?null:e.d(n);return r===null?(this.counts.empty++,null):(this.counts.found++,this.clock.observe(r.writtenAt,this.now()),r)}async scan(e,t){for(let n=e;n>=t;n-=this.settings.scanBatch){let e=[];for(let r=n;r>n-this.settings.scanBatch&&r>=t;r--)e.push(r);let r=await Promise.all(e.map(e=>this.read(e))),i=r.findIndex(e=>e!==null);if(i>=0)return{slot:e[i],note:r[i]}}return null}},D={resendAfterMs:1e4,maxResends:5,reactionWindowMs:1e3,isLive:()=>!0},O=class{constructor(e,t,n,r,i=D){this.key=e,this.profile=t,this.write=n,this.events=r,this.settings=i,this.outgoing=new Map,this.reactions=new Map,this.stopped=!1}get address(){return this.key.publicKey().address().toHex()}send(t){let{message:n,bytes:r}=e.M(this.key,{...t,...this.profile,own:!1}),i={message:n,bytes:r,writes:0,written:!1,inFlight:!1,timer:null,lastError:null};return this.outgoing.set(n.id,i),this.events.pending(n),this.attempt(i),n}react(t,n,r){let i=JSON.stringify([t,n,r]),a=this.reactions.get(i);if(a){a.taps++;return}let o=setTimeout(()=>{let a=this.reactions.get(i);this.reactions.delete(i),a&&a.taps%2==1&&!this.stopped&&this.send({topic:t,type:e.O.REACTION,target:n,text:r})},this.settings.reactionWindowMs);this.reactions.set(i,{taps:1,timer:o})}confirm(e){if(e.addr!==this.address)return;let t=this.outgoing.get(e.id);t&&(this.clearTimer(t),this.outgoing.delete(e.id),this.events.confirmed(t.message))}retry(e){let t=this.outgoing.get(e);return!t||this.stopped?!1:(this.clearTimer(t),t.writes=0,t.inFlight||this.attempt(t),!0)}isPending(e){return this.outgoing.has(e)}stop(){this.stopped=!0;for(let e of this.outgoing.values())this.clearTimer(e);for(let{timer:e}of this.reactions.values())clearTimeout(e);this.reactions.clear()}attempt(e){!this.stopped&&this.outgoing.has(e.message.id)&&(e.writes++,e.inFlight=!0,this.write(e.bytes).then(()=>{e.inFlight=!1,!e.written&&this.outgoing.has(e.message.id)&&(e.written=!0,this.events.written(e.message)),this.scheduleNext(e)},t=>{e.inFlight=!1,e.lastError=t,this.scheduleNext(e)}))}scheduleNext(e){!this.stopped&&this.outgoing.has(e.message.id)&&(e.timer=setTimeout(()=>{if(e.timer=null,!this.settings.isLive()){this.scheduleNext(e);return}if(e.writes>this.settings.maxResends){this.events.failed(e.message,e.lastError??Error(`never seen on the chat feed`));return}this.attempt(e)},this.settings.resendAfterMs))}clearTimer(e){e.timer&&=(clearTimeout(e.timer),null)}},k=[1e3,2e3,4e3,8e3],A=3;function j(e){return(e.startsWith(`0x`)?e.slice(2):e).toLowerCase()}var M=class{constructor(e,r={}){this.emitter=new o,this.seen=new Set,this.status=null,this.generation=0,this.running=!1,this.starting=null,this.openTimer=null,this.wakeOpen=null;let{user:i,infra:a}=e;this.key=new t.PrivateKey(j(i.privateKey)),this.topic=a.chatTopic,this.profile={name:i.nickname,via:a.via,...i.binding?{idb:i.binding}:{}},this.openRetryMs=r.openRetryMs??k,this.senderSettings={...D,isLive:()=>this.status===_.LIVE,...r.sender};let s={slotReadMs:a.socReadTimeout??5e3,feedReadMs:a.feedReadTimeout??15e3,writeMs:a.gsocWriteTimeout??1e4},c={...a.noteSlotMs===void 0?{}:{slotMs:a.noteSlotMs},...a.noteHeartbeatMs===void 0?{}:{heartbeatMs:a.noteHeartbeatMs},...r.notes};this.source=r.source??p(new t.Bee(a.beeUrl),j(a.chatAddress),a.chatTopic,s,c.slotMs),this.write=r.write??g(new t.Bee(a.writeUrl??a.beeUrl),a.stamp?j(a.stamp):h,j(a.gsocResourceId),a.gsocTopic,s);let l=r.follower?.now??(()=>Date.now()),u={pollIntervalMs:a.pollingInterval??1e3,...r.follower,now:l},d=this.source;this.notes=w(d)?new E(d,c,l):null,this.follower=new b(this.source,this.topic,{entry:e=>this.show(e),skipped:(e,t,r)=>this.emitter.emit(n.MESSAGE_SKIPPED,{index:e,reason:t,detail:r}),status:e=>this.setStatus(e),error:e=>this.emitter.emit(n.ERROR,e)},u,this.notes),this.history=new S(this.source,this.topic,{skipped:e=>this.emitter.emit(n.MESSAGE_SKIPPED,{index:-1,reason:`history-row`,detail:`${e} rows of a history file`}),error:e=>this.emitter.emit(n.ERROR,e)},this.notes),this.sender=this.makeSender()}getEmitter(){return this.emitter}getStatus(){return this.status}getAddress(){return this.key.publicKey().address().toHex()}start(){if(this.running)return this.starting??Promise.resolve();this.running=!0,this.generation++,this.seen.clear(),this.sender=this.makeSender();let e=this.open(this.generation).finally(()=>{this.starting===e&&(this.starting=null)});return this.starting=e,e}async stop(){this.running=!1,this.generation++,this.openTimer&&=(clearTimeout(this.openTimer),null),this.wakeOpen?.(),this.follower.stop(),this.sender.stop()}async sendMessage(t,n,i){if(n===e.O.REACTION){if(!i)throw Error(`a reaction names the message it reacts to`);return this.sender.react(this.topic,i,t),null}return r(this.sender.send({topic:this.topic,type:n,target:i??``,text:t}))}retrySendMessage(e){return this.sender.retry(typeof e==`string`?e:e.id)}async fetchPreviousMessages(){this.emitter.emit(n.LOADING_PREVIOUS_MESSAGES,!0);try{let e=this.generation,t=await this.history.loadOlder();return e===this.generation?this.showRows(t):[]}finally{this.emitter.emit(n.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasOlder()}orderMessages(e){return[...e].sort((e,t)=>e.index>=0&&t.index>=0?e.index-t.index:e.index>=0||t.index>=0?e.index>=0?-1:1:e.timestamp-t.timestamp)}async open(e){this.emitter.emit(n.LOADING_INIT,!0);let t=0;for(;e===this.generation;)try{let t=await this.history.open();if(e!==this.generation)return;this.showRows(t.rows),this.follower.start(t.startAt,t.notes),this.emitter.emit(n.LOADING_INIT,!1);return}catch(r){if(e!==this.generation)return;t++,this.emitter.emit(n.ERROR,r),this.setStatus(_.RECONNECTING),t===A&&this.emitter.emit(n.CRITICAL_ERROR,r),await this.waitToOpen(this.openRetryMs[Math.min(t,this.openRetryMs.length)-1]??8e3)}}waitToOpen(e){return new Promise(t=>{this.wakeOpen=()=>{this.wakeOpen=null,t()},this.openTimer=setTimeout(()=>{this.openTimer=null,this.wakeOpen?.()},e)})}makeSender(){return new O(this.key,this.profile,this.write,{pending:e=>this.emitter.emit(n.MESSAGE_REQUEST_INITIATED,r(e)),written:e=>this.emitter.emit(n.MESSAGE_REQUEST_UPLOADED,r(e)),failed:(e,t)=>{this.emitter.emit(n.ERROR,t),this.emitter.emit(n.MESSAGE_REQUEST_ERROR,r(e))},confirmed:()=>{}},this.senderSettings)}show(e){this.seen.has(e.seq)||(this.seen.add(e.seq),this.sender.confirm(e.msg),this.emitter.emit(n.MESSAGE_RECEIVED,i(e)))}showRows(e){let t=[];for(let r of e){if(this.seen.has(r.seq))continue;this.seen.add(r.seq),this.sender.confirm(r.msg);let e=i(r);t.push(e),this.emitter.emit(n.MESSAGE_RECEIVED,e)}return t}setStatus(e){e!==this.status&&(this.status=e,this.emitter.emit(n.STATUS,e))}};exports.CHAT_BINDING_TITLE=e.z,exports.ChatEmitter=o,exports.ChatMessageError=e.S,exports.DEFAULT_NOTE_HEARTBEAT_MS=e.t,exports.DEFAULT_NOTE_SLOT_MS=e.n,exports.EVENTS=n,exports.FeedStatus=_,exports.HeadLookupTimeoutError=l,exports.MAX_BINDING_CHARACTERS=e.B,exports.MAX_ENTRY_BYTES=e.p,exports.MAX_MESSAGE_BYTES=e.C,exports.MAX_NAME_CHARACTERS=e.w,exports.MAX_NOTE_BYTES=e.r,exports.MAX_TEXT_CHARACTERS=e.T,exports.MAX_TOPIC_CHARACTERS=e.E,exports.MESSAGE_VERSION=e.D,exports.MessageType=e.O,exports.MessageVia=e.k,exports.NOTE_VERSION=e.i,exports.OWN_FEED_TOPIC_PREFIX=e.b,exports.SwarmChat=M,exports.UnreadableGatewayError=y,exports.UnreadableSlotError=u,exports.chatBindingSchema=e.V,exports.chatMessageSchema=e.A,exports.checkChatBinding=e.H,exports.countCharacters=e.j,exports.createChatMessage=e.M,exports.encodeChatMessage=e.N,exports.encodeSlotNote=e.a,exports.feedEntrySchema=e.m,exports.hasValidSignature=e.P,exports.historyFileSchema=e.h,exports.historyLinkSchema=e.g,exports.historyRowSchema=e._,exports.newMessageId=e.F,exports.noteAddress=e.o,exports.noteIdentifier=e.s,exports.noteIdentifierText=e.c,exports.noteSlotEnd=e.l,exports.noteSlotOf=e.u,exports.ownFeedTopic=e.x,exports.parseChatMessage=e.I,exports.parseFeedEntry=e.v,exports.parseHistoryFile=e.y,exports.parseSlotNote=e.d,exports.signedBytes=e.L,exports.slotNoteSchema=e.f,exports.verifyChatMessage=e.R;
|