@solarpunkltd/swarm-chat-js 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -8
- package/dist/chat.d.ts +8 -1
- package/dist/follower.d.ts +53 -10
- package/dist/history.d.ts +20 -6
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +278 -119
- package/dist/message/index.d.ts +1 -0
- package/dist/message/note.d.ts +38 -0
- package/dist/message.cjs +1 -1
- package/dist/message.js +139 -95
- package/dist/notes.d.ts +88 -0
- package/dist/pacer.d.ts +31 -0
- package/dist/swarm.d.ts +8 -1
- package/package.json +10 -12
package/dist/message/index.d.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Identifier, Reference } from '@ethersphere/bee-js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Slot notes: the chat server's announcement, after a stretch of wall-clock time in which messages landed, of the
|
|
5
|
+
* newest feed slot it has written. A reader asks for a note only once its time slot is over, and asks for a feed slot
|
|
6
|
+
* only once a note has named it, so it never asks Bee for an address that is not written yet. Bee answers a chunk it
|
|
7
|
+
* could not find by skipping every peer it asked for that address for a minute, which is what made new messages
|
|
8
|
+
* arrive about forty seconds late while readers polled the next slot.
|
|
9
|
+
*
|
|
10
|
+
* Time slot `s` covers `[s * slotMs, (s + 1) * slotMs)` in Unix milliseconds. Its note is a single owner chunk signed
|
|
11
|
+
* by the chat feed's key, at the identifier keccak256(`<topic>/note/<slotMs>/<s>`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const NOTE_VERSION = 1;
|
|
14
|
+
export declare const DEFAULT_NOTE_SLOT_MS = 2000;
|
|
15
|
+
export declare const DEFAULT_NOTE_HEARTBEAT_MS = 30000;
|
|
16
|
+
/** A note is far below one chunk, so it is a single owner chunk with its payload inline. */
|
|
17
|
+
export declare const MAX_NOTE_BYTES = 256;
|
|
18
|
+
export declare const slotNoteSchema: z.ZodObject<{
|
|
19
|
+
v: z.ZodLiteral<1>;
|
|
20
|
+
newest: z.ZodNumber;
|
|
21
|
+
writtenAt: z.ZodNumber;
|
|
22
|
+
}, z.core.$strict>;
|
|
23
|
+
export type SlotNote = z.infer<typeof slotNoteSchema>;
|
|
24
|
+
/** The time slot that holds the instant `timeMs`. */
|
|
25
|
+
export declare function noteSlotOf(timeMs: number, slotMs: number): number;
|
|
26
|
+
/** The instant time slot `slot` ends, which is the first instant of the next one. */
|
|
27
|
+
export declare function noteSlotEnd(slot: number, slotMs: number): number;
|
|
28
|
+
export declare function noteIdentifierText(topic: string, slotMs: number, slot: number): string;
|
|
29
|
+
export declare function noteIdentifier(topic: string, slotMs: number, slot: number): Identifier;
|
|
30
|
+
/**
|
|
31
|
+
* Where a note lives, the address `GET /chunks/<address>` reads it from: keccak256 of the identifier and the owner,
|
|
32
|
+
* the single owner chunk rule.
|
|
33
|
+
*/
|
|
34
|
+
export declare function noteAddress(topic: string, slotMs: number, slot: number, owner: string): Reference;
|
|
35
|
+
/** The payload, with its fields in this order, so two encodings of one note are the same bytes. */
|
|
36
|
+
export declare function encodeSlotNote(note: Omit<SlotNote, 'v'>): Uint8Array;
|
|
37
|
+
/** A note, or null for anything that fails the strict check, which a reader treats as no note at all. */
|
|
38
|
+
export declare function parseSlotNote(payload: Uint8Array): SlotNote | null;
|
package/dist/message.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@ethersphere/bee-js"),t=require("zod");var n=7,r=2048,i=128,a=500,o=20,s={TEXT:`text`,THREAD:`thread`,REACTION:`reaction`};function c(e){let t=0;for(let n of e)t++;return t}function l(e){return t.z.string().regex(RegExp(`^[0-9a-f]{${e}}$`),`expected ${e} lowercase hex characters`)}function u(e,n){return t.z.string().refine(t=>{let r=c(t);return r>=e&&r<=n},`expected ${e} to ${n} characters`)}var d=l(32),f=t.z.strictObject({v:t.z.literal(7),topic:u(1,128),id:d,type:t.z.enum([s.TEXT,s.THREAD,s.REACTION]),target:t.z.union([t.z.literal(``),d]),text:u(1,500),name:u(1,20),addr:l(40),ts:t.z.number().int().nonnegative().max(2**53-1),sig:l(130)}).refine(e=>e.type===s.TEXT==(e.target===``),{message:`a text message has no target, a thread reply or a reaction names one`,path:[`target`]}),p=class extends Error{constructor(e,t){super(`chat message refused (${e}): ${t}`),this.reason=e,this.name=`ChatMessageError`}},m=new TextEncoder,h=new TextDecoder(`utf-8`,{fatal:!0});function g(e){return m.encode(JSON.stringify([e.v,e.topic,e.id,e.type,e.target,e.text,e.name,e.addr,e.ts]))}function _(e){let t={v:e.v,topic:e.topic,id:e.id,type:e.type,target:e.target,text:e.text,name:e.name,addr:e.addr,ts:e.ts,sig:e.sig};return m.encode(JSON.stringify(t))}function v(){let e=crypto.getRandomValues(new Uint8Array(16));return Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``)}function y(e){return e.issues.map(e=>`${e.path.join(`.`)||`message`}: ${e.message}`).join(`, `)}function b(t,n){let i=t instanceof e.PrivateKey?t:new e.PrivateKey(t),a={v:7,topic:n.topic,id:n.id??v(),type:n.type,target:n.target??``,text:n.text,name:n.name,addr:i.publicKey().address().toHex(),ts:n.ts??Date.now()},o={...a,sig:`0`.repeat(130)},s=f.safeParse(o);if(!s.success)throw new p(`shape`,y(s.error));let c=_(o).byteLength;if(c>2048)throw new p(`too-large`,`${c} bytes, the cap is ${r}`);let l={...a,sig:i.sign(g(a)).toHex()};return{message:l,bytes:_(l)}}function x(t){try{return new e.Signature(t.sig).recoverPublicKey(g(t)).address().toHex()===t.addr}catch{return!1}}function S(e){let t=f.safeParse(e);return t.success?x(t.data)?{ok:!0,message:t.data}:{ok:!1,reason:`signature`,detail:`the signature was not made by addr over these fields`}:{ok:!1,reason:`shape`,detail:y(t.error)}}function C(e){if(e.byteLength>2048)return{ok:!1,reason:`too-large`,detail:`${e.byteLength} bytes, the cap is ${r}`};let t;try{t=h.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}let n;try{n=JSON.parse(t)}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}return S(n)}var w=4096,T=t.z.string().regex(/^[0-9a-f]{64}([0-9a-f]{64})?$/,`expected a 64 or 128 hex character reference`),E=t.z.number().int().nonnegative().max(2**53-1),D=t.z.strictObject({ref:T,toSeq:E}),O=t.z.strictObject({v:t.z.literal(7),seq:E,at:E,msg:f,history:D.nullable()}),k=t.z.strictObject({seq:E,at:E,msg:f}),A=t.z.strictObject({v:t.z.literal(7),topic:t.z.string().min(1),fromSeq:E,toSeq:E,messages:t.z.array(t.z.unknown()),prev:D.nullable()}).refine(e=>e.fromSeq<=e.toSeq,{message:`fromSeq is after toSeq`,path:[`fromSeq`]}),j=new TextDecoder(`utf-8`,{fatal:!0});function M(e){let t;try{t=j.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}try{return{ok:!0,value:JSON.parse(t)}}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}}function N(e){return e.issues.map(e=>`${e.path.join(`.`)||`value`}: ${e.message}`).join(`, `)}function P(e,t,n){let r=M(e);if(!r.ok)return r;let i=O.safeParse(r.value);return i.success?i.data.seq===t?i.data.msg.topic===n?{ok:!0,value:i.data}:{ok:!1,reason:`mismatch`,detail:`the message belongs to another chat`}:{ok:!1,reason:`mismatch`,detail:`seq ${i.data.seq} at index ${t}`}:{ok:!1,reason:`shape`,detail:N(i.error)}}function F(e,t,n){let r=M(e);if(!r.ok)return r;let i=A.safeParse(r.value);if(!i.success)return{ok:!1,reason:`shape`,detail:N(i.error)};if(i.data.topic!==t||i.data.toSeq!==n.toSeq)return{ok:!1,reason:`mismatch`,detail:`the file is not the one its link names`};let{fromSeq:a,toSeq:o,prev:s}=i.data,c=[],l=0;for(let e of i.data.messages){let n=k.safeParse(e);n.success&&n.data.seq>=a&&n.data.seq<=o&&n.data.msg.topic===t?c.push(n.data):l++}return{ok:!0,value:{topic:t,fromSeq:a,toSeq:o,rows:c,prev:s,skipped:l}}}exports.ChatMessageError=p,exports.MAX_ENTRY_BYTES=w,exports.MAX_MESSAGE_BYTES=r,exports.MAX_NAME_CHARACTERS=o,exports.MAX_TEXT_CHARACTERS=a,exports.MAX_TOPIC_CHARACTERS=i,exports.MESSAGE_VERSION=n,exports.MessageType=s,exports.chatMessageSchema=f,exports.checkChatMessage=S,exports.countCharacters=c,exports.createChatMessage=b,exports.encodeChatMessage=_,exports.feedEntrySchema=O,exports.hasValidSignature=x,exports.historyFileSchema=A,exports.historyLinkSchema=D,exports.historyRowSchema=k,exports.newMessageId=v,exports.parseChatMessage=C,exports.parseFeedEntry=P,exports.parseHistoryFile=F,exports.signedBytes=g;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@ethersphere/bee-js"),t=require("zod");var n=7,r=2048,i=128,a=500,o=20,s={TEXT:`text`,THREAD:`thread`,REACTION:`reaction`};function c(e){let t=0;for(let n of e)t++;return t}function l(e){return t.z.string().regex(RegExp(`^[0-9a-f]{${e}}$`),`expected ${e} lowercase hex characters`)}function u(e,n){return t.z.string().refine(t=>{let r=c(t);return r>=e&&r<=n},`expected ${e} to ${n} characters`)}var d=l(32),f=t.z.strictObject({v:t.z.literal(7),topic:u(1,128),id:d,type:t.z.enum([s.TEXT,s.THREAD,s.REACTION]),target:t.z.union([t.z.literal(``),d]),text:u(1,500),name:u(1,20),addr:l(40),ts:t.z.number().int().nonnegative().max(2**53-1),sig:l(130)}).refine(e=>e.type===s.TEXT==(e.target===``),{message:`a text message has no target, a thread reply or a reaction names one`,path:[`target`]}),p=class extends Error{constructor(e,t){super(`chat message refused (${e}): ${t}`),this.reason=e,this.name=`ChatMessageError`}},m=new TextEncoder,h=new TextDecoder(`utf-8`,{fatal:!0});function g(e){return m.encode(JSON.stringify([e.v,e.topic,e.id,e.type,e.target,e.text,e.name,e.addr,e.ts]))}function _(e){let t={v:e.v,topic:e.topic,id:e.id,type:e.type,target:e.target,text:e.text,name:e.name,addr:e.addr,ts:e.ts,sig:e.sig};return m.encode(JSON.stringify(t))}function v(){let e=crypto.getRandomValues(new Uint8Array(16));return Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``)}function y(e){return e.issues.map(e=>`${e.path.join(`.`)||`message`}: ${e.message}`).join(`, `)}function b(t,n){let i=t instanceof e.PrivateKey?t:new e.PrivateKey(t),a={v:7,topic:n.topic,id:n.id??v(),type:n.type,target:n.target??``,text:n.text,name:n.name,addr:i.publicKey().address().toHex(),ts:n.ts??Date.now()},o={...a,sig:`0`.repeat(130)},s=f.safeParse(o);if(!s.success)throw new p(`shape`,y(s.error));let c=_(o).byteLength;if(c>2048)throw new p(`too-large`,`${c} bytes, the cap is ${r}`);let l={...a,sig:i.sign(g(a)).toHex()};return{message:l,bytes:_(l)}}function x(t){try{return new e.Signature(t.sig).recoverPublicKey(g(t)).address().toHex()===t.addr}catch{return!1}}function S(e){let t=f.safeParse(e);return t.success?x(t.data)?{ok:!0,message:t.data}:{ok:!1,reason:`signature`,detail:`the signature was not made by addr over these fields`}:{ok:!1,reason:`shape`,detail:y(t.error)}}function C(e){if(e.byteLength>2048)return{ok:!1,reason:`too-large`,detail:`${e.byteLength} bytes, the cap is ${r}`};let t;try{t=h.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}let n;try{n=JSON.parse(t)}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}return S(n)}var w=4096,T=t.z.string().regex(/^[0-9a-f]{64}([0-9a-f]{64})?$/,`expected a 64 or 128 hex character reference`),E=t.z.number().int().nonnegative().max(2**53-1),D=t.z.strictObject({ref:T,toSeq:E}),O=t.z.strictObject({v:t.z.literal(7),seq:E,at:E,msg:f,history:D.nullable()}),k=t.z.strictObject({seq:E,at:E,msg:f}),A=t.z.strictObject({v:t.z.literal(7),topic:t.z.string().min(1),fromSeq:E,toSeq:E,messages:t.z.array(t.z.unknown()),prev:D.nullable()}).refine(e=>e.fromSeq<=e.toSeq,{message:`fromSeq is after toSeq`,path:[`fromSeq`]}),j=new TextDecoder(`utf-8`,{fatal:!0});function M(e){let t;try{t=j.decode(e)}catch{return{ok:!1,reason:`not-utf8`,detail:`the payload is not valid UTF-8`}}try{return{ok:!0,value:JSON.parse(t)}}catch{return{ok:!1,reason:`not-json`,detail:`the payload is not JSON`}}}function N(e){return e.issues.map(e=>`${e.path.join(`.`)||`value`}: ${e.message}`).join(`, `)}function P(e,t,n){let r=M(e);if(!r.ok)return r;let i=O.safeParse(r.value);return i.success?i.data.seq===t?i.data.msg.topic===n?{ok:!0,value:i.data}:{ok:!1,reason:`mismatch`,detail:`the message belongs to another chat`}:{ok:!1,reason:`mismatch`,detail:`seq ${i.data.seq} at index ${t}`}:{ok:!1,reason:`shape`,detail:N(i.error)}}function F(e,t,n){let r=M(e);if(!r.ok)return r;let i=A.safeParse(r.value);if(!i.success)return{ok:!1,reason:`shape`,detail:N(i.error)};if(i.data.topic!==t||i.data.toSeq!==n.toSeq)return{ok:!1,reason:`mismatch`,detail:`the file is not the one its link names`};let{fromSeq:a,toSeq:o,prev:s}=i.data,c=[],l=0;for(let e of i.data.messages){let n=k.safeParse(e);n.success&&n.data.seq>=a&&n.data.seq<=o&&n.data.msg.topic===t?c.push(n.data):l++}return{ok:!0,value:{topic:t,fromSeq:a,toSeq:o,rows:c,prev:s,skipped:l}}}var I=1,L=2e3,R=3e4,z=256,B=t.z.strictObject({v:t.z.literal(1),newest:t.z.number().int().min(-1).max(2**53-1),writtenAt:t.z.number().int().nonnegative().max(2**53-1)});function V(e){if(!Number.isInteger(e)||e<=0)throw RangeError(`a note slot is a positive whole number of milliseconds, not ${e}`)}function H(e,t){return V(t),Math.floor(e/t)}function U(e,t){return V(t),(e+1)*t}function W(e,t,n){return`${e}/note/${t}/${n}`}function G(t,n,r){return e.Identifier.fromString(W(t,n,r))}function K(t,n,r,i){let a=G(t,n,r);return new e.Reference(e.Bytes.keccak256(e.Bytes.concat(a.toUint8Array(),new e.EthAddress(i).toUint8Array())))}var q=new TextEncoder,J=new TextDecoder(`utf-8`,{fatal:!0});function Y(e){let t={v:1,newest:e.newest,writtenAt:e.writtenAt};return q.encode(JSON.stringify(t))}function X(e){if(e.length>256)return null;try{let t=B.safeParse(JSON.parse(J.decode(e)));return t.success?t.data:null}catch{return null}}exports.ChatMessageError=p,exports.DEFAULT_NOTE_HEARTBEAT_MS=R,exports.DEFAULT_NOTE_SLOT_MS=L,exports.MAX_ENTRY_BYTES=w,exports.MAX_MESSAGE_BYTES=r,exports.MAX_NAME_CHARACTERS=o,exports.MAX_NOTE_BYTES=z,exports.MAX_TEXT_CHARACTERS=a,exports.MAX_TOPIC_CHARACTERS=i,exports.MESSAGE_VERSION=n,exports.MessageType=s,exports.NOTE_VERSION=I,exports.chatMessageSchema=f,exports.checkChatMessage=S,exports.countCharacters=c,exports.createChatMessage=b,exports.encodeChatMessage=_,exports.encodeSlotNote=Y,exports.feedEntrySchema=O,exports.hasValidSignature=x,exports.historyFileSchema=A,exports.historyLinkSchema=D,exports.historyRowSchema=k,exports.newMessageId=v,exports.noteAddress=K,exports.noteIdentifier=G,exports.noteIdentifierText=W,exports.noteSlotEnd=U,exports.noteSlotOf=H,exports.parseChatMessage=C,exports.parseFeedEntry=P,exports.parseHistoryFile=F,exports.parseSlotNote=X,exports.signedBytes=g,exports.slotNoteSchema=B;
|
package/dist/message.js
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { z as
|
|
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
3
|
//#region src/message/message.ts
|
|
4
|
-
var
|
|
4
|
+
var s = 7, c = 2048, l = 128, u = 500, d = 20, f = {
|
|
5
5
|
TEXT: "text",
|
|
6
6
|
THREAD: "thread",
|
|
7
7
|
REACTION: "reaction"
|
|
8
8
|
};
|
|
9
|
-
function
|
|
9
|
+
function p(e) {
|
|
10
10
|
let t = 0;
|
|
11
11
|
for (let n of e) t++;
|
|
12
12
|
return t;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
return
|
|
14
|
+
function m(e) {
|
|
15
|
+
return o.string().regex(RegExp(`^[0-9a-f]{${e}}$`), `expected ${e} lowercase hex characters`);
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
return
|
|
19
|
-
let r =
|
|
17
|
+
function h(e, t) {
|
|
18
|
+
return o.string().refine((n) => {
|
|
19
|
+
let r = p(n);
|
|
20
20
|
return r >= e && r <= t;
|
|
21
21
|
}, `expected ${e} to ${t} characters`);
|
|
22
22
|
}
|
|
23
|
-
var
|
|
24
|
-
v:
|
|
25
|
-
topic:
|
|
26
|
-
id:
|
|
27
|
-
type:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
31
|
]),
|
|
32
|
-
target:
|
|
33
|
-
text:
|
|
34
|
-
name:
|
|
35
|
-
addr:
|
|
36
|
-
ts:
|
|
37
|
-
sig:
|
|
38
|
-
}).refine((e) => e.type ===
|
|
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
39
|
message: "a text message has no target, a thread reply or a reaction names one",
|
|
40
40
|
path: ["target"]
|
|
41
|
-
}),
|
|
41
|
+
}), v = class extends Error {
|
|
42
42
|
constructor(e, t) {
|
|
43
43
|
super(`chat message refused (${e}): ${t}`), this.reason = e, this.name = "ChatMessageError";
|
|
44
44
|
}
|
|
45
|
-
},
|
|
46
|
-
function
|
|
47
|
-
return
|
|
45
|
+
}, y = new TextEncoder(), b = new TextDecoder("utf-8", { fatal: !0 });
|
|
46
|
+
function x(e) {
|
|
47
|
+
return y.encode(JSON.stringify([
|
|
48
48
|
e.v,
|
|
49
49
|
e.topic,
|
|
50
50
|
e.id,
|
|
@@ -56,7 +56,7 @@ function _(e) {
|
|
|
56
56
|
e.ts
|
|
57
57
|
]));
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
function S(e) {
|
|
60
60
|
let t = {
|
|
61
61
|
v: e.v,
|
|
62
62
|
topic: e.topic,
|
|
@@ -69,52 +69,52 @@ function v(e) {
|
|
|
69
69
|
ts: e.ts,
|
|
70
70
|
sig: e.sig
|
|
71
71
|
};
|
|
72
|
-
return
|
|
72
|
+
return y.encode(JSON.stringify(t));
|
|
73
73
|
}
|
|
74
|
-
function
|
|
74
|
+
function C() {
|
|
75
75
|
let e = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
76
76
|
return Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
|
|
77
77
|
}
|
|
78
|
-
function
|
|
78
|
+
function w(e) {
|
|
79
79
|
return e.issues.map((e) => `${e.path.join(".") || "message"}: ${e.message}`).join(", ");
|
|
80
80
|
}
|
|
81
|
-
function
|
|
82
|
-
let
|
|
81
|
+
function T(e, t) {
|
|
82
|
+
let n = e instanceof r ? e : new r(e), i = {
|
|
83
83
|
v: 7,
|
|
84
|
-
topic:
|
|
85
|
-
id:
|
|
86
|
-
type:
|
|
87
|
-
target:
|
|
88
|
-
text:
|
|
89
|
-
name:
|
|
90
|
-
addr:
|
|
91
|
-
ts:
|
|
92
|
-
},
|
|
93
|
-
...
|
|
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
94
|
sig: "0".repeat(130)
|
|
95
|
-
},
|
|
96
|
-
if (!
|
|
97
|
-
let
|
|
98
|
-
if (
|
|
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
99
|
let l = {
|
|
100
|
-
...
|
|
101
|
-
sig:
|
|
100
|
+
...i,
|
|
101
|
+
sig: n.sign(x(i)).toHex()
|
|
102
102
|
};
|
|
103
103
|
return {
|
|
104
104
|
message: l,
|
|
105
|
-
bytes:
|
|
105
|
+
bytes: S(l)
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
-
function
|
|
108
|
+
function E(e) {
|
|
109
109
|
try {
|
|
110
|
-
return new
|
|
110
|
+
return new a(e.sig).recoverPublicKey(x(e)).address().toHex() === e.addr;
|
|
111
111
|
} catch {
|
|
112
112
|
return !1;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
function
|
|
116
|
-
let t =
|
|
117
|
-
return t.success ?
|
|
115
|
+
function D(e) {
|
|
116
|
+
let t = _.safeParse(e);
|
|
117
|
+
return t.success ? E(t.data) ? {
|
|
118
118
|
ok: !0,
|
|
119
119
|
message: t.data
|
|
120
120
|
} : {
|
|
@@ -124,18 +124,18 @@ function C(e) {
|
|
|
124
124
|
} : {
|
|
125
125
|
ok: !1,
|
|
126
126
|
reason: "shape",
|
|
127
|
-
detail:
|
|
127
|
+
detail: w(t.error)
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
-
function
|
|
130
|
+
function O(e) {
|
|
131
131
|
if (e.byteLength > 2048) return {
|
|
132
132
|
ok: !1,
|
|
133
133
|
reason: "too-large",
|
|
134
|
-
detail: `${e.byteLength} bytes, the cap is ${
|
|
134
|
+
detail: `${e.byteLength} bytes, the cap is ${c}`
|
|
135
135
|
};
|
|
136
136
|
let t;
|
|
137
137
|
try {
|
|
138
|
-
t =
|
|
138
|
+
t = b.decode(e);
|
|
139
139
|
} catch {
|
|
140
140
|
return {
|
|
141
141
|
ok: !1,
|
|
@@ -153,38 +153,38 @@ function w(e) {
|
|
|
153
153
|
detail: "the payload is not JSON"
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
-
return
|
|
156
|
+
return D(n);
|
|
157
157
|
}
|
|
158
158
|
//#endregion
|
|
159
159
|
//#region src/message/entry.ts
|
|
160
|
-
var
|
|
161
|
-
ref:
|
|
162
|
-
toSeq:
|
|
163
|
-
}),
|
|
164
|
-
v:
|
|
165
|
-
seq:
|
|
166
|
-
at:
|
|
167
|
-
msg:
|
|
168
|
-
history:
|
|
169
|
-
}),
|
|
170
|
-
seq:
|
|
171
|
-
at:
|
|
172
|
-
msg:
|
|
173
|
-
}),
|
|
174
|
-
v:
|
|
175
|
-
topic:
|
|
176
|
-
fromSeq:
|
|
177
|
-
toSeq:
|
|
178
|
-
messages:
|
|
179
|
-
prev:
|
|
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
180
|
}).refine((e) => e.fromSeq <= e.toSeq, {
|
|
181
181
|
message: "fromSeq is after toSeq",
|
|
182
182
|
path: ["fromSeq"]
|
|
183
|
-
}),
|
|
184
|
-
function
|
|
183
|
+
}), I = new TextDecoder("utf-8", { fatal: !0 });
|
|
184
|
+
function L(e) {
|
|
185
185
|
let t;
|
|
186
186
|
try {
|
|
187
|
-
t =
|
|
187
|
+
t = I.decode(e);
|
|
188
188
|
} catch {
|
|
189
189
|
return {
|
|
190
190
|
ok: !1,
|
|
@@ -205,13 +205,13 @@ function N(e) {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
function
|
|
208
|
+
function R(e) {
|
|
209
209
|
return e.issues.map((e) => `${e.path.join(".") || "value"}: ${e.message}`).join(", ");
|
|
210
210
|
}
|
|
211
|
-
function
|
|
212
|
-
let r =
|
|
211
|
+
function z(e, t, n) {
|
|
212
|
+
let r = L(e);
|
|
213
213
|
if (!r.ok) return r;
|
|
214
|
-
let i =
|
|
214
|
+
let i = N.safeParse(r.value);
|
|
215
215
|
return i.success ? i.data.seq === t ? i.data.msg.topic === n ? {
|
|
216
216
|
ok: !0,
|
|
217
217
|
value: i.data
|
|
@@ -226,17 +226,17 @@ function F(e, t, n) {
|
|
|
226
226
|
} : {
|
|
227
227
|
ok: !1,
|
|
228
228
|
reason: "shape",
|
|
229
|
-
detail:
|
|
229
|
+
detail: R(i.error)
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
-
function
|
|
233
|
-
let r =
|
|
232
|
+
function B(e, t, n) {
|
|
233
|
+
let r = L(e);
|
|
234
234
|
if (!r.ok) return r;
|
|
235
|
-
let i =
|
|
235
|
+
let i = F.safeParse(r.value);
|
|
236
236
|
if (!i.success) return {
|
|
237
237
|
ok: !1,
|
|
238
238
|
reason: "shape",
|
|
239
|
-
detail:
|
|
239
|
+
detail: R(i.error)
|
|
240
240
|
};
|
|
241
241
|
if (i.data.topic !== t || i.data.toSeq !== n.toSeq) return {
|
|
242
242
|
ok: !1,
|
|
@@ -245,7 +245,7 @@ function I(e, t, n) {
|
|
|
245
245
|
};
|
|
246
246
|
let { fromSeq: a, toSeq: o, prev: s } = i.data, c = [], l = 0;
|
|
247
247
|
for (let e of i.data.messages) {
|
|
248
|
-
let n =
|
|
248
|
+
let n = P.safeParse(e);
|
|
249
249
|
n.success && n.data.seq >= a && n.data.seq <= o && n.data.msg.topic === t ? c.push(n.data) : l++;
|
|
250
250
|
}
|
|
251
251
|
return {
|
|
@@ -261,4 +261,48 @@ function I(e, t, n) {
|
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
263
|
//#endregion
|
|
264
|
-
|
|
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 };
|
package/dist/notes.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type SlotNote } from './message/index.js';
|
|
2
|
+
import { type ChatSource } from './swarm.js';
|
|
3
|
+
export interface NoteSettings {
|
|
4
|
+
/** The length of a time slot. Must be the server's, since it is part of every note's address. */
|
|
5
|
+
slotMs: number;
|
|
6
|
+
/** The longest the server goes without a note while a chat is active. Must be the server's. */
|
|
7
|
+
heartbeatMs: number;
|
|
8
|
+
/** How long after a slot ends, by the server's clock, its note is asked for. */
|
|
9
|
+
marginMs: number;
|
|
10
|
+
/** Notes read at once when looking back over several slots, newest first. */
|
|
11
|
+
scanBatch: number;
|
|
12
|
+
/** The furthest the clock correction goes either way. */
|
|
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;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_NOTE_SETTINGS: NoteSettings;
|
|
18
|
+
/** A note that was found, and the time slot it was found at. */
|
|
19
|
+
export interface FoundNote {
|
|
20
|
+
slot: number;
|
|
21
|
+
note: SlotNote;
|
|
22
|
+
}
|
|
23
|
+
export type NoteSource = Required<Pick<ChatSource, 'readNote'>>;
|
|
24
|
+
/** Whether a source can read notes, which a source written before notes existed cannot. */
|
|
25
|
+
export declare function readsNotes(source: ChatSource): source is ChatSource & NoteSource;
|
|
26
|
+
/**
|
|
27
|
+
* How far this viewer's clock runs ahead of the server's, in milliseconds, as far as reads can tell. A negative value
|
|
28
|
+
* is a clock that runs behind.
|
|
29
|
+
*
|
|
30
|
+
* Reads can only bound it from above. Whatever the server stamped, a note's `writtenAt` or an entry's `at`, it stamped
|
|
31
|
+
* before this viewer received it, so the viewer is at most `received - stamped` ahead. A read scheduled by this
|
|
32
|
+
* viewer's own clock says nothing more, since the same clock decides when it is sent. Learning that a clock runs
|
|
33
|
+
* ahead would need a read sent before the note existed, which is the request this whole scheme exists to avoid.
|
|
34
|
+
*
|
|
35
|
+
* So the correction trusts the local clock, `min(0, bound)`, which only ever moves a read earlier when the bound
|
|
36
|
+
* proves the clock runs behind, and never makes a read early. Once notes stop being found for longer than the
|
|
37
|
+
* heartbeat, the local clock is no longer trusted and every read waits out the whole bound. That is late by at most
|
|
38
|
+
* the freshest read's own delay and never early.
|
|
39
|
+
*
|
|
40
|
+
* A clock that runs behind sees every stamp exactly as old as it expects, since it reads everything as late as it
|
|
41
|
+
* runs behind, so no read proves it behind and its messages arrive that much late. Proving it would also take a read
|
|
42
|
+
* of something not yet written by the viewer's own clock.
|
|
43
|
+
*/
|
|
44
|
+
export declare class ClockCorrection {
|
|
45
|
+
private readonly limitMs;
|
|
46
|
+
private aheadAtMost;
|
|
47
|
+
private trusted;
|
|
48
|
+
constructor(limitMs: number);
|
|
49
|
+
observe(stampedMs: number, receivedMs: number): void;
|
|
50
|
+
/** Reads have stopped finding notes, which a clock that runs ahead of the server's explains. */
|
|
51
|
+
distrust(): void;
|
|
52
|
+
get isTrusted(): boolean;
|
|
53
|
+
get ms(): number;
|
|
54
|
+
}
|
|
55
|
+
/** Reads notes for the follower and the opening alike, so both share one clock correction and one count. */
|
|
56
|
+
export declare class NoteReader {
|
|
57
|
+
private readonly source;
|
|
58
|
+
readonly now: () => number;
|
|
59
|
+
readonly clock: ClockCorrection;
|
|
60
|
+
/** Note reads that found a note, and those that found none, an unreadable chunk or a note that failed its check. */
|
|
61
|
+
readonly counts: {
|
|
62
|
+
found: number;
|
|
63
|
+
empty: number;
|
|
64
|
+
};
|
|
65
|
+
readonly settings: NoteSettings;
|
|
66
|
+
private readonly pacer;
|
|
67
|
+
constructor(source: NoteSource, settings: Partial<NoteSettings>, now: () => number);
|
|
68
|
+
/** The newest time slot whose note may be asked for at `now`: over, and the margin past, by the server's clock. */
|
|
69
|
+
lastReadableSlot(now?: number): number;
|
|
70
|
+
/** The local time from which slot `slot`'s note may be asked for. */
|
|
71
|
+
readableAt(slot: number): number;
|
|
72
|
+
/** How many slots back a reader looks for a note: one heartbeat and two slots, the longest a note can be apart. */
|
|
73
|
+
get lookBackSlots(): number;
|
|
74
|
+
/** How long a follower goes without finding a note before it stops trusting its clock. */
|
|
75
|
+
get silenceMs(): number;
|
|
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
|
+
*/
|
|
81
|
+
read(slot: number): Promise<SlotNote | null>;
|
|
82
|
+
/**
|
|
83
|
+
* The newest note from slot `newest` back to slot `oldest`, read newest first, `scanBatch` at a time, stopping at
|
|
84
|
+
* the first batch that finds one. A note names the newest feed slot written by then, so the newest note found is the
|
|
85
|
+
* only one that matters.
|
|
86
|
+
*/
|
|
87
|
+
scan(newest: number, oldest: number): Promise<FoundNote | null>;
|
|
88
|
+
}
|
package/dist/pacer.d.ts
ADDED
|
@@ -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/swarm.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface ChatSource {
|
|
|
17
17
|
} | null>;
|
|
18
18
|
/** A history file's bytes. Rejects when it cannot be read. */
|
|
19
19
|
readFile(reference: string): Promise<Uint8Array>;
|
|
20
|
+
/**
|
|
21
|
+
* The payload of time slot `slot`'s note, or null when Bee answered that it is not there. Sorts its failures as
|
|
22
|
+
* `readSlot` does. A source without it follows the chat by polling the next slot, as before notes existed.
|
|
23
|
+
*/
|
|
24
|
+
readNote?(slot: number): Promise<Uint8Array | null>;
|
|
20
25
|
}
|
|
21
26
|
/** Bee's head lookup did not answer within its timeout, which on a long chat or a busy gateway is ordinary. */
|
|
22
27
|
export declare class HeadLookupTimeoutError extends Error {
|
|
@@ -59,8 +64,10 @@ export declare function readSlotThrough(index: number, read: () => Promise<Uint8
|
|
|
59
64
|
/**
|
|
60
65
|
* Reads a chat feed through bee-js's feed reader with an explicit index, which reads `GET /chunks/{address}` and
|
|
61
66
|
* answers 404 only when the chunk could not be found, where `/soc/{owner}/{id}` answers 404 for any failure at all.
|
|
67
|
+
* Notes are read through bee-js's single owner chunk reader, which reads the same `GET /chunks/{address}` and checks
|
|
68
|
+
* the chunk is the owner's.
|
|
62
69
|
*/
|
|
63
|
-
export declare function beeChatSource(bee: Bee, owner: string, chatTopic: string, timeouts: SwarmTimeouts): ChatSource;
|
|
70
|
+
export declare function beeChatSource(bee: Bee, owner: string, chatTopic: string, timeouts: SwarmTimeouts, noteSlotMs?: number): ChatSource;
|
|
64
71
|
/**
|
|
65
72
|
* A batch id for a gateway that stamps what is written through it and ignores the one it is sent. Bee refuses a
|
|
66
73
|
* write with no batch id at all, so something well formed has to go. The value 6.x sent, kept so a gateway that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarpunkltd/swarm-chat-js",
|
|
3
|
-
"version": "7.
|
|
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
|
-
"
|
|
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
|
+
}
|