@solarpunkltd/swarm-chat-js 7.2.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
@@ -25,9 +25,11 @@ the inbox and writes the feed. Version 7 of the library reads and writes the v7
25
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
26
  written. Bee answers such a request by skipping its peers for that address for a minute, which made new messages
27
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 on later reads, so a message arrives late rather than never. A slot that fails its checks is skipped
29
- and never stops the chat. Gateway failures back off with jitter up to eight seconds. A reader of a server that
30
- writes no notes polls the next slot as 7.1 did, and is led by notes from the first one it finds.
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.
31
33
  5. **Older messages.** Loading older messages is a click, which reads the history file before the oldest one shown.
32
34
 
33
35
  ## Installation
@@ -81,7 +83,7 @@ await chat.stop();
81
83
  | `infra.gsocResourceId` | The mined key every sender signs inbox writes with. The server's operator mines it. |
82
84
  | `infra.chatTopic` | The chat's topic, which every message carries and the feed is named by. |
83
85
  | `infra.chatAddress` | The server's feed owner address. |
84
- | `infra.pollingInterval` | How often a reader of a server without notes 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. |
85
87
  | `infra.socReadTimeout` | One feed slot read, 5,000 ms by default. |
86
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. |
87
89
  | `infra.gsocWriteTimeout` | One inbox write, 10,000 ms by default. |
@@ -195,6 +197,12 @@ notes are found it trusts its own clock, moving reads earlier only when that bou
195
197
  no note is found for longer than a heartbeat, it waits out the whole bound from then on, which is never early. A
196
198
  clock that runs behind sees every stamp as old as it expects, so its messages arrive as late as it runs behind.
197
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
+
198
206
  ## Changes in 7.2
199
207
 
200
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
package/dist/chat.d.ts CHANGED
@@ -29,7 +29,7 @@ export interface ChatSettings {
29
29
  chatTopic: string;
30
30
  /** The server's feed owner address. */
31
31
  chatAddress: string;
32
- /** 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. */
33
33
  pollingInterval?: number;
34
34
  /**
35
35
  * The head lookup and a history file download, 15,000 ms by default. An idle Bee answers the head lookup of a
@@ -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 polled again. When it
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
- /** The wait before the next read: the poll interval while polling, the next note's time while led by notes. */
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 `missedExpiryMs`. The map keeps
143
- * its entries in the order they were last tried, so a slot that never loads moves to the back and cannot keep the
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.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.js CHANGED
@@ -1,7 +1,7 @@
1
- import { ChatMessageError as e, DEFAULT_NOTE_HEARTBEAT_MS as t, DEFAULT_NOTE_SLOT_MS as n, MAX_ENTRY_BYTES as r, MAX_MESSAGE_BYTES as i, MAX_NAME_CHARACTERS as a, MAX_NOTE_BYTES as o, MAX_TEXT_CHARACTERS as s, MAX_TOPIC_CHARACTERS as c, MESSAGE_VERSION as l, MessageType as u, NOTE_VERSION as d, chatMessageSchema as f, checkChatMessage as p, countCharacters as m, createChatMessage as h, encodeChatMessage as g, encodeSlotNote as _, feedEntrySchema as v, hasValidSignature as ee, historyFileSchema as te, historyLinkSchema as y, historyRowSchema as ne, newMessageId as b, noteAddress as x, noteIdentifier as S, noteIdentifierText as C, noteSlotEnd as w, noteSlotOf as T, parseChatMessage as E, parseFeedEntry as D, parseHistoryFile as O, parseSlotNote as k, signedBytes as A, slotNoteSchema as re } from "./message.js";
2
- import { Bee as j, BeeResponseError as M, Identifier as N, PrivateKey as P, Topic as F } from "@ethersphere/bee-js";
1
+ import { ChatMessageError as e, DEFAULT_NOTE_HEARTBEAT_MS as t, DEFAULT_NOTE_SLOT_MS as n, MAX_ENTRY_BYTES as r, MAX_MESSAGE_BYTES as i, MAX_NAME_CHARACTERS as a, MAX_NOTE_BYTES as o, MAX_TEXT_CHARACTERS as s, MAX_TOPIC_CHARACTERS as c, MESSAGE_VERSION as l, MessageType as u, NOTE_VERSION as d, chatMessageSchema as f, checkChatMessage as p, countCharacters as m, createChatMessage as h, encodeChatMessage as g, encodeSlotNote as _, feedEntrySchema as ee, hasValidSignature as te, historyFileSchema as ne, historyLinkSchema as v, historyRowSchema as y, newMessageId as b, noteAddress as x, noteIdentifier as S, noteIdentifierText as re, noteSlotEnd as C, noteSlotOf as w, parseChatMessage as T, parseFeedEntry as E, parseHistoryFile as D, parseSlotNote as O, signedBytes as k, slotNoteSchema as A } from "./message.js";
2
+ import { Bee as j, BeeResponseError as M, Identifier as ie, PrivateKey as N, Topic as P } from "@ethersphere/bee-js";
3
3
  //#region src/events.ts
4
- var I = {
4
+ var F = {
5
5
  LOADING_INIT: "loadingInit",
6
6
  LOADING_PREVIOUS_MESSAGES: "loadingPreviousMessages",
7
7
  MESSAGE_RECEIVED: "messageReceived",
@@ -13,13 +13,13 @@ var I = {
13
13
  MESSAGE_SKIPPED: "messageSkipped",
14
14
  ERROR: "error"
15
15
  };
16
- function L(e) {
17
- return z(e, -1, e.ts);
16
+ function I(e) {
17
+ return R(e, -1, e.ts);
18
18
  }
19
- function R(e) {
20
- return z(e.msg, e.seq, e.at);
19
+ function L(e) {
20
+ return R(e.msg, e.seq, e.at);
21
21
  }
22
- function z(e, t, n) {
22
+ function R(e, t, n) {
23
23
  return {
24
24
  id: e.id,
25
25
  type: e.type,
@@ -34,7 +34,7 @@ function z(e, t, n) {
34
34
  sentAt: e.ts
35
35
  };
36
36
  }
37
- var B = class {
37
+ var z = class {
38
38
  constructor() {
39
39
  this.listeners = /* @__PURE__ */ new Map(), this.on = (e, t) => {
40
40
  this.listeners.set(e, [...this.listeners.get(e) ?? [], t]);
@@ -51,33 +51,57 @@ var B = class {
51
51
  console.error(`a listener for ${e} threw`, t);
52
52
  }
53
53
  }
54
- }, V = class extends Error {
54
+ }, B = 2e4, V = class {
55
+ constructor(e, t) {
56
+ this.spacingMs = e, this.now = t, this.missedAt = /* @__PURE__ */ new Map();
57
+ }
58
+ readableAt(e, t = this.now()) {
59
+ let n = this.missedAt.get(e);
60
+ return n === void 0 || this.expired(n, t) ? -Infinity : n + this.spacingMs;
61
+ }
62
+ mayRead(e, t = this.now()) {
63
+ return t >= this.readableAt(e, t);
64
+ }
65
+ missed(e, t = this.now()) {
66
+ for (let [e, n] of this.missedAt) this.expired(n, t) && this.missedAt.delete(e);
67
+ this.missedAt.set(e, t);
68
+ }
69
+ found(e) {
70
+ this.missedAt.delete(e);
71
+ }
72
+ clear() {
73
+ this.missedAt.clear();
74
+ }
75
+ expired(e, t) {
76
+ return e > t || t - e >= this.spacingMs;
77
+ }
78
+ }, H = class extends Error {
55
79
  constructor(e) {
56
80
  super("the feed head lookup did not answer in time", e), this.name = "HeadLookupTimeoutError";
57
81
  }
58
- }, H = class extends Error {
82
+ }, U = class extends Error {
59
83
  constructor(e, t) {
60
84
  super(`feed slot ${e} was served but could not be read`, t), this.index = e, this.name = "UnreadableSlotError";
61
85
  }
62
86
  };
63
- function U(e) {
87
+ function W(e) {
64
88
  return e instanceof M && (e.status === 404 || e.status === 500);
65
89
  }
66
- async function W(e, t) {
90
+ async function G(e, t) {
67
91
  try {
68
92
  return await t();
69
93
  } catch (t) {
70
- if (U(t)) return null;
71
- throw t instanceof M ? t : new H(e, { cause: t });
94
+ if (W(t)) return null;
95
+ throw t instanceof M ? t : new U(e, { cause: t });
72
96
  }
73
97
  }
74
- function G(e, t, r, i, a = n) {
75
- let o = F.fromString(r), s = (n) => e.feed.makeReader(o, t, K(n));
98
+ function K(e, t, r, i, a = n) {
99
+ let o = P.fromString(r), s = (n) => e.feed.makeReader(o, t, q(n));
76
100
  return {
77
- readNote: (n) => W(n, async () => (await e.soc.makeReader(t, K(i.slotReadMs)).download(S(r, a, n))).payload.toUint8Array()),
78
- readSlot: (e) => W(e, async () => (await s(i.slotReadMs).downloadPayload({ index: e })).payload.toUint8Array()),
101
+ readNote: (n) => G(n, async () => (await e.soc.makeReader(t, q(i.slotReadMs)).download(S(r, a, n))).payload.toUint8Array()),
102
+ readSlot: (e) => G(e, async () => (await s(i.slotReadMs).downloadPayload({ index: e })).payload.toUint8Array()),
79
103
  async readHead() {
80
- let n = K(i.feedReadMs);
104
+ let n = q(i.feedReadMs);
81
105
  try {
82
106
  let { payload: r, feedIndex: i } = await e.feed.makeReader(o, t, n).downloadPayload();
83
107
  return {
@@ -85,33 +109,34 @@ function G(e, t, r, i, a = n) {
85
109
  payload: r.toUint8Array()
86
110
  };
87
111
  } catch (e) {
88
- if (U(e)) return null;
89
- throw n.signal?.aborted ? new V({ cause: e }) : e;
112
+ if (W(e)) return null;
113
+ throw n.signal?.aborted ? new H({ cause: e }) : e;
90
114
  }
91
115
  },
92
116
  async readFile(t) {
93
- return (await e.data.download(t, void 0, K(i.feedReadMs))).toUint8Array();
117
+ return (await e.data.download(t, void 0, q(i.feedReadMs))).toUint8Array();
94
118
  }
95
119
  };
96
120
  }
97
- function K(e) {
121
+ function q(e) {
98
122
  return { signal: AbortSignal.timeout(e) };
99
123
  }
100
- var ie = "0123456789abcdef".repeat(4);
101
- function q(e, t, n, r, i) {
102
- let a = new P(n), o = N.fromString(r);
124
+ var ae = "0123456789abcdef".repeat(4);
125
+ function J(e, t, n, r, i) {
126
+ let a = new N(n), o = ie.fromString(r);
103
127
  return async (n) => {
104
- await e.messaging.gsocSend(t, a, o, n, { deferred: !1 }, K(i.writeMs));
128
+ await e.messaging.gsocSend(t, a, o, n, { deferred: !1 }, q(i.writeMs));
105
129
  };
106
130
  }
107
131
  //#endregion
108
132
  //#region src/follower.ts
109
- var J = {
133
+ var Y = {
110
134
  LIVE: "live",
111
135
  RECONNECTING: "reconnecting",
112
136
  STALLED: "stalled"
113
- }, Y = {
137
+ }, oe = {
114
138
  pollIntervalMs: 1e3,
139
+ missingReadSpacingMs: B,
115
140
  maxSlotsPerPoll: 16,
116
141
  unservedPollsBeforeProbe: 3,
117
142
  unreadablePollsBeforeFailure: 3,
@@ -133,12 +158,12 @@ var J = {
133
158
  constructor(e) {
134
159
  super(`the gateway keeps answering feed slot ${e} with something that is not a chunk`), this.index = e, this.name = "UnreadableGatewayError";
135
160
  }
136
- }, ae = class {
161
+ }, se = class {
137
162
  constructor(e, t, n, r = {}, i = null) {
138
163
  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 = /* @__PURE__ */ new Map(), this.timer = null, this.generation = 0, this.running = !1, this.status = null, this.settings = {
139
- ...Y,
164
+ ...oe,
140
165
  ...r
141
- };
166
+ }, this.pacer = new V(this.settings.missingReadSpacingMs, this.settings.now);
142
167
  }
143
168
  get nextIndex() {
144
169
  return this.next;
@@ -150,7 +175,7 @@ var J = {
150
175
  return this.ceiling !== null;
151
176
  }
152
177
  start(e, t = null) {
153
- 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);
178
+ 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);
154
179
  }
155
180
  stop() {
156
181
  this.running = !1, this.generation++, this.timer &&= (clearTimeout(this.timer), null);
@@ -177,7 +202,11 @@ var J = {
177
202
  }
178
203
  }
179
204
  untilNextRead() {
180
- return this.ceiling === null || this.notes === null ? this.settings.pollIntervalMs : Math.max(0, this.notes.readableAt(this.nextNoteSlot) - this.settings.now());
205
+ let e = this.settings.now(), t = (t) => {
206
+ let n = this.pacer.readableAt(t);
207
+ return n > e ? n : e + this.settings.pollIntervalMs;
208
+ }, n = [...this.missed.keys()].map(t);
209
+ 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);
181
210
  }
182
211
  async readDueNotes(e, t) {
183
212
  let n = this.settings.now(), r = e.lastReadableSlot(n);
@@ -192,19 +221,24 @@ var J = {
192
221
  }
193
222
  }
194
223
  async read(e) {
224
+ let t;
195
225
  try {
196
- return await this.source.readSlot(e);
226
+ t = await this.source.readSlot(e);
197
227
  } catch (e) {
198
- if (e instanceof H) return "unreadable";
228
+ if (e instanceof U) return "unreadable";
199
229
  throw e;
200
230
  }
231
+ return t === null ? this.pacer.missed(e) : this.pacer.found(e), t;
232
+ }
233
+ async readUnnamed(e) {
234
+ return this.pacer.mayRead(e) ? this.read(e) : "held";
201
235
  }
202
236
  async walk(e) {
203
237
  for (let t = 0; t < this.settings.maxSlotsPerPoll; t++) {
204
238
  let n = this.next;
205
239
  if (this.ceiling !== null && n > this.ceiling) return t;
206
- let r = await this.read(n);
207
- if (e !== this.generation) return t;
240
+ let r = this.ceiling === null ? await this.readUnnamed(n) : await this.read(n);
241
+ if (e !== this.generation || r === "held") return t;
208
242
  if (this.ceiling !== null && (r === null || r === "unreadable")) {
209
243
  this.remember(n), this.next = n + 1;
210
244
  continue;
@@ -225,8 +259,8 @@ var J = {
225
259
  }
226
260
  shouldProbe() {
227
261
  if (this.unservedPolls !== this.nextProbeAt) return !1;
228
- let { probeIntervalCapMs: e, pollIntervalMs: t } = this.settings, n = Math.max(1, Math.ceil(e / t));
229
- return this.nextProbeAt += Math.min(this.nextProbeAt, n), !0;
262
+ let { probeIntervalCapMs: e, pollIntervalMs: t, missingReadSpacingMs: n } = this.settings, r = Math.max(1, Math.ceil(e / Math.max(t, n)));
263
+ return this.nextProbeAt += Math.min(this.nextProbeAt, r), !0;
230
264
  }
231
265
  resetRefusals() {
232
266
  this.unservedPolls = 0, this.nextProbeAt = this.settings.unservedPollsBeforeProbe;
@@ -234,17 +268,17 @@ var J = {
234
268
  async probePast(e, t) {
235
269
  let n = /* @__PURE__ */ new Set([e]);
236
270
  for (let r of this.settings.probeDistances) {
237
- let i = e + r, a = await this.read(i);
271
+ let i = e + r, a = await this.readUnnamed(i);
238
272
  if (t !== this.generation) return !1;
239
- if (a === null || a === "unreadable") {
273
+ if (a === null || a === "unreadable" || a === "held") {
240
274
  n.add(i);
241
275
  continue;
242
276
  }
243
277
  this.resetRefusals(), this.remember(e);
244
278
  for (let r = e + 1; r < i; r++) {
245
- let e = n.has(r) ? null : await this.read(r);
279
+ let e = n.has(r) ? null : await this.readUnnamed(r);
246
280
  if (t !== this.generation) return !1;
247
- e === null || e === "unreadable" ? this.remember(r) : this.accept(r, e);
281
+ e === null || e === "unreadable" || e === "held" ? this.remember(r) : this.accept(r, e);
248
282
  }
249
283
  return this.next = i + 1, this.accept(i, a), !0;
250
284
  }
@@ -262,7 +296,7 @@ var J = {
262
296
  async readMissed(e) {
263
297
  let t = this.settings.now();
264
298
  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"));
265
- let n = [...this.missed.keys()].slice(0, this.settings.missedReadsPerPoll);
299
+ let n = [...this.missed.keys()].filter((e) => this.pacer.mayRead(e, t)).slice(0, this.settings.missedReadsPerPoll);
266
300
  for (let t of n) {
267
301
  let n = await this.read(t);
268
302
  if (e !== this.generation) return;
@@ -271,16 +305,16 @@ var J = {
271
305
  }
272
306
  }
273
307
  accept(e, t) {
274
- let n = D(t, e, this.topic);
308
+ let n = E(t, e, this.topic);
275
309
  n.ok ? (this.notes?.clock.observe(n.value.at, this.settings.now()), this.events.entry(n.value)) : this.events.skipped(e, n.reason, n.detail);
276
310
  }
277
311
  gatewayFailed(e) {
278
- this.gatewayFailures++, this.setStatus(J.RECONNECTING), this.events.error(e);
312
+ this.gatewayFailures++, this.setStatus(Y.RECONNECTING), this.events.error(e);
279
313
  }
280
314
  gatewayAnswered() {
281
315
  this.gatewayFailures = 0;
282
316
  let e = this.settings.now(), t = [...this.missed.values()].some((t) => e - t >= this.settings.stallAfterMs);
283
- this.setStatus(t ? J.STALLED : J.LIVE);
317
+ this.setStatus(t ? Y.STALLED : Y.LIVE);
284
318
  }
285
319
  backoffMs() {
286
320
  let { pollIntervalMs: e, backoffCapMs: t, random: n } = this.settings, r = Math.min(t, e * 2 ** this.gatewayFailures);
@@ -289,7 +323,7 @@ var J = {
289
323
  setStatus(e) {
290
324
  e !== this.status && (this.status = e, this.events.status(e));
291
325
  }
292
- }, oe = 16, se = class {
326
+ }, ce = 16, le = class {
293
327
  constructor(e, t, n, r = null) {
294
328
  this.source = e, this.topic = t, this.events = n, this.notes = r, this.older = null, this.loading = null;
295
329
  }
@@ -334,7 +368,7 @@ var J = {
334
368
  try {
335
369
  return await this.source.readHead();
336
370
  } catch (e) {
337
- if (e instanceof V) return this.events.error(e), null;
371
+ if (e instanceof H) return this.events.error(e), null;
338
372
  throw e;
339
373
  }
340
374
  }
@@ -349,30 +383,30 @@ var J = {
349
383
  async loadOlderOnce() {
350
384
  let e = this.older;
351
385
  if (e === null) return [];
352
- let t = await this.source.readFile(e.ref), n = O(t, this.topic, e);
386
+ let t = await this.source.readFile(e.ref), n = D(t, this.topic, e);
353
387
  if (!n.ok) throw this.older = null, Error(`history file ${e.ref} refused (${n.reason}): ${n.detail}`);
354
388
  return this.report(n.value.skipped), this.older = n.value.prev, n.value.rows;
355
389
  }
356
390
  async newestLink(e, t) {
357
391
  let n = t === void 0 ? e : e - 1;
358
392
  if (t !== void 0) {
359
- let n = D(t, e, this.topic);
393
+ let n = E(t, e, this.topic);
360
394
  if (n.ok) return n.value.history;
361
395
  }
362
- for (let t = n; t >= Math.max(0, e - oe); t--) {
396
+ for (let t = n; t >= Math.max(0, e - ce); t--) {
363
397
  let e = await this.source.readSlot(t).catch((e) => {
364
- if (e instanceof H) return null;
398
+ if (e instanceof U) return null;
365
399
  throw e;
366
400
  });
367
401
  if (e === null) continue;
368
- let n = D(e, t, this.topic);
402
+ let n = E(e, t, this.topic);
369
403
  if (n.ok) return n.value.history;
370
404
  }
371
405
  return null;
372
406
  }
373
407
  async readFile(e) {
374
408
  try {
375
- let t = O(await this.source.readFile(e.ref), this.topic, e);
409
+ let t = D(await this.source.readFile(e.ref), this.topic, e);
376
410
  return t.ok ? (this.report(t.value.skipped), this.older = t.value.prev, t.value.rows) : (this.events.error(/* @__PURE__ */ Error(`history file ${e.ref} refused (${t.reason}): ${t.detail}`)), "refused");
377
411
  } catch (t) {
378
412
  return this.events.error(t), this.older = e, null;
@@ -381,17 +415,18 @@ var J = {
381
415
  report(e) {
382
416
  e > 0 && this.events.skipped(e);
383
417
  }
384
- }, ce = {
418
+ }, ue = {
385
419
  slotMs: n,
386
420
  heartbeatMs: t,
387
421
  marginMs: 1e3,
388
422
  scanBatch: 4,
389
- clockLimitMs: 3e5
423
+ clockLimitMs: 3e5,
424
+ missingReadSpacingMs: B
390
425
  };
391
- function le(e) {
426
+ function Z(e) {
392
427
  return typeof e.readNote == "function";
393
428
  }
394
- var ue = class {
429
+ var de = class {
395
430
  constructor(e) {
396
431
  this.limitMs = e, this.aheadAtMost = Infinity, this.trusted = !0;
397
432
  }
@@ -408,21 +443,21 @@ var ue = class {
408
443
  let e = Number.isFinite(this.aheadAtMost), t = this.trusted || !e ? Math.min(0, this.aheadAtMost) : this.aheadAtMost;
409
444
  return Math.max(-this.limitMs, Math.min(this.limitMs, t));
410
445
  }
411
- }, Z = class {
446
+ }, fe = class {
412
447
  constructor(e, t, n) {
413
448
  this.source = e, this.now = n, this.counts = {
414
449
  found: 0,
415
450
  empty: 0
416
451
  }, this.settings = {
417
- ...ce,
452
+ ...ue,
418
453
  ...t
419
- }, this.clock = new ue(this.settings.clockLimitMs);
454
+ }, this.clock = new de(this.settings.clockLimitMs), this.pacer = new V(this.settings.missingReadSpacingMs, n);
420
455
  }
421
456
  lastReadableSlot(e = this.now()) {
422
- return T(e - this.settings.marginMs - this.clock.ms, this.settings.slotMs) - 1;
457
+ return w(e - this.settings.marginMs - this.clock.ms, this.settings.slotMs) - 1;
423
458
  }
424
459
  readableAt(e) {
425
- return w(e, this.settings.slotMs) + this.settings.marginMs + this.clock.ms;
460
+ return C(e, this.settings.slotMs) + this.settings.marginMs + this.clock.ms;
426
461
  }
427
462
  get lookBackSlots() {
428
463
  return Math.ceil(this.settings.heartbeatMs / this.settings.slotMs) + 2;
@@ -431,14 +466,15 @@ var ue = class {
431
466
  return this.settings.heartbeatMs + 2 * this.settings.slotMs + this.settings.marginMs;
432
467
  }
433
468
  async read(e) {
469
+ if (!this.pacer.mayRead(e)) return null;
434
470
  let t;
435
471
  try {
436
- t = await this.source.readNote(e);
472
+ t = await this.source.readNote(e), t === null && this.pacer.missed(e);
437
473
  } catch (e) {
438
- if (!(e instanceof H)) throw e;
474
+ if (!(e instanceof U)) throw e;
439
475
  t = null;
440
476
  }
441
- let n = t === null ? null : k(t);
477
+ let n = t === null ? null : O(t);
442
478
  return n === null ? (this.counts.empty++, null) : (this.counts.found++, this.clock.observe(n.writtenAt, this.now()), n);
443
479
  }
444
480
  async scan(e, t) {
@@ -458,7 +494,7 @@ var ue = class {
458
494
  maxResends: 5,
459
495
  reactionWindowMs: 1e3,
460
496
  isLive: () => !0
461
- }, de = class {
497
+ }, pe = class {
462
498
  constructor(e, t, n, r, i = Q) {
463
499
  this.key = e, this.name = t, this.write = n, this.events = r, this.settings = i, this.outgoing = /* @__PURE__ */ new Map(), this.reactions = /* @__PURE__ */ new Map(), this.stopped = !1;
464
500
  }
@@ -545,22 +581,22 @@ var ue = class {
545
581
  clearTimer(e) {
546
582
  e.timer &&= (clearTimeout(e.timer), null);
547
583
  }
548
- }, fe = [
584
+ }, me = [
549
585
  1e3,
550
586
  2e3,
551
587
  4e3,
552
588
  8e3
553
- ], pe = 3;
589
+ ], he = 3;
554
590
  function $(e) {
555
591
  return (e.startsWith("0x") ? e.slice(2) : e).toLowerCase();
556
592
  }
557
- var me = class {
593
+ var ge = class {
558
594
  constructor(e, t = {}) {
559
- this.emitter = new B(), this.seen = /* @__PURE__ */ new Set(), this.status = null, this.generation = 0, this.running = !1, this.starting = null, this.openTimer = null, this.wakeOpen = null;
595
+ this.emitter = new z(), this.seen = /* @__PURE__ */ new Set(), this.status = null, this.generation = 0, this.running = !1, this.starting = null, this.openTimer = null, this.wakeOpen = null;
560
596
  let { user: n, infra: r } = e;
561
- this.key = new P($(n.privateKey)), this.topic = r.chatTopic, this.nickname = n.nickname, this.openRetryMs = t.openRetryMs ?? fe, this.senderSettings = {
597
+ this.key = new N($(n.privateKey)), this.topic = r.chatTopic, this.nickname = n.nickname, this.openRetryMs = t.openRetryMs ?? me, this.senderSettings = {
562
598
  ...Q,
563
- isLive: () => this.status === J.LIVE,
599
+ isLive: () => this.status === Y.LIVE,
564
600
  ...t.sender
565
601
  };
566
602
  let i = {
@@ -572,28 +608,28 @@ var me = class {
572
608
  ...r.noteHeartbeatMs === void 0 ? {} : { heartbeatMs: r.noteHeartbeatMs },
573
609
  ...t.notes
574
610
  };
575
- this.source = t.source ?? G(new j(r.beeUrl), $(r.chatAddress), r.chatTopic, i, a.slotMs), this.write = t.write ?? q(new j(r.writeUrl ?? r.beeUrl), r.stamp ? $(r.stamp) : ie, $(r.gsocResourceId), r.gsocTopic, i);
611
+ this.source = t.source ?? K(new j(r.beeUrl), $(r.chatAddress), r.chatTopic, i, a.slotMs), this.write = t.write ?? J(new j(r.writeUrl ?? r.beeUrl), r.stamp ? $(r.stamp) : ae, $(r.gsocResourceId), r.gsocTopic, i);
576
612
  let o = t.follower?.now ?? (() => Date.now()), s = {
577
613
  pollIntervalMs: r.pollingInterval ?? 1e3,
578
614
  ...t.follower,
579
615
  now: o
580
616
  }, c = this.source;
581
- this.notes = le(c) ? new Z(c, a, o) : null, this.follower = new ae(this.source, this.topic, {
617
+ this.notes = Z(c) ? new fe(c, a, o) : null, this.follower = new se(this.source, this.topic, {
582
618
  entry: (e) => this.show(e),
583
- skipped: (e, t, n) => this.emitter.emit(I.MESSAGE_SKIPPED, {
619
+ skipped: (e, t, n) => this.emitter.emit(F.MESSAGE_SKIPPED, {
584
620
  index: e,
585
621
  reason: t,
586
622
  detail: n
587
623
  }),
588
624
  status: (e) => this.setStatus(e),
589
- error: (e) => this.emitter.emit(I.ERROR, e)
590
- }, s, this.notes), this.history = new se(this.source, this.topic, {
591
- skipped: (e) => this.emitter.emit(I.MESSAGE_SKIPPED, {
625
+ error: (e) => this.emitter.emit(F.ERROR, e)
626
+ }, s, this.notes), this.history = new le(this.source, this.topic, {
627
+ skipped: (e) => this.emitter.emit(F.MESSAGE_SKIPPED, {
592
628
  index: -1,
593
629
  reason: "history-row",
594
630
  detail: `${e} rows of a history file`
595
631
  }),
596
- error: (e) => this.emitter.emit(I.ERROR, e)
632
+ error: (e) => this.emitter.emit(F.ERROR, e)
597
633
  }, this.notes), this.sender = this.makeSender();
598
634
  }
599
635
  getEmitter() {
@@ -621,7 +657,7 @@ var me = class {
621
657
  if (!n) throw Error("a reaction names the message it reacts to");
622
658
  return this.sender.react(this.topic, n, e), null;
623
659
  }
624
- return L(this.sender.send({
660
+ return I(this.sender.send({
625
661
  topic: this.topic,
626
662
  type: t,
627
663
  target: n ?? "",
@@ -632,12 +668,12 @@ var me = class {
632
668
  return this.sender.retry(typeof e == "string" ? e : e.id);
633
669
  }
634
670
  async fetchPreviousMessages() {
635
- this.emitter.emit(I.LOADING_PREVIOUS_MESSAGES, !0);
671
+ this.emitter.emit(F.LOADING_PREVIOUS_MESSAGES, !0);
636
672
  try {
637
673
  let e = this.generation, t = await this.history.loadOlder();
638
674
  return e === this.generation ? this.showRows(t) : [];
639
675
  } finally {
640
- this.emitter.emit(I.LOADING_PREVIOUS_MESSAGES, !1);
676
+ this.emitter.emit(F.LOADING_PREVIOUS_MESSAGES, !1);
641
677
  }
642
678
  }
643
679
  hasPreviousMessages() {
@@ -647,16 +683,16 @@ var me = class {
647
683
  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);
648
684
  }
649
685
  async open(e) {
650
- this.emitter.emit(I.LOADING_INIT, !0);
686
+ this.emitter.emit(F.LOADING_INIT, !0);
651
687
  let t = 0;
652
688
  for (; e === this.generation;) try {
653
689
  let t = await this.history.open();
654
690
  if (e !== this.generation) return;
655
- this.showRows(t.rows), this.follower.start(t.startAt, t.notes), this.emitter.emit(I.LOADING_INIT, !1);
691
+ this.showRows(t.rows), this.follower.start(t.startAt, t.notes), this.emitter.emit(F.LOADING_INIT, !1);
656
692
  return;
657
693
  } catch (n) {
658
694
  if (e !== this.generation) return;
659
- t++, this.emitter.emit(I.ERROR, n), this.setStatus(J.RECONNECTING), t === pe && this.emitter.emit(I.CRITICAL_ERROR, n), await this.waitToOpen(this.openRetryMs[Math.min(t, this.openRetryMs.length) - 1] ?? 8e3);
695
+ t++, this.emitter.emit(F.ERROR, n), this.setStatus(Y.RECONNECTING), t === he && this.emitter.emit(F.CRITICAL_ERROR, n), await this.waitToOpen(this.openRetryMs[Math.min(t, this.openRetryMs.length) - 1] ?? 8e3);
660
696
  }
661
697
  }
662
698
  waitToOpen(e) {
@@ -669,31 +705,31 @@ var me = class {
669
705
  });
670
706
  }
671
707
  makeSender() {
672
- return new de(this.key, this.nickname, this.write, {
673
- pending: (e) => this.emitter.emit(I.MESSAGE_REQUEST_INITIATED, L(e)),
674
- written: (e) => this.emitter.emit(I.MESSAGE_REQUEST_UPLOADED, L(e)),
708
+ return new pe(this.key, this.nickname, this.write, {
709
+ pending: (e) => this.emitter.emit(F.MESSAGE_REQUEST_INITIATED, I(e)),
710
+ written: (e) => this.emitter.emit(F.MESSAGE_REQUEST_UPLOADED, I(e)),
675
711
  failed: (e, t) => {
676
- this.emitter.emit(I.ERROR, t), this.emitter.emit(I.MESSAGE_REQUEST_ERROR, L(e));
712
+ this.emitter.emit(F.ERROR, t), this.emitter.emit(F.MESSAGE_REQUEST_ERROR, I(e));
677
713
  },
678
714
  confirmed: () => {}
679
715
  }, this.senderSettings);
680
716
  }
681
717
  show(e) {
682
- this.seen.has(e.seq) || (this.seen.add(e.seq), this.sender.confirm(e.msg), this.emitter.emit(I.MESSAGE_RECEIVED, R(e)));
718
+ this.seen.has(e.seq) || (this.seen.add(e.seq), this.sender.confirm(e.msg), this.emitter.emit(F.MESSAGE_RECEIVED, L(e)));
683
719
  }
684
720
  showRows(e) {
685
721
  let t = [];
686
722
  for (let n of e) {
687
723
  if (this.seen.has(n.seq)) continue;
688
724
  this.seen.add(n.seq), this.sender.confirm(n.msg);
689
- let e = R(n);
690
- t.push(e), this.emitter.emit(I.MESSAGE_RECEIVED, e);
725
+ let e = L(n);
726
+ t.push(e), this.emitter.emit(F.MESSAGE_RECEIVED, e);
691
727
  }
692
728
  return t;
693
729
  }
694
730
  setStatus(e) {
695
- e !== this.status && (this.status = e, this.emitter.emit(I.STATUS, e));
731
+ e !== this.status && (this.status = e, this.emitter.emit(F.STATUS, e));
696
732
  }
697
733
  };
698
734
  //#endregion
699
- export { B as ChatEmitter, e as ChatMessageError, t as DEFAULT_NOTE_HEARTBEAT_MS, n as DEFAULT_NOTE_SLOT_MS, I as EVENTS, J as FeedStatus, V as HeadLookupTimeoutError, r as MAX_ENTRY_BYTES, i as MAX_MESSAGE_BYTES, a as MAX_NAME_CHARACTERS, o as MAX_NOTE_BYTES, s as MAX_TEXT_CHARACTERS, c as MAX_TOPIC_CHARACTERS, l as MESSAGE_VERSION, u as MessageType, d as NOTE_VERSION, me as SwarmChat, X as UnreadableGatewayError, H as UnreadableSlotError, f as chatMessageSchema, p as checkChatMessage, m as countCharacters, h as createChatMessage, g as encodeChatMessage, _ as encodeSlotNote, v as feedEntrySchema, ee as hasValidSignature, te as historyFileSchema, y as historyLinkSchema, ne as historyRowSchema, b as newMessageId, x as noteAddress, S as noteIdentifier, C as noteIdentifierText, w as noteSlotEnd, T as noteSlotOf, E as parseChatMessage, D as parseFeedEntry, O as parseHistoryFile, k as parseSlotNote, A as signedBytes, re as slotNoteSchema };
735
+ export { z as ChatEmitter, e as ChatMessageError, t as DEFAULT_NOTE_HEARTBEAT_MS, n as DEFAULT_NOTE_SLOT_MS, F as EVENTS, Y as FeedStatus, H as HeadLookupTimeoutError, r as MAX_ENTRY_BYTES, i as MAX_MESSAGE_BYTES, a as MAX_NAME_CHARACTERS, o as MAX_NOTE_BYTES, s as MAX_TEXT_CHARACTERS, c as MAX_TOPIC_CHARACTERS, l as MESSAGE_VERSION, u as MessageType, d as NOTE_VERSION, ge as SwarmChat, X as UnreadableGatewayError, U as UnreadableSlotError, f as chatMessageSchema, p as checkChatMessage, m as countCharacters, h as createChatMessage, g as encodeChatMessage, _ as encodeSlotNote, ee as feedEntrySchema, te as hasValidSignature, ne as historyFileSchema, v as historyLinkSchema, y as historyRowSchema, b as newMessageId, x as noteAddress, S as noteIdentifier, re as noteIdentifierText, C as noteSlotEnd, w as noteSlotOf, T as parseChatMessage, E as parseFeedEntry, D as parseHistoryFile, O as parseSlotNote, k as signedBytes, A as slotNoteSchema };
package/dist/notes.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface NoteSettings {
11
11
  scanBatch: number;
12
12
  /** The furthest the clock correction goes either way. */
13
13
  clockLimitMs: number;
14
+ /** The shortest wait between two reads of a note that came back missing, see `pacer.ts` for why. */
15
+ missingReadSpacingMs: number;
14
16
  }
15
17
  export declare const DEFAULT_NOTE_SETTINGS: NoteSettings;
16
18
  /** A note that was found, and the time slot it was found at. */
@@ -61,6 +63,7 @@ export declare class NoteReader {
61
63
  empty: number;
62
64
  };
63
65
  readonly settings: NoteSettings;
66
+ private readonly pacer;
64
67
  constructor(source: NoteSource, settings: Partial<NoteSettings>, now: () => number);
65
68
  /** The newest time slot whose note may be asked for at `now`: over, and the margin past, by the server's clock. */
66
69
  lastReadableSlot(now?: number): number;
@@ -70,7 +73,11 @@ export declare class NoteReader {
70
73
  get lookBackSlots(): number;
71
74
  /** How long a follower goes without finding a note before it stops trusting its clock. */
72
75
  get silenceMs(): number;
73
- /** One slot's note, or null when there is none to be had. Rejects only when the gateway fails. */
76
+ /**
77
+ * One slot's note, or null when there is none to be had. Rejects only when the gateway fails. A note that came back
78
+ * missing is not asked for again within the spacing, which only an opening tried again or a clock found to run
79
+ * ahead would do, and is then null without asking.
80
+ */
74
81
  read(slot: number): Promise<SlotNote | null>;
75
82
  /**
76
83
  * The newest note from slot `newest` back to slot `oldest`, read newest first, `scanBatch` at a time, stopping at
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The shortest wait between two reads of one address that came back missing: three reads a minute at most.
3
+ *
4
+ * Bee answers the first few reads of a chunk it cannot find 404, and puts every peer it asked on a skip list for that
5
+ * chunk for a minute (`ethersphere/bee`, `pkg/retrieval/retrieval.go`, `errSkip` and `skiplistDur = time.Minute`).
6
+ * Once no peer is left it answers 500 at once, because "no peer left" is not `storage.ErrNotFound`
7
+ * (`pkg/api/chunk.go`). A chunk written while its address is on that list stays unreadable on that node for up to a
8
+ * minute. Measured on swarm-dev on 2026-10-12: an address read every second for 20 s and then written was readable
9
+ * after 33 to 36 s, one nobody had asked for at once. About four reads of a missing address exhaust the list, so one
10
+ * read every twenty seconds stays under it.
11
+ */
12
+ export declare const DEFAULT_MISSING_READ_SPACING_MS = 20000;
13
+ /**
14
+ * Holds back a read of a slot whose address came back missing until `spacingMs` has passed since that answer. A slot
15
+ * that was found, or never read, may be read at once. A follower keeps one for feed slots and a note reader one for
16
+ * time slots.
17
+ */
18
+ export declare class MissingReadPacer {
19
+ private readonly spacingMs;
20
+ private readonly now;
21
+ private readonly missedAt;
22
+ constructor(spacingMs: number, now: () => number);
23
+ /** The local time from which `slot` may be read again, which is in the past for an address not held back. */
24
+ readableAt(slot: number, now?: number): number;
25
+ mayRead(slot: number, now?: number): boolean;
26
+ missed(slot: number, now?: number): void;
27
+ found(slot: number): void;
28
+ clear(): void;
29
+ /** A miss recorded after `now` was recorded before the clock was set back, and holds nothing. */
30
+ private expired;
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarpunkltd/swarm-chat-js",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "description": "JS library for chatting on Swarm",
5
5
  "keywords": [
6
6
  "chat",
@@ -36,15 +36,6 @@
36
36
  "require": "./dist/message.cjs"
37
37
  }
38
38
  },
39
- "scripts": {
40
- "build": "vite build && tsc -p tsconfig.build.json",
41
- "prepack": "npm run build",
42
- "lint": "oxlint --type-aware .",
43
- "format": "oxfmt .",
44
- "format:check": "oxfmt --check .",
45
- "typecheck": "tsc --noEmit",
46
- "test": "vitest run"
47
- },
48
39
  "dependencies": {
49
40
  "zod": "4.6.5"
50
41
  },
@@ -64,5 +55,12 @@
64
55
  "engines": {
65
56
  "node": ">=24"
66
57
  },
67
- "packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c"
68
- }
58
+ "scripts": {
59
+ "build": "vite build && tsc -p tsconfig.build.json",
60
+ "lint": "oxlint --type-aware .",
61
+ "format": "oxfmt .",
62
+ "format:check": "oxfmt --check .",
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "vitest run"
65
+ }
66
+ }