@solarpunkltd/swarm-chat-js 7.1.0 → 7.3.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 CHANGED
@@ -15,13 +15,21 @@ the inbox and writes the feed. Version 7 of the library reads and writes the v7
15
15
  bytes are written again every ten seconds, up to five times, and then it is failed with a manual retry.
16
16
  2. **Publishing.** The server checks each message's shape and signature and writes it to the chat's feed as the next
17
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.
18
+ links to the newest file saved. Once a two-second time slot ends in which a message landed, and at least every 30
19
+ seconds while the chat is active, it writes a slot note naming the newest entry. See
20
+ [Slot notes](#slot-notes-for-servers-and-tools).
21
+ 3. **Opening.** A reader reads the notes of the slots just ended, newest first, back as far as one heartbeat. The
22
+ newest note found names the newest entry, whose history file is shown, and the entries after it are read. Only
23
+ when no note is found, which is a server that writes none, does it ask Bee for the feed's head instead.
24
+ 4. **Following.** Once each time slot is over, plus a second, a reader asks for that slot's note once, and reads the
25
+ feed entries up to the newest one a note named, never further. So it never asks Bee for an address before it is
26
+ written. Bee answers such a request by skipping its peers for that address for a minute, which made new messages
27
+ arrive about forty seconds late while readers polled the next slot. An entry a note named that does not load is
28
+ read again later, so a message arrives late rather than never. A slot that fails its checks is skipped and never
29
+ stops the chat. Gateway failures back off with jitter up to eight seconds. A reader that finds no note, which is a
30
+ server that writes none or a chat it has not opened yet, polls the next slot, and is led by notes from the first
31
+ one it finds. Any address that came back missing, a feed slot or a note, is asked for again no sooner than twenty
32
+ seconds later, three times a minute at most, which keeps Bee from skipping its peers for it.
25
33
  5. **Older messages.** Loading older messages is a click, which reads the history file before the oldest one shown.
26
34
 
27
35
  ## Installation
@@ -75,10 +83,12 @@ await chat.stop();
75
83
  | `infra.gsocResourceId` | The mined key every sender signs inbox writes with. The server's operator mines it. |
76
84
  | `infra.chatTopic` | The chat's topic, which every message carries and the feed is named by. |
77
85
  | `infra.chatAddress` | The server's feed owner address. |
78
- | `infra.pollingInterval` | How often a reader polls at the live edge, 1,000 ms by default. |
86
+ | `infra.pollingInterval` | The shortest wait between two polls, and the first wait after the gateway failed, 1,000 ms by default. |
79
87
  | `infra.socReadTimeout` | One feed slot read, 5,000 ms by default. |
80
88
  | `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. |
81
89
  | `infra.gsocWriteTimeout` | One inbox write, 10,000 ms by default. |
90
+ | `infra.noteSlotMs` | The server's `NOTE_SLOT_MS`, 2,000 ms by default. It is part of every note's address, so a different value finds no notes and polls instead. |
91
+ | `infra.noteHeartbeatMs` | The server's `NOTE_HEARTBEAT_MS`, 30,000 ms by default, which sets how far back an opening looks for a note. |
82
92
 
83
93
  ## Methods
84
94
 
@@ -161,6 +171,46 @@ signed with bee-js's `PrivateKey.sign` and checked with `Signature.recoverPublic
161
171
  | `feedEntrySchema`, `parseFeedEntry` | The feed entry, `{v, seq, at, msg, history}`. |
162
172
  | `historyFileSchema`, `parseHistoryFile` | The history file, `{v, topic, fromSeq, toSeq, messages, prev}`. |
163
173
 
174
+ ## Slot notes, for servers and tools
175
+
176
+ A note tells readers which feed entries exist, so they never ask for one that does not. Time slot `s` covers
177
+ `[s * slotMs, (s + 1) * slotMs)` in Unix milliseconds, `slotMs` being 2,000 by default. The note of slot `s` is a
178
+ single owner chunk signed by the chat feed's key, at the identifier keccak256 of the UTF-8 text
179
+ `<chat topic>/note/<slotMs>/<s>`, and is read through `GET /chunks/<address>`. Its payload is
180
+ `{"v":1,"newest":<the highest feed index written>,"writtenAt":<the server's clock in ms>}`, `newest` -1 for a chat with
181
+ nothing written yet, and a payload that fails this strict shape counts as no note.
182
+
183
+ The server writes a slot's note once the slot is over, when an entry landed since its last note that was written, or
184
+ when a heartbeat has passed since that note. An entry's own write has finished before any note names it. A note write
185
+ that fails is not retried at its address: the next slot's note carries the same news.
186
+
187
+ | Export | Use |
188
+ | ---------------------------------------------------------------- | ------------------------------------------------------ |
189
+ | `noteSlotOf(timeMs, slotMs)`, `noteSlotEnd(slot, slotMs)` | The slot arithmetic. |
190
+ | `noteIdentifier(topic, slotMs, slot)`, `noteAddress(..., owner)` | Where a note is written and read. |
191
+ | `encodeSlotNote(note)`, `parseSlotNote(bytes)`, `slotNoteSchema` | The payload, encoded in field order and read strictly. |
192
+ | `DEFAULT_NOTE_SLOT_MS`, `DEFAULT_NOTE_HEARTBEAT_MS` | The defaults both sides share. |
193
+
194
+ **The reader's clock.** A reader schedules note reads by its own clock. Every note's `writtenAt` and every entry's `at`
195
+ was stamped before the reader received it, so the reader runs at most `received - stamped` ahead of the server. While
196
+ notes are found it trusts its own clock, moving reads earlier only when that bound proves the clock runs behind. When
197
+ no note is found for longer than a heartbeat, it waits out the whole bound from then on, which is never early. A
198
+ clock that runs behind sees every stamp as old as it expects, so its messages arrive as late as it runs behind.
199
+
200
+ ## Changes in 7.3
201
+
202
+ - A reader asks Bee for an address that came back missing no more than three times a minute, a feed slot and a note
203
+ alike. Polling the next slot every second, as a reader that found no note did, kept that slot on Bee's skip list,
204
+ so the first messages in a quiet room arrived 40 to 55 s late. A slot a note names is still read at once.
205
+
206
+ ## Changes in 7.2
207
+
208
+ - Readers follow a chat by its slot notes and no longer ask for the next feed slot before it exists, so a new message
209
+ arrives in seconds rather than about forty. Opening needs no head lookup either. A server without notes is followed
210
+ as 7.1 followed it.
211
+ - The note protocol is exported from the root and from `./message` for the server. New settings `infra.noteSlotMs`
212
+ and `infra.noteHeartbeatMs`.
213
+
164
214
  ## Changes from 6.x
165
215
 
166
216
  - Messages are signed over every field and verified by the server. 6.x signed four fields with a double prefix, and its
package/dist/chat.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ChatEmitter, type MessageData } from './events.js';
2
2
  import { FeedStatus, type FollowerSettings } from './follower.js';
3
3
  import { MessageType } from './message/index.js';
4
+ import { type NoteSettings } from './notes.js';
4
5
  import { type GsocWrite, type SenderSettings } from './sender.js';
5
6
  import { type ChatSource } from './swarm.js';
6
7
  export interface ChatSettings {
@@ -28,7 +29,7 @@ export interface ChatSettings {
28
29
  chatTopic: string;
29
30
  /** The server's feed owner address. */
30
31
  chatAddress: string;
31
- /** How often the reader polls at the live edge, 1,000 ms by default. */
32
+ /** The shortest wait between two polls, and the first wait after the gateway failed, 1,000 ms by default. */
32
33
  pollingInterval?: number;
33
34
  /**
34
35
  * The head lookup and a history file download, 15,000 ms by default. An idle Bee answers the head lookup of a
@@ -40,6 +41,10 @@ export interface ChatSettings {
40
41
  gsocWriteTimeout?: number;
41
42
  /** One feed slot read, 5,000 ms by default. */
42
43
  socReadTimeout?: number;
44
+ /** The server's NOTE_SLOT_MS, 2,000 ms by default. It is part of every note's address, so it must match. */
45
+ noteSlotMs?: number;
46
+ /** The server's NOTE_HEARTBEAT_MS, 30,000 ms by default, how far back an opening looks for a note. */
47
+ noteHeartbeatMs?: number;
43
48
  };
44
49
  }
45
50
  /** What a test hands in instead of the network, and the tuning a test shortens. */
@@ -48,6 +53,7 @@ export interface ChatParts {
48
53
  write: GsocWrite;
49
54
  follower: Partial<FollowerSettings>;
50
55
  sender: Partial<SenderSettings>;
56
+ notes: Partial<NoteSettings>;
51
57
  /** The waits between opening attempts, the last repeated. */
52
58
  openRetryMs: readonly number[];
53
59
  }
@@ -66,6 +72,7 @@ export declare class SwarmChat {
66
72
  private readonly openRetryMs;
67
73
  private readonly follower;
68
74
  private readonly history;
75
+ private readonly notes;
69
76
  private sender;
70
77
  private readonly seen;
71
78
  private status;
@@ -1,4 +1,5 @@
1
1
  import { type FeedEntry, type ReadRefusal } from './message/index.js';
2
+ import type { NoteReader } from './notes.js';
2
3
  import { type ChatSource } from './swarm.js';
3
4
  export declare const FeedStatus: {
4
5
  /** The gateway answers. The next slot may simply not be written yet, which is the ordinary state of a quiet chat. */
@@ -12,7 +13,10 @@ export type FeedStatus = (typeof FeedStatus)[keyof typeof FeedStatus];
12
13
  /** Why a slot was not shown: its payload failed a check, or it was stepped past and the retry list was full. */
13
14
  export type SkipReason = ReadRefusal | 'unreachable';
14
15
  export interface FollowerSettings {
16
+ /** The shortest wait between two polls, and the first wait after the gateway failed. */
15
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;
16
20
  /** Slots one poll may walk before handing back, which only a viewer catching up on a backlog reaches. */
17
21
  maxSlotsPerPoll: number;
18
22
  /** Polls the next slot may be refused before asking whether anything lies behind it. */
@@ -41,6 +45,11 @@ export declare class UnreadableGatewayError extends Error {
41
45
  readonly index: number;
42
46
  constructor(index: number);
43
47
  }
48
+ /** Where a follower that reads notes starts: the newest feed slot a note named, and the next time slot to ask. */
49
+ export interface NoteStart {
50
+ newest: number;
51
+ nextNoteSlot: number;
52
+ }
44
53
  export interface FollowerEvents {
45
54
  entry(entry: FeedEntry): void;
46
55
  skipped(index: number, reason: SkipReason, detail: string): void;
@@ -48,17 +57,32 @@ export interface FollowerEvents {
48
57
  error(error: unknown): void;
49
58
  }
50
59
  /**
51
- * Follows a chat feed the way the video player follows its playlist feed: explicit slot reads after the last one
52
- * seen, several in one poll, until one is refused. A refused next slot is the live edge and is polled again. When it
60
+ * Follows a chat feed by explicit slot reads, in one of two ways.
61
+ *
62
+ * Led by notes, when the server writes them: once each time slot is over the follower asks for its note, and reads
63
+ * feed slots only up to the newest one a note has named. Every address it asks for is then either written or never
64
+ * will be, so Bee never learns to skip its peers for a message about to arrive. A slot a note named that does not load
65
+ * goes to the retry list. Silence longer than the heartbeat makes the follower stop trusting its own clock.
66
+ *
67
+ * Polling, when no note has been found, which is how a server without notes is followed: reads after the last slot
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
53
71
  * keeps being refused, the follower looks a few slots past it, and a later slot that answers means the refused one is
54
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
55
- * message skipped arrives late rather than never.
73
+ * message skipped arrives late rather than never. A follower that can read notes keeps asking for them while it polls
74
+ * and is led by them from the first one it finds.
56
75
  */
57
76
  export declare class FeedFollower {
58
77
  private readonly source;
59
78
  private readonly topic;
60
79
  private readonly events;
80
+ private readonly notes;
61
81
  private next;
82
+ /** The newest feed slot a note has named, or null while no note has been found and the feed is polled. */
83
+ private ceiling;
84
+ private nextNoteSlot;
85
+ private lastNoteFoundAt;
62
86
  private unservedPolls;
63
87
  private nextProbeAt;
64
88
  private unreadablePolls;
@@ -69,19 +93,38 @@ export declare class FeedFollower {
69
93
  private running;
70
94
  private status;
71
95
  private readonly settings;
72
- constructor(source: ChatSource, topic: string, events: FollowerEvents, settings?: Partial<FollowerSettings>);
96
+ private readonly pacer;
97
+ constructor(source: ChatSource, topic: string, events: FollowerEvents, settings?: Partial<FollowerSettings>, notes?: NoteReader | null);
73
98
  /** The index of the next slot this follower will ask for. */
74
99
  get nextIndex(): number;
75
100
  get missedIndices(): number[];
76
- /** Starts polling at `fromIndex`, with the first poll at once. Starting again restarts from the new index. */
77
- start(fromIndex: number): void;
101
+ /** Whether notes lead the follower, rather than polling. */
102
+ get ledByNotes(): boolean;
103
+ /**
104
+ * Starts at `fromIndex`, with the first read at once, led by notes from `notes` when the opening found one and
105
+ * polling otherwise. Starting again restarts from the new index.
106
+ */
107
+ start(fromIndex: number, notes?: NoteStart | null): void;
78
108
  /** Cancels the next poll, and makes a poll in flight discard what it reads. */
79
109
  stop(): void;
80
110
  private schedule;
81
111
  private poll;
82
- /** Reads forward from the next slot. Returns how many slots it consumed. Throws when the gateway fails. */
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
+ */
117
+ private untilNextRead;
118
+ /**
119
+ * Asks for the notes of every time slot that has ended since the last ask. One slot is read on its own. Several,
120
+ * after timers were slowed in a hidden tab, are read the way an opening reads them, newest first, back no further
121
+ * than a heartbeat, stopping at the first found, since the newest note names everything an older one does.
122
+ */
123
+ private readDueNotes;
83
124
  /** A slot's payload, null when refused, or 'unreadable' when it was served and failed its own check. */
84
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;
85
128
  /**
86
129
  * Reads forward from the next slot. Returns how many slots it showed. Throws when the gateway fails, and when the
87
130
  * next slot has come back unreadable on several polls in a row with nothing behind it, which is the gateway answering
@@ -108,9 +151,9 @@ export declare class FeedFollower {
108
151
  private probePast;
109
152
  private remember;
110
153
  /**
111
- * Reads the stepped-past slots tried least recently, and lets go of any unread for `missedExpiryMs`. The map keeps
112
- * its entries in the order they were last tried, so a slot that never loads moves to the back and cannot keep the
113
- * 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.
114
157
  */
115
158
  private readMissed;
116
159
  private accept;
package/dist/history.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type { NoteStart } from './follower.js';
1
2
  import { type HistoryRow } from './message/index.js';
3
+ import type { NoteReader } from './notes.js';
2
4
  import { type ChatSource } from './swarm.js';
3
5
  /** Where a chat's reading starts, and what it shows before the first poll. */
4
6
  export interface Opening {
@@ -6,6 +8,8 @@ export interface Opening {
6
8
  startAt: number;
7
9
  /** Messages of the newest history file, oldest first. */
8
10
  rows: HistoryRow[];
11
+ /** Where notes lead the follower from, or null when no note was found and the chat is followed by polling. */
12
+ notes: NoteStart | null;
9
13
  }
10
14
  export interface HistoryEvents {
11
15
  /** Rows of a history file left out because they failed their checks. */
@@ -15,19 +19,29 @@ export interface HistoryEvents {
15
19
  }
16
20
  /**
17
21
  * Opens a chat from the newest history file and the feed entries after it, and hands out older files one click at a
18
- * time. Opening asks Bee's head lookup once. A 404 there is not taken on its own, because Bee answers 404 for a
19
- * lookup that failed as well as for a feed with no update, so the chat is then read from slot 0, which is missing only
20
- * when the chat is empty.
22
+ * time.
23
+ *
24
+ * Opening reads the notes of the time slots just ended, newest first, back as far as one heartbeat, and the newest
25
+ * note found names the newest feed slot, whose entry links the newest history file. That asks Bee for nothing that is
26
+ * not written. Only when no note is found, which is a server that writes none, does opening ask Bee's head lookup
27
+ * once, which itself asks for slots not yet written. A 404 there is not taken on its own, because Bee answers 404 for
28
+ * a lookup that failed as well as for a feed with no update, so the chat is then read from slot 0, which is missing
29
+ * only when the chat is empty.
21
30
  */
22
31
  export declare class ChatHistory {
23
32
  private readonly source;
24
33
  private readonly topic;
25
34
  private readonly events;
35
+ private readonly notes;
26
36
  private older;
27
37
  private loading;
28
- constructor(source: ChatSource, topic: string, events: HistoryEvents);
38
+ constructor(source: ChatSource, topic: string, events: HistoryEvents, notes?: NoteReader | null);
29
39
  /** Rejects when the gateway fails before anything was learned, so the caller can try again. */
30
40
  open(): Promise<Opening>;
41
+ /** The newest note of the slots just ended, as where the follower starts, or null when there is none. */
42
+ private newestNote;
43
+ /** Opens from the history file the entry at or below `headIndex` links, the head's payload when already read. */
44
+ private openAt;
31
45
  /**
32
46
  * The head, or null for a lookup that found nothing or ran out of time. Bee's head lookup has a floor of about a
33
47
  * second that grows with the feed, and was measured at 30 to 48 seconds on a loaded gateway, so a lookup that
@@ -41,8 +55,8 @@ export declare class ChatHistory {
41
55
  loadOlder(): Promise<HistoryRow[]>;
42
56
  private loadOlderOnce;
43
57
  /**
44
- * The history link of the newest readable entry at or below the head. Null means no history file is named, so the
45
- * chat is read from its first slot.
58
+ * The history link of the newest readable entry at or below the head, reading the head first when its payload is
59
+ * not given. Null means no history file is named, so the chat is read from its first slot.
46
60
  */
47
61
  private newestLink;
48
62
  /** The rows of the file `link` names, null when it could not be read now, or 'refused' when it fails its checks. */
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(e,n,r,i){let a=t.Topic.fromString(r),o=t=>e.feed.makeReader(a,n,f(t));return{readSlot:e=>u(e,async()=>(await o(i.slotReadMs).downloadPayload({index:e})).payload.toUint8Array()),async readHead(){let t=f(i.feedReadMs);try{let{payload:r,feedIndex:i}=await e.feed.makeReader(a,n,t).downloadPayload();return{index:Number(i.toBigInt()),payload:r.toUint8Array()}}catch(e){if(l(e))return null;throw t.signal?.aborted?new s({cause:e}):e}},async readFile(t){return(await e.data.download(t,void 0,f(i.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={}){this.source=e,this.topic=t,this.events=n,this.next=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()]}start(e){this.stop(),this.next=e,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{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.settings.pollIntervalMs)}}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,r=await this.read(n);if(e!==this.generation)return t;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.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){this.source=e,this.topic=t,this.events=n,this.older=null,this.loading=null}async open(){this.older=null;let e=await this.readHead();if(e===null)return{startAt:0,rows:[]};let t=await this.newestLink(e.index,e.payload);if(t===null)return{startAt:0,rows:[]};let n=await this.readFile(t);return n===`refused`?{startAt:0,rows:[]}:{startAt:t.toSeq+1,rows:n??[]}}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=e.parseFeedEntry(n,t,this.topic);if(r.ok)return r.value.history;for(let n=t-1;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={resendAfterMs:1e4,maxResends:5,reactionWindowMs:1e3,isLive:()=>!0},S=class{constructor(e,t,n,r,i=x){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)}},C=[1e3,2e3,4e3,8e3],w=3;function T(e){return(e.startsWith(`0x`)?e.slice(2):e).toLowerCase()}var E=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(T(i.privateKey)),this.topic=a.chatTopic,this.nickname=i.nickname,this.openRetryMs=r.openRetryMs??C,this.senderSettings={...x,isLive:()=>this.status===h.LIVE,...r.sender};let s={slotReadMs:a.socReadTimeout??5e3,feedReadMs:a.feedReadTimeout??15e3,writeMs:a.gsocWriteTimeout??1e4};this.source=r.source??d(new t.Bee(a.beeUrl),T(a.chatAddress),a.chatTopic,s),this.write=r.write??m(new t.Bee(a.writeUrl??a.beeUrl),a.stamp?T(a.stamp):p,T(a.gsocResourceId),a.gsocTopic,s),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)},{pollIntervalMs:a.pollingInterval??1e3,...r.follower}),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.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),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===w&&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 S(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.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_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.SwarmChat=E,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.feedEntrySchema=e.feedEntrySchema,exports.hasValidSignature=e.hasValidSignature,exports.historyFileSchema=e.historyFileSchema,exports.historyLinkSchema=e.historyLinkSchema,exports.historyRowSchema=e.historyRowSchema,exports.newMessageId=e.newMessageId,exports.parseChatMessage=e.parseChatMessage,exports.parseFeedEntry=e.parseFeedEntry,exports.parseHistoryFile=e.parseHistoryFile,exports.signedBytes=e.signedBytes;
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=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.DEFAULT_NOTE_SLOT_MS){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.noteIdentifier(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.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(_.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.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-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.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)}},C={slotMs:e.DEFAULT_NOTE_SLOT_MS,heartbeatMs:e.DEFAULT_NOTE_HEARTBEAT_MS,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.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){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.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}},D={resendAfterMs:1e4,maxResends:5,reactionWindowMs:1e3,isLive:()=>!0},O=class{constructor(e,t,n,r,i=D){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)}},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.nickname=i.nickname,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.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(_.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.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=_,exports.HeadLookupTimeoutError=l,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=M,exports.UnreadableGatewayError=y,exports.UnreadableSlotError=u,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;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './message/index.js';
2
2
  export { SwarmChat, type ChatSettings, type ChatParts } from './chat.js';
3
3
  export { EVENTS, ChatEmitter, type ChatEvent, type ChatEventPayloads, type MessageData, type SkippedMessage, } from './events.js';
4
4
  export { FeedStatus, UnreadableGatewayError, type SkipReason, type FollowerSettings } from './follower.js';
5
+ export type { NoteSettings } from './notes.js';
5
6
  export type { SenderSettings, GsocWrite } from './sender.js';
6
7
  export { HeadLookupTimeoutError, UnreadableSlotError, type ChatSource } from './swarm.js';
7
8
  export type { EthAddress } from '@ethersphere/bee-js';