@solarpunkltd/swarm-chat-js 6.2.8 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -196
- package/dist/chat.d.ts +106 -0
- package/dist/events.d.ts +80 -0
- package/dist/follower.d.ts +122 -0
- package/dist/history.d.ts +51 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +576 -11
- package/dist/message/entry.d.ts +97 -0
- package/dist/message/index.d.ts +2 -0
- package/dist/message/message.d.ts +90 -0
- package/dist/message.cjs +1 -0
- package/dist/message.js +264 -0
- package/dist/sender.d.ts +62 -0
- package/dist/swarm.d.ts +74 -0
- package/package.json +48 -54
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -6578
- package/dist/interfaces/chat.d.ts +0 -41
- package/dist/interfaces/chat.d.ts.map +0 -1
- package/dist/interfaces/index.d.ts +0 -3
- package/dist/interfaces/index.d.ts.map +0 -1
- package/dist/interfaces/message.d.ts +0 -31
- package/dist/interfaces/message.d.ts.map +0 -1
- package/dist/lib/constants.d.ts +0 -10
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/core.d.ts +0 -31
- package/dist/lib/core.d.ts.map +0 -1
- package/dist/lib/history.d.ts +0 -27
- package/dist/lib/history.d.ts.map +0 -1
- package/dist/lib/utils.d.ts +0 -32
- package/dist/lib/utils.d.ts.map +0 -1
- package/dist/scripts/mine.d.ts +0 -3
- package/dist/scripts/mine.d.ts.map +0 -1
- package/dist/swarm-chat-umd.js +0 -11
- package/dist/transports/MessageTransport.d.ts +0 -7
- package/dist/transports/MessageTransport.d.ts.map +0 -1
- package/dist/transports/PollingTransport.d.ts +0 -20
- package/dist/transports/PollingTransport.d.ts.map +0 -1
- package/dist/utils/bee.d.ts +0 -18
- package/dist/utils/bee.d.ts.map +0 -1
- package/dist/utils/common.d.ts +0 -5
- package/dist/utils/common.d.ts.map +0 -1
- package/dist/utils/error.d.ts +0 -8
- package/dist/utils/error.d.ts.map +0 -1
- package/dist/utils/eventEmitter.d.ts +0 -11
- package/dist/utils/eventEmitter.d.ts.map +0 -1
- package/dist/utils/logger.d.ts +0 -12
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/validation.d.ts +0 -18
- package/dist/utils/validation.d.ts.map +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type FeedEntry, type ReadRefusal } from './message/index.js';
|
|
2
|
+
import { type ChatSource } from './swarm.js';
|
|
3
|
+
export declare const FeedStatus: {
|
|
4
|
+
/** The gateway answers. The next slot may simply not be written yet, which is the ordinary state of a quiet chat. */
|
|
5
|
+
readonly LIVE: 'live';
|
|
6
|
+
/** The gateway does not answer, and reads are being held off between retries. */
|
|
7
|
+
readonly RECONNECTING: 'reconnecting';
|
|
8
|
+
/** The gateway answers, but a message known to exist has not loaded for a while. */
|
|
9
|
+
readonly STALLED: 'stalled';
|
|
10
|
+
};
|
|
11
|
+
export type FeedStatus = (typeof FeedStatus)[keyof typeof FeedStatus];
|
|
12
|
+
/** Why a slot was not shown: its payload failed a check, or it was stepped past and the retry list was full. */
|
|
13
|
+
export type SkipReason = ReadRefusal | 'unreachable';
|
|
14
|
+
export interface FollowerSettings {
|
|
15
|
+
pollIntervalMs: number;
|
|
16
|
+
/** Slots one poll may walk before handing back, which only a viewer catching up on a backlog reaches. */
|
|
17
|
+
maxSlotsPerPoll: number;
|
|
18
|
+
/** Polls the next slot may be refused before asking whether anything lies behind it. */
|
|
19
|
+
unservedPollsBeforeProbe: number;
|
|
20
|
+
/** The longest gap between probes of a refused live edge, however long the chat has been quiet. */
|
|
21
|
+
probeIntervalCapMs: number;
|
|
22
|
+
/** How far past a refused slot to look, stopping at the first that answers. */
|
|
23
|
+
probeDistances: readonly number[];
|
|
24
|
+
/** Polls in a row whose next slot came back unreadable, with nothing readable behind it, before the gateway is failing. */
|
|
25
|
+
unreadablePollsBeforeFailure: number;
|
|
26
|
+
/** Stepped-past slots kept for reading again on later polls. */
|
|
27
|
+
missedLimit: number;
|
|
28
|
+
/** Stepped-past slots read again in one poll. */
|
|
29
|
+
missedReadsPerPoll: number;
|
|
30
|
+
/** How long a stepped-past slot is read again before it is let go and reported unreachable. */
|
|
31
|
+
missedExpiryMs: number;
|
|
32
|
+
backoffCapMs: number;
|
|
33
|
+
/** How long a stepped-past slot may stay unread before the chat is called stalled. */
|
|
34
|
+
stallAfterMs: number;
|
|
35
|
+
random: () => number;
|
|
36
|
+
now: () => number;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_FOLLOWER_SETTINGS: FollowerSettings;
|
|
39
|
+
/** The gateway answers the next slot, poll after poll, with something that is not a chunk. */
|
|
40
|
+
export declare class UnreadableGatewayError extends Error {
|
|
41
|
+
readonly index: number;
|
|
42
|
+
constructor(index: number);
|
|
43
|
+
}
|
|
44
|
+
export interface FollowerEvents {
|
|
45
|
+
entry(entry: FeedEntry): void;
|
|
46
|
+
skipped(index: number, reason: SkipReason, detail: string): void;
|
|
47
|
+
status(status: FeedStatus): void;
|
|
48
|
+
error(error: unknown): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
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
|
|
53
|
+
* keeps being refused, the follower looks a few slots past it, and a later slot that answers means the refused one is
|
|
54
|
+
* 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.
|
|
56
|
+
*/
|
|
57
|
+
export declare class FeedFollower {
|
|
58
|
+
private readonly source;
|
|
59
|
+
private readonly topic;
|
|
60
|
+
private readonly events;
|
|
61
|
+
private next;
|
|
62
|
+
private unservedPolls;
|
|
63
|
+
private nextProbeAt;
|
|
64
|
+
private unreadablePolls;
|
|
65
|
+
private gatewayFailures;
|
|
66
|
+
private readonly missed;
|
|
67
|
+
private timer;
|
|
68
|
+
private generation;
|
|
69
|
+
private running;
|
|
70
|
+
private status;
|
|
71
|
+
private readonly settings;
|
|
72
|
+
constructor(source: ChatSource, topic: string, events: FollowerEvents, settings?: Partial<FollowerSettings>);
|
|
73
|
+
/** The index of the next slot this follower will ask for. */
|
|
74
|
+
get nextIndex(): number;
|
|
75
|
+
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;
|
|
78
|
+
/** Cancels the next poll, and makes a poll in flight discard what it reads. */
|
|
79
|
+
stop(): void;
|
|
80
|
+
private schedule;
|
|
81
|
+
private poll;
|
|
82
|
+
/** Reads forward from the next slot. Returns how many slots it consumed. Throws when the gateway fails. */
|
|
83
|
+
/** A slot's payload, null when refused, or 'unreadable' when it was served and failed its own check. */
|
|
84
|
+
private read;
|
|
85
|
+
/**
|
|
86
|
+
* Reads forward from the next slot. Returns how many slots it showed. Throws when the gateway fails, and when the
|
|
87
|
+
* next slot has come back unreadable on several polls in a row with nothing behind it, which is the gateway answering
|
|
88
|
+
* with something that is not a chunk rather than one bad slot.
|
|
89
|
+
*
|
|
90
|
+
* An unreadable next slot is paced like a refused one: it ends the poll, is counted, and is stepped past only
|
|
91
|
+
* through the probe once a later slot answers. Stepping past it at once let a gateway that answers every path with a
|
|
92
|
+
* web page walk the reader past the head, sixteen slots a poll with no wait between polls.
|
|
93
|
+
*/
|
|
94
|
+
private walk;
|
|
95
|
+
/**
|
|
96
|
+
* Probes at the threshold and then at doubling intervals, three, six, twelve polls and on, until the gap reaches
|
|
97
|
+
* `probeIntervalCapMs`, and at that interval after. A quiet chat stays refused for minutes, and probing each of
|
|
98
|
+
* those polls would cost four reads a poll to find nothing. Doubling without a cap left a hole that opened thirteen
|
|
99
|
+
* minutes into a quiet unfound for up to thirteen more.
|
|
100
|
+
*/
|
|
101
|
+
private shouldProbe;
|
|
102
|
+
private resetRefusals;
|
|
103
|
+
/**
|
|
104
|
+
* Looks past a refused slot. Returns whether a later slot answered. The probe jumps, so the slots it jumped over
|
|
105
|
+
* are read in order before the one that answered: each is shown if it loads and kept for later if it is refused
|
|
106
|
+
* too, since every slot of a chat is a message of its own.
|
|
107
|
+
*/
|
|
108
|
+
private probePast;
|
|
109
|
+
private remember;
|
|
110
|
+
/**
|
|
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.
|
|
114
|
+
*/
|
|
115
|
+
private readMissed;
|
|
116
|
+
private accept;
|
|
117
|
+
private gatewayFailed;
|
|
118
|
+
private gatewayAnswered;
|
|
119
|
+
/** Doubles from the poll interval up to the cap, and draws each wait from its upper half so viewers drift apart. */
|
|
120
|
+
private backoffMs;
|
|
121
|
+
private setStatus;
|
|
122
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type HistoryRow } from './message/index.js';
|
|
2
|
+
import { type ChatSource } from './swarm.js';
|
|
3
|
+
/** Where a chat's reading starts, and what it shows before the first poll. */
|
|
4
|
+
export interface Opening {
|
|
5
|
+
/** The first feed index the follower reads. */
|
|
6
|
+
startAt: number;
|
|
7
|
+
/** Messages of the newest history file, oldest first. */
|
|
8
|
+
rows: HistoryRow[];
|
|
9
|
+
}
|
|
10
|
+
export interface HistoryEvents {
|
|
11
|
+
/** Rows of a history file left out because they failed their checks. */
|
|
12
|
+
skipped(count: number): void;
|
|
13
|
+
/** A history file that could not be read. Opening goes on without it, and loading older messages offers it again. */
|
|
14
|
+
error(error: unknown): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 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.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ChatHistory {
|
|
23
|
+
private readonly source;
|
|
24
|
+
private readonly topic;
|
|
25
|
+
private readonly events;
|
|
26
|
+
private older;
|
|
27
|
+
private loading;
|
|
28
|
+
constructor(source: ChatSource, topic: string, events: HistoryEvents);
|
|
29
|
+
/** Rejects when the gateway fails before anything was learned, so the caller can try again. */
|
|
30
|
+
open(): Promise<Opening>;
|
|
31
|
+
/**
|
|
32
|
+
* 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
|
+
* second that grows with the feed, and was measured at 30 to 48 seconds on a loaded gateway, so a lookup that
|
|
34
|
+
* outlives its timeout is read as nothing known and the chat is read from slot 0, which a chat of an event's size
|
|
35
|
+
* affords. Waiting on it instead left a late joiner's chat unopened. A quick failure still rejects, so it is retried.
|
|
36
|
+
*/
|
|
37
|
+
private readHead;
|
|
38
|
+
/** Whether a click on "load older" has something to load. */
|
|
39
|
+
hasOlder(): boolean;
|
|
40
|
+
/** The file before the oldest one shown, oldest first. Rejects when it cannot be read, and offers it again. */
|
|
41
|
+
loadOlder(): Promise<HistoryRow[]>;
|
|
42
|
+
private loadOlderOnce;
|
|
43
|
+
/**
|
|
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.
|
|
46
|
+
*/
|
|
47
|
+
private newestLink;
|
|
48
|
+
/** The rows of the file `link` names, null when it could not be read now, or 'refused' when it fails its checks. */
|
|
49
|
+
private readFile;
|
|
50
|
+
private report;
|
|
51
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +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},c=!r.source||!r.write?new t.Bee(a.beeUrl):null;this.source=r.source??d(c,T(a.chatAddress),a.chatTopic,s),this.write=r.write??m(c,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;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export type { EthAddress } from '
|
|
8
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export * from './message/index.js';
|
|
2
|
+
export { SwarmChat, type ChatSettings, type ChatParts } from './chat.js';
|
|
3
|
+
export { EVENTS, ChatEmitter, type ChatEvent, type ChatEventPayloads, type MessageData, type SkippedMessage, } from './events.js';
|
|
4
|
+
export { FeedStatus, UnreadableGatewayError, type SkipReason, type FollowerSettings } from './follower.js';
|
|
5
|
+
export type { SenderSettings, GsocWrite } from './sender.js';
|
|
6
|
+
export { HeadLookupTimeoutError, UnreadableSlotError, type ChatSource } from './swarm.js';
|
|
7
|
+
export type { EthAddress } from '@ethersphere/bee-js';
|