@solarpunkltd/swarm-chat-js 7.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/message.js CHANGED
@@ -1,308 +1,2 @@
1
- import { Bytes as e, EthAddress as t, Identifier as n, PrivateKey as r, Reference as i, Signature as a } from "@ethersphere/bee-js";
2
- import { z as o } from "zod";
3
- //#region src/message/message.ts
4
- var s = 7, c = 2048, l = 128, u = 500, d = 20, f = {
5
- TEXT: "text",
6
- THREAD: "thread",
7
- REACTION: "reaction"
8
- };
9
- function p(e) {
10
- let t = 0;
11
- for (let n of e) t++;
12
- return t;
13
- }
14
- function m(e) {
15
- return o.string().regex(RegExp(`^[0-9a-f]{${e}}$`), `expected ${e} lowercase hex characters`);
16
- }
17
- function h(e, t) {
18
- return o.string().refine((n) => {
19
- let r = p(n);
20
- return r >= e && r <= t;
21
- }, `expected ${e} to ${t} characters`);
22
- }
23
- var g = m(32), _ = o.strictObject({
24
- v: o.literal(7),
25
- topic: h(1, 128),
26
- id: g,
27
- type: o.enum([
28
- f.TEXT,
29
- f.THREAD,
30
- f.REACTION
31
- ]),
32
- target: o.union([o.literal(""), g]),
33
- text: h(1, 500),
34
- name: h(1, 20),
35
- addr: m(40),
36
- ts: o.number().int().nonnegative().max(2 ** 53 - 1),
37
- sig: m(130)
38
- }).refine((e) => e.type === f.TEXT == (e.target === ""), {
39
- message: "a text message has no target, a thread reply or a reaction names one",
40
- path: ["target"]
41
- }), v = class extends Error {
42
- constructor(e, t) {
43
- super(`chat message refused (${e}): ${t}`), this.reason = e, this.name = "ChatMessageError";
44
- }
45
- }, y = new TextEncoder(), b = new TextDecoder("utf-8", { fatal: !0 });
46
- function x(e) {
47
- return y.encode(JSON.stringify([
48
- e.v,
49
- e.topic,
50
- e.id,
51
- e.type,
52
- e.target,
53
- e.text,
54
- e.name,
55
- e.addr,
56
- e.ts
57
- ]));
58
- }
59
- function S(e) {
60
- let t = {
61
- v: e.v,
62
- topic: e.topic,
63
- id: e.id,
64
- type: e.type,
65
- target: e.target,
66
- text: e.text,
67
- name: e.name,
68
- addr: e.addr,
69
- ts: e.ts,
70
- sig: e.sig
71
- };
72
- return y.encode(JSON.stringify(t));
73
- }
74
- function C() {
75
- let e = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
76
- return Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
77
- }
78
- function w(e) {
79
- return e.issues.map((e) => `${e.path.join(".") || "message"}: ${e.message}`).join(", ");
80
- }
81
- function T(e, t) {
82
- let n = e instanceof r ? e : new r(e), i = {
83
- v: 7,
84
- topic: t.topic,
85
- id: t.id ?? C(),
86
- type: t.type,
87
- target: t.target ?? "",
88
- text: t.text,
89
- name: t.name,
90
- addr: n.publicKey().address().toHex(),
91
- ts: t.ts ?? Date.now()
92
- }, a = {
93
- ...i,
94
- sig: "0".repeat(130)
95
- }, o = _.safeParse(a);
96
- if (!o.success) throw new v("shape", w(o.error));
97
- let s = S(a).byteLength;
98
- if (s > 2048) throw new v("too-large", `${s} bytes, the cap is ${c}`);
99
- let l = {
100
- ...i,
101
- sig: n.sign(x(i)).toHex()
102
- };
103
- return {
104
- message: l,
105
- bytes: S(l)
106
- };
107
- }
108
- function E(e) {
109
- try {
110
- return new a(e.sig).recoverPublicKey(x(e)).address().toHex() === e.addr;
111
- } catch {
112
- return !1;
113
- }
114
- }
115
- function D(e) {
116
- let t = _.safeParse(e);
117
- return t.success ? E(t.data) ? {
118
- ok: !0,
119
- message: t.data
120
- } : {
121
- ok: !1,
122
- reason: "signature",
123
- detail: "the signature was not made by addr over these fields"
124
- } : {
125
- ok: !1,
126
- reason: "shape",
127
- detail: w(t.error)
128
- };
129
- }
130
- function O(e) {
131
- if (e.byteLength > 2048) return {
132
- ok: !1,
133
- reason: "too-large",
134
- detail: `${e.byteLength} bytes, the cap is ${c}`
135
- };
136
- let t;
137
- try {
138
- t = b.decode(e);
139
- } catch {
140
- return {
141
- ok: !1,
142
- reason: "not-utf8",
143
- detail: "the payload is not valid UTF-8"
144
- };
145
- }
146
- let n;
147
- try {
148
- n = JSON.parse(t);
149
- } catch {
150
- return {
151
- ok: !1,
152
- reason: "not-json",
153
- detail: "the payload is not JSON"
154
- };
155
- }
156
- return D(n);
157
- }
158
- //#endregion
159
- //#region src/message/entry.ts
160
- var k = 4096, A = o.string().regex(/^[0-9a-f]{64}([0-9a-f]{64})?$/, "expected a 64 or 128 hex character reference"), j = o.number().int().nonnegative().max(2 ** 53 - 1), M = o.strictObject({
161
- ref: A,
162
- toSeq: j
163
- }), N = o.strictObject({
164
- v: o.literal(7),
165
- seq: j,
166
- at: j,
167
- msg: _,
168
- history: M.nullable()
169
- }), P = o.strictObject({
170
- seq: j,
171
- at: j,
172
- msg: _
173
- }), F = o.strictObject({
174
- v: o.literal(7),
175
- topic: o.string().min(1),
176
- fromSeq: j,
177
- toSeq: j,
178
- messages: o.array(o.unknown()),
179
- prev: M.nullable()
180
- }).refine((e) => e.fromSeq <= e.toSeq, {
181
- message: "fromSeq is after toSeq",
182
- path: ["fromSeq"]
183
- }), I = new TextDecoder("utf-8", { fatal: !0 });
184
- function L(e) {
185
- let t;
186
- try {
187
- t = I.decode(e);
188
- } catch {
189
- return {
190
- ok: !1,
191
- reason: "not-utf8",
192
- detail: "the payload is not valid UTF-8"
193
- };
194
- }
195
- try {
196
- return {
197
- ok: !0,
198
- value: JSON.parse(t)
199
- };
200
- } catch {
201
- return {
202
- ok: !1,
203
- reason: "not-json",
204
- detail: "the payload is not JSON"
205
- };
206
- }
207
- }
208
- function R(e) {
209
- return e.issues.map((e) => `${e.path.join(".") || "value"}: ${e.message}`).join(", ");
210
- }
211
- function z(e, t, n) {
212
- let r = L(e);
213
- if (!r.ok) return r;
214
- let i = N.safeParse(r.value);
215
- return i.success ? i.data.seq === t ? i.data.msg.topic === n ? {
216
- ok: !0,
217
- value: i.data
218
- } : {
219
- ok: !1,
220
- reason: "mismatch",
221
- detail: "the message belongs to another chat"
222
- } : {
223
- ok: !1,
224
- reason: "mismatch",
225
- detail: `seq ${i.data.seq} at index ${t}`
226
- } : {
227
- ok: !1,
228
- reason: "shape",
229
- detail: R(i.error)
230
- };
231
- }
232
- function B(e, t, n) {
233
- let r = L(e);
234
- if (!r.ok) return r;
235
- let i = F.safeParse(r.value);
236
- if (!i.success) return {
237
- ok: !1,
238
- reason: "shape",
239
- detail: R(i.error)
240
- };
241
- if (i.data.topic !== t || i.data.toSeq !== n.toSeq) return {
242
- ok: !1,
243
- reason: "mismatch",
244
- detail: "the file is not the one its link names"
245
- };
246
- let { fromSeq: a, toSeq: o, prev: s } = i.data, c = [], l = 0;
247
- for (let e of i.data.messages) {
248
- let n = P.safeParse(e);
249
- n.success && n.data.seq >= a && n.data.seq <= o && n.data.msg.topic === t ? c.push(n.data) : l++;
250
- }
251
- return {
252
- ok: !0,
253
- value: {
254
- topic: t,
255
- fromSeq: a,
256
- toSeq: o,
257
- rows: c,
258
- prev: s,
259
- skipped: l
260
- }
261
- };
262
- }
263
- //#endregion
264
- //#region src/message/note.ts
265
- var V = 1, H = 2e3, U = 3e4, W = 256, G = o.strictObject({
266
- v: o.literal(1),
267
- newest: o.number().int().min(-1).max(2 ** 53 - 1),
268
- writtenAt: o.number().int().nonnegative().max(2 ** 53 - 1)
269
- });
270
- function K(e) {
271
- if (!Number.isInteger(e) || e <= 0) throw RangeError(`a note slot is a positive whole number of milliseconds, not ${e}`);
272
- }
273
- function q(e, t) {
274
- return K(t), Math.floor(e / t);
275
- }
276
- function J(e, t) {
277
- return K(t), (e + 1) * t;
278
- }
279
- function Y(e, t, n) {
280
- return `${e}/note/${t}/${n}`;
281
- }
282
- function X(e, t, r) {
283
- return n.fromString(Y(e, t, r));
284
- }
285
- function Z(n, r, a, o) {
286
- let s = X(n, r, a);
287
- return new i(e.keccak256(e.concat(s.toUint8Array(), new t(o).toUint8Array())));
288
- }
289
- var Q = new TextEncoder(), $ = new TextDecoder("utf-8", { fatal: !0 });
290
- function ee(e) {
291
- let t = {
292
- v: 1,
293
- newest: e.newest,
294
- writtenAt: e.writtenAt
295
- };
296
- return Q.encode(JSON.stringify(t));
297
- }
298
- function te(e) {
299
- if (e.length > 256) return null;
300
- try {
301
- let t = G.safeParse(JSON.parse($.decode(e)));
302
- return t.success ? t.data : null;
303
- } catch {
304
- return null;
305
- }
306
- }
307
- //#endregion
308
- export { v as ChatMessageError, U as DEFAULT_NOTE_HEARTBEAT_MS, H as DEFAULT_NOTE_SLOT_MS, k as MAX_ENTRY_BYTES, c as MAX_MESSAGE_BYTES, d as MAX_NAME_CHARACTERS, W as MAX_NOTE_BYTES, u as MAX_TEXT_CHARACTERS, l as MAX_TOPIC_CHARACTERS, s as MESSAGE_VERSION, f as MessageType, V as NOTE_VERSION, _ as chatMessageSchema, D as checkChatMessage, p as countCharacters, T as createChatMessage, S as encodeChatMessage, ee as encodeSlotNote, N as feedEntrySchema, E as hasValidSignature, F as historyFileSchema, M as historyLinkSchema, P as historyRowSchema, C as newMessageId, Z as noteAddress, X as noteIdentifier, Y as noteIdentifierText, J as noteSlotEnd, q as noteSlotOf, O as parseChatMessage, z as parseFeedEntry, B as parseHistoryFile, te as parseSlotNote, x as signedBytes, G as slotNoteSchema };
1
+ import { A as e, B as t, C as n, D as r, E as i, F as a, H as o, I as s, L as c, M as l, N as u, O as d, P as f, R as p, S as m, T as h, V as g, _, a as v, b as y, c as b, d as x, f as S, g as C, h as w, i as T, j as E, k as D, l as O, m as k, n as A, o as j, p as M, r as N, s as P, t as F, u as I, v as L, w as R, x as z, y as B, z as V } from "./message-Dbh8qrpg.js";
2
+ export { V as CHAT_BINDING_TITLE, m as ChatMessageError, F as DEFAULT_NOTE_HEARTBEAT_MS, A as DEFAULT_NOTE_SLOT_MS, t as MAX_BINDING_CHARACTERS, M as MAX_ENTRY_BYTES, n as MAX_MESSAGE_BYTES, R as MAX_NAME_CHARACTERS, N as MAX_NOTE_BYTES, h as MAX_TEXT_CHARACTERS, i as MAX_TOPIC_CHARACTERS, r as MESSAGE_VERSION, d as MessageType, D as MessageVia, T as NOTE_VERSION, y as OWN_FEED_TOPIC_PREFIX, g as chatBindingSchema, e as chatMessageSchema, o as checkChatBinding, E as countCharacters, l as createChatMessage, u as encodeChatMessage, v as encodeSlotNote, k as feedEntrySchema, f as hasValidSignature, w as historyFileSchema, C as historyLinkSchema, _ as historyRowSchema, a as newMessageId, j as noteAddress, P as noteIdentifier, b as noteIdentifierText, O as noteSlotEnd, I as noteSlotOf, z as ownFeedTopic, s as parseChatMessage, L as parseFeedEntry, B as parseHistoryFile, x as parseSlotNote, c as signedBytes, S as slotNoteSchema, p as verifyChatMessage };
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/dist/sender.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PrivateKey } from '@ethersphere/bee-js';
2
- import { type ChatMessage, type ChatMessageDraft } from './message/index.js';
2
+ import { type ChatBinding, type ChatMessage, type ChatMessageDraft, type MessageVia } from './message/index.js';
3
3
  /** Writes one payload to the chat's GSOC address. Rejects when the write did not reach the node. */
4
4
  export type GsocWrite = (payload: Uint8Array) => Promise<void>;
5
5
  export interface SenderSettings {
@@ -26,21 +26,28 @@ export interface SenderEvents {
26
26
  /** Read back from the chat feed. */
27
27
  confirmed(message: ChatMessage): void;
28
28
  }
29
- export type SendDraft = Omit<ChatMessageDraft, 'name' | 'ts' | 'id'>;
29
+ export type SendDraft = Omit<ChatMessageDraft, 'name' | 'ts' | 'id' | 'via' | 'own' | 'idb'>;
30
+ /** What every message this sender signs says about who sent it and how. */
31
+ export interface SenderProfile {
32
+ name: string;
33
+ via: MessageVia;
34
+ /** The sender's Swarm ID chat binding, for a signed-in sender only. */
35
+ idb?: ChatBinding;
36
+ }
30
37
  /**
31
38
  * Sends signed messages as GSOC writes and keeps each one pending until the chat feed shows it. A pending message is
32
39
  * written again with the identical bytes, so the server can tell a resend from a new message by its id.
33
40
  */
34
41
  export declare class Sender {
35
42
  private readonly key;
36
- private readonly name;
43
+ private readonly profile;
37
44
  private readonly write;
38
45
  private readonly events;
39
46
  private readonly settings;
40
47
  private readonly outgoing;
41
48
  private readonly reactions;
42
49
  private stopped;
43
- constructor(key: PrivateKey, name: string, write: GsocWrite, events: SenderEvents, settings?: SenderSettings);
50
+ constructor(key: PrivateKey, profile: SenderProfile, write: GsocWrite, events: SenderEvents, settings?: SenderSettings);
44
51
  get address(): string;
45
52
  /** Builds, signs and writes a message. Throws a ChatMessageError, and writes nothing, for one the server would refuse. */
46
53
  send(draft: SendDraft): ChatMessage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarpunkltd/swarm-chat-js",
3
- "version": "7.2.0",
3
+ "version": "8.0.0",
4
4
  "description": "JS library for chatting on Swarm",
5
5
  "keywords": [
6
6
  "chat",
@@ -46,6 +46,7 @@
46
46
  "test": "vitest run"
47
47
  },
48
48
  "dependencies": {
49
+ "@ethersphere/core-sdk": "0.1.1",
49
50
  "zod": "4.6.5"
50
51
  },
51
52
  "devDependencies": {