Merge pull request #87 from waku-org/web-chat-bump-js-waku

This commit is contained in:
fryorcraken.eth 2022-09-02 14:32:33 +10:00 committed by GitHub
commit f698d97013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1934 additions and 1639 deletions

View File

@ -4,7 +4,8 @@
- Group chat
- React/TypeScript
- Waku Relay
- Waku Filter
- Waku Light Push
- Waku Store
A ReactJS chat app is provided as a showcase of the library used in the browser.

View File

@ -1,70 +0,0 @@
const webpack = require("webpack");
module.exports = {
dev: (config) => {
// Override webpack 5 config from react-scripts to load polyfills
if (!config.resolve) config.resolve = {};
if (!config.resolve.fallback) config.resolve.fallback = {};
Object.assign(config.resolve.fallback, {
assert: require.resolve("assert"),
buffer: require.resolve("buffer"),
crypto: false,
http: require.resolve("http-browserify"),
https: require.resolve("https-browserify"),
stream: require.resolve("stream-browserify"),
url: require.resolve("url"),
zlib: require.resolve("browserify-zlib"),
});
if (!config.plugins) config.plugins = [];
config.plugins.push(
new webpack.DefinePlugin({
"process.env.ENV": JSON.stringify("dev"),
})
);
config.plugins.push(
new webpack.ProvidePlugin({
process: "process/browser.js",
Buffer: ["buffer", "Buffer"],
})
);
if (!config.ignoreWarnings) config.ignoreWarnings = [];
config.ignoreWarnings.push(/Failed to parse source map/);
return config;
},
prod: (config) => {
// Override webpack 5 config from react-scripts to load polyfills
if (!config.resolve) config.resolve = {};
if (!config.resolve.fallback) config.resolve.fallback = {};
Object.assign(config.resolve.fallback, {
assert: require.resolve("assert"),
buffer: require.resolve("buffer"),
crypto: false,
http: require.resolve("http-browserify"),
https: require.resolve("https-browserify"),
stream: require.resolve("stream-browserify"),
url: require.resolve("url"),
zlib: require.resolve("browserify-zlib"),
});
if (!config.plugins) config.plugins = [];
config.plugins.push(
new webpack.DefinePlugin({
"process.env.ENV": JSON.stringify("prod"),
})
);
config.plugins.push(
new webpack.ProvidePlugin({
process: "process/browser.js",
Buffer: ["buffer", "Buffer"],
})
);
if (!config.ignoreWarnings) config.ignoreWarnings = [];
config.ignoreWarnings.push(/Failed to parse source map/);
return config;
},
};

View File

@ -2,53 +2,44 @@
"name": "web-chat",
"version": "0.1.0",
"private": true,
"homepage": "/examples/web-chat",
"homepage": "/web-chat",
"dependencies": {
"@livechat/ui-kit": "^0.5.0-20",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"js-waku": "^0.24.0",
"libp2p-interfaces": "^4.0.6",
"long": "^5.2.0",
"multiaddr": "^10.0.1",
"peer-id": "^0.16.0",
"@multiformats/multiaddr": "^10.4.0",
"js-waku": "0.24.0-f52dd9e",
"process": "^0.11.10",
"protobufjs": "^7.0.0",
"protons-runtime": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"server-name-generator": "^1.0.5",
"stream-browserify": "^3.0.0"
"uint8arraylist": "^2.3.2"
},
"devDependencies": {
"@types/jest": "^27.5.0",
"@types/node": "^17.0.32",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"assert": "^2.0.0",
"cra-webpack-rewired": "^1.0.1",
"cspell": "^6.0.0",
"gh-pages": "^4.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"protons": "^5.1.0",
"react-scripts": "5.0.1",
"typescript": "^4.6.4",
"url": "^0.11.0"
},
"scripts": {
"start": "cra-webpack-rewired start",
"build": "cra-webpack-rewired build",
"test:unit": "cra-webpack-rewired test",
"start": "react-scripts start",
"build": "react-scripts build",
"test:unit": "exit 0",
"fix": "run-s fix:*",
"test": "run-s build test:*",
"test:lint": "eslint src --ext .ts --ext .tsx",
"test:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" \"./config/*.js\" --list-different",
"test:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --list-different",
"test:spelling": "cspell \"{README.md,.github/*.md,src/**/*.{ts,tsx},public/**/*.html}\" -c ../.cspell.json",
"fix:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" \"./config/*.js\" --write",
"fix:prettier": "prettier \"src/**/*.{ts,tsx}\" \"./*.json\" --write",
"fix:lint": "eslint src --ext .ts --ext .tsx --fix",
"proto": "run-s proto:*",
"proto:build": "buf generate",
"proto": "protons src/proto/*.proto",
"js-waku:build": "cd ../; npm run build",
"predeploy": "run-s js-waku:build build",
"deploy": "gh-pages -d build"

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,14 @@
import { useEffect, useReducer, useState } from "react";
import "./App.css";
import {
discovery,
getPredefinedBootstrapNodes,
PageDirection,
Protocols,
Waku,
WakuFilter,
WakuLightPush,
WakuMessage,
WakuRelay,
WakuStore,
} from "js-waku";
import handleCommand from "./command";
import Room from "./Room";
@ -13,6 +16,14 @@ import { WakuContext } from "./WakuContext";
import { ThemeProvider } from "@livechat/ui-kit";
import { generate } from "server-name-generator";
import { Message } from "./Message";
import {
Fleet,
getPredefinedBootstrapNodes,
} from "js-waku/lib/predefined_bootstrap_nodes";
import { waitForRemotePeer } from "js-waku/lib/wait_for_remote_peer";
import { PeerDiscoveryStaticPeers } from "js-waku/lib/peer_discovery_static_list";
import { defaultLibp2p } from "js-waku/lib/create_waku";
import process from "process";
const themes = {
AuthorName: {
@ -110,7 +121,7 @@ export default function App() {
// Let's retrieve previous messages before listening to new messages
if (!historicalMessagesRetrieved) return;
const handleRelayMessage = (wakuMsg: WakuMessage) => {
const handleIncomingMessage = (wakuMsg: WakuMessage) => {
console.log("Message received: ", wakuMsg);
const msg = Message.fromWakuMessage(wakuMsg);
if (msg) {
@ -118,10 +129,26 @@ export default function App() {
}
};
waku.relay.addObserver(handleRelayMessage, [ChatContentTopic]);
let unsubscribe: undefined | (() => Promise<void>);
waku.filter.subscribe(handleIncomingMessage, [ChatContentTopic]).then(
(_unsubscribe) => {
console.log("subscribed to ", ChatContentTopic);
unsubscribe = _unsubscribe;
},
(e) => {
console.error("Failed to subscribe", e);
}
);
return function cleanUp() {
waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]);
if (!waku) return;
if (typeof unsubscribe === "undefined") return;
unsubscribe().then(
() => {
console.log("unsubscribed to ", ChatContentTopic);
},
(e) => console.error("Failed to unsubscribe", e)
);
};
}, [waku, historicalMessagesRetrieved]);
@ -130,7 +157,11 @@ export default function App() {
if (historicalMessagesRetrieved) return;
const retrieveMessages = async () => {
await waku.waitForRemotePeer();
await waitForRemotePeer(waku, [
Protocols.Store,
Protocols.Filter,
Protocols.LightPush,
]);
console.log(`Retrieving archived messages`);
try {
@ -175,19 +206,23 @@ export default function App() {
async function initWaku(setter: (waku: Waku) => void) {
try {
const waku = await Waku.create({
libp2p: {
config: {
pubsub: {
enabled: true,
emitSelf: true,
},
},
},
bootstrap: {
peers: getPredefinedBootstrapNodes(selectFleetEnv()),
},
// TODO: Remove this declaration once there are optional in js-waku
const wakuRelay = new WakuRelay({ emitSelf: true });
const libp2p = await defaultLibp2p(wakuRelay, {
peerDiscovery: [
new PeerDiscoveryStaticPeers(
getPredefinedBootstrapNodes(selectFleetEnv())
),
],
});
const wakuStore = new WakuStore(libp2p);
const wakuLightPush = new WakuLightPush(libp2p);
const wakuFilter = new WakuFilter(libp2p);
const waku = new Waku({}, libp2p, wakuStore, wakuLightPush, wakuFilter);
await waku.start();
setter(waku);
} catch (e) {
@ -197,10 +232,11 @@ async function initWaku(setter: (waku: Waku) => void) {
function selectFleetEnv() {
// Works with react-scripts
if (process?.env?.NODE_ENV === "development") {
return discovery.predefined.Fleet.Test;
// TODO: Re-enable the switch once nwaku v0.12 is deployed
if (true || process?.env?.NODE_ENV === "development") {
return Fleet.Test;
} else {
return discovery.predefined.Fleet.Prod;
return Fleet.Prod;
}
}

View File

@ -16,12 +16,10 @@ export default function ChatList(props: Props) {
const renderedMessages = props.messages.map((message) => (
<LiveMessage
key={
message.sentTimestamp
? message.sentTimestamp.valueOf()
: "" +
message.timestamp.valueOf() +
message.nick +
message.payloadAsUtf8
message.nick +
message.payloadAsUtf8 +
message.timestamp.valueOf() +
message.sentTimestamp?.valueOf()
}
authorName={message.nick}
date={formatDisplayDate(message)}

View File

@ -1,4 +1,4 @@
import { ChangeEvent, KeyboardEvent, useState } from "react";
import { ChangeEvent, KeyboardEvent, useEffect, useState } from "react";
import { useWaku } from "./WakuContext";
import {
TextInput,
@ -15,6 +15,7 @@ interface Props {
export default function MessageInput(props: Props) {
const [inputText, setInputText] = useState<string>("");
const [activeButton, setActiveButton] = useState<boolean>(false);
const { waku } = useWaku();
const sendMessage = async () => {
@ -39,9 +40,21 @@ export default function MessageInput(props: Props) {
}
};
// Enable the button if there are relay peers available or the user is sending a command
const activeButton =
(waku && waku.relay.getPeers().size !== 0) || inputText.startsWith("/");
// Enable the button if there are peers available or the user is sending a command
useEffect(() => {
if (inputText.startsWith("/")) {
setActiveButton(true);
} else if (waku) {
(async () => {
const peers = await waku.lightPush.peers();
if (!!peers) {
setActiveButton(true);
} else {
setActiveButton(false);
}
})();
}
}, [activeButton, inputText, waku]);
return (
<TextComposer

View File

@ -1,4 +1,4 @@
import { WakuMessage } from "js-waku";
import { PushResponse, WakuMessage } from "js-waku";
import { ChatContentTopic } from "./App";
import ChatList from "./ChatList";
import MessageInput from "./MessageInput";
@ -18,28 +18,22 @@ export default function Room(props: Props) {
const { waku } = useWaku();
const [storePeers, setStorePeers] = useState(0);
const [relayPeers, setRelayPeers] = useState(0);
useEffect(() => {
if (!waku) return;
// Update relay peer count on heartbeat
waku.relay.on("gossipsub:heartbeat", () => {
setRelayPeers(waku.relay.getPeers().size);
});
}, [waku]);
const [filterPeers, setFilterPeers] = useState(0);
const [lightPushPeers, setLightPushPeers] = useState(0);
useEffect(() => {
if (!waku) return;
// Update store peer when new peer connected & identified
waku.libp2p.peerStore.on("change:protocols", async () => {
let counter = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _peer of waku.store.peers) {
counter++;
}
setStorePeers(counter);
waku.libp2p.peerStore.addEventListener("change:protocols", async () => {
const storePeers = await waku.store.peers();
setStorePeers(storePeers.length);
const filterPeers = await waku.filter.peers();
setFilterPeers(filterPeers.length);
const lightPushPeers = await waku.lightPush.peers();
setLightPushPeers(lightPushPeers.length);
});
}, [waku]);
@ -49,7 +43,9 @@ export default function Room(props: Props) {
style={{ height: "98vh", display: "flex", flexDirection: "column" }}
>
<TitleBar
leftIcons={[`Peers: ${relayPeers} relay ${storePeers} store.`]}
leftIcons={[
`Peers: ${lightPushPeers} light push, ${filterPeers} filter, ${storePeers} store.`,
]}
title="Waku v2 chat app"
/>
<ChatList messages={props.messages} />
@ -61,7 +57,7 @@ export default function Room(props: Props) {
messageToSend,
props.nick,
props.commandHandler,
waku.relay.send.bind(waku.relay)
waku.lightPush.push.bind(waku.lightPush)
);
}
: undefined
@ -75,7 +71,7 @@ async function handleMessage(
message: string,
nick: string,
commandHandler: (cmd: string) => void,
messageSender: (msg: WakuMessage) => Promise<void>
messageSender: (msg: WakuMessage) => Promise<PushResponse | null>
) {
if (message.startsWith("/")) {
commandHandler(message);
@ -87,6 +83,6 @@ async function handleMessage(
ChatContentTopic,
{ timestamp }
);
return messageSender(wakuMsg);
await messageSender(wakuMsg);
}
}

View File

@ -1,30 +0,0 @@
import WakuMock, { Message } from "./WakuMock";
test("Messages are emitted", async () => {
const wakuMock = await WakuMock.create();
let message: Message;
wakuMock.on("message", (msg) => {
message = msg;
});
await new Promise((resolve) => setTimeout(resolve, 2000));
// @ts-ignore
expect(message.message).toBeDefined();
});
test("Messages are sent", async () => {
const wakuMock = await WakuMock.create();
const text = "This is a message.";
let message: Message;
wakuMock.on("message", (msg) => {
message = msg;
});
await wakuMock.send(text);
// @ts-ignore
expect(message.message).toEqual(text);
});

View File

@ -1,69 +0,0 @@
class EventEmitter<T> {
public callbacks: { [key: string]: Array<(data: T) => void> };
constructor() {
this.callbacks = {};
}
on(event: string, cb: (data: T) => void) {
if (!this.callbacks[event]) this.callbacks[event] = [];
this.callbacks[event].push(cb);
}
emit(event: string, data: T) {
let cbs = this.callbacks[event];
if (cbs) {
cbs.forEach((cb) => cb(data));
}
}
}
export interface Message {
timestamp: Date;
handle: string;
message: string;
}
export default class WakuMock extends EventEmitter<Message> {
index: number;
intervalId?: number | NodeJS.Timeout;
private constructor() {
super();
this.index = 0;
}
public static async create(): Promise<WakuMock> {
await new Promise((resolve) => setTimeout(resolve, 500));
const wakuMock = new WakuMock();
wakuMock.startInterval();
return wakuMock;
}
public async send(message: string): Promise<void> {
const timestamp = new Date();
const handle = "me";
this.emit("message", {
timestamp,
handle,
message,
});
}
private startInterval() {
if (this.intervalId === undefined) {
this.intervalId = setInterval(this.emitMessage.bind(this), 1000);
}
}
private emitMessage() {
const handle = "you";
const timestamp = new Date();
this.emit("message", {
timestamp,
handle,
message: `This is message #${this.index++}.`,
});
}
}

View File

@ -1,5 +1,4 @@
import { Reader } from "protobufjs/minimal";
import { utils } from "js-waku";
import * as proto from "./proto/chat_message";
/**
@ -20,8 +19,8 @@ export class ChatMessage {
nick: string,
text: string
): ChatMessage {
const timestampNumber = Math.floor(timestamp.valueOf() / 1000);
const payload = Buffer.from(text, "utf-8");
const timestampNumber = BigInt(Math.floor(timestamp.valueOf() / 1000));
const payload = utils.utf8ToBytes(text);
return new ChatMessage({
timestamp: timestampNumber,
@ -35,7 +34,7 @@ export class ChatMessage {
* @param bytes The payload to decode.
*/
static decode(bytes: Uint8Array): ChatMessage {
const protoMsg = proto.ChatMessage.decode(Reader.create(bytes));
const protoMsg = proto.ChatMessage.decode(bytes);
return new ChatMessage(protoMsg);
}
@ -44,11 +43,11 @@ export class ChatMessage {
* @returns The encoded payload.
*/
encode(): Uint8Array {
return proto.ChatMessage.encode(this.proto).finish();
return proto.ChatMessage.encode(this.proto);
}
get timestamp(): Date {
return new Date(this.proto.timestamp * 1000);
return new Date(Number(this.proto.timestamp * BigInt(1000)));
}
get nick(): string {
@ -60,6 +59,6 @@ export class ChatMessage {
return "";
}
return Buffer.from(this.proto.payload).toString("utf-8");
return utils.bytesToUtf8(this.proto.payload);
}
}

View File

@ -1,5 +1,4 @@
import { multiaddr } from "multiaddr";
import PeerId from "peer-id";
import { multiaddr } from "@multiformats/multiaddr";
import { Waku } from "js-waku";
function help(): string[] {
@ -26,7 +25,7 @@ function info(waku: Waku | undefined): string[] {
if (!waku) {
return ["Waku node is starting"];
}
return [`PeerId: ${waku.libp2p.peerId.toB58String()}`];
return [`PeerId: ${waku.libp2p.peerId.toString()}`];
}
function connect(peer: string | undefined, waku: Waku | undefined): string[] {
@ -42,9 +41,7 @@ function connect(peer: string | undefined, waku: Waku | undefined): string[] {
if (!peerId) {
return ["Peer Id needed to dial"];
}
waku.addPeerToAddressBook(PeerId.createFromB58String(peerId), [
peerMultiaddr,
]);
waku.addPeerToAddressBook(peerId, [peerMultiaddr]);
return [
`${peerId}: ${peerMultiaddr.toString()} added to address book, autodial in progress`,
];
@ -58,13 +55,10 @@ async function peers(waku: Waku | undefined): Promise<string[]> {
return ["Waku node is starting"];
}
let response: string[] = [];
const peers = [];
const peers = await waku.libp2p.peerStore.all();
for await (const peer of waku.libp2p.peerStore.getPeers()) {
peers.push(peer);
}
Array.from(peers).forEach((peer) => {
response.push(peer.id.toB58String() + ":");
response.push(peer.id.toString() + ":");
let addresses = " addresses: [";
peer.addresses.forEach(({ multiaddr }) => {
addresses += " " + multiaddr.toString() + ",";
@ -88,21 +82,14 @@ function connections(waku: Waku | undefined): string[] {
return ["Waku node is starting"];
}
let response: string[] = [];
waku.libp2p.connections.forEach(
(
connections: import("libp2p-interfaces/src/connection/connection")[],
peerId
) => {
response.push(peerId + ":");
let strConnections = " connections: [";
connections.forEach((connection) => {
strConnections += JSON.stringify(connection.stat);
strConnections += "; " + JSON.stringify(connection.streams);
});
strConnections += "]";
response.push(strConnections);
}
);
let strConnections = " connections: \n";
waku.libp2p.connectionManager.getConnections().forEach((connection) => {
strConnections += connection.remotePeer.toString() + ", ";
strConnections += JSON.stringify(connection.stat);
strConnections += "; " + JSON.stringify(connection.streams);
strConnections += "\n";
});
response.push(strConnections);
if (response.length === 0) {
response.push("Not connected to any peer.");
}

View File

@ -1,169 +1,117 @@
/* eslint-disable */
import Long from "long";
import _m0 from "protobufjs/minimal";
/* eslint-disable import/export */
/* eslint-disable @typescript-eslint/no-namespace */
export const protobufPackage = "";
import { encodeMessage, decodeMessage, message } from "protons-runtime";
import type { Uint8ArrayList } from "uint8arraylist";
import type { Codec } from "protons-runtime";
export interface ChatMessage {
timestamp: number;
timestamp: bigint;
nick: string;
payload: Uint8Array;
}
function createBaseChatMessage(): ChatMessage {
return { timestamp: 0, nick: "", payload: new Uint8Array() };
}
export namespace ChatMessage {
let _codec: Codec<ChatMessage>;
export const ChatMessage = {
encode(
message: ChatMessage,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.timestamp !== 0) {
writer.uint32(8).uint64(message.timestamp);
export const codec = (): Codec<ChatMessage> => {
if (_codec == null) {
_codec = message<ChatMessage>(
(obj, writer, opts = {}) => {
if (opts.lengthDelimited !== false) {
writer.fork();
}
if (obj.timestamp != null) {
writer.uint32(8);
writer.uint64(obj.timestamp);
} else {
throw new Error(
'Protocol error: required field "timestamp" was not found in object'
);
}
if (obj.nick != null) {
writer.uint32(18);
writer.string(obj.nick);
} else {
throw new Error(
'Protocol error: required field "nick" was not found in object'
);
}
if (obj.payload != null) {
writer.uint32(26);
writer.bytes(obj.payload);
} else {
throw new Error(
'Protocol error: required field "payload" was not found in object'
);
}
if (opts.lengthDelimited !== false) {
writer.ldelim();
}
},
(reader, length) => {
const obj: any = {
timestamp: 0n,
nick: "",
payload: new Uint8Array(0),
};
const end = length == null ? reader.len : reader.pos + length;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
obj.timestamp = reader.uint64();
break;
case 2:
obj.nick = reader.string();
break;
case 3:
obj.payload = reader.bytes();
break;
default:
reader.skipType(tag & 7);
break;
}
}
if (obj.timestamp == null) {
throw new Error(
'Protocol error: value for required field "timestamp" was not found in protobuf'
);
}
if (obj.nick == null) {
throw new Error(
'Protocol error: value for required field "nick" was not found in protobuf'
);
}
if (obj.payload == null) {
throw new Error(
'Protocol error: value for required field "payload" was not found in protobuf'
);
}
return obj;
}
);
}
if (message.nick !== "") {
writer.uint32(18).string(message.nick);
}
if (message.payload.length !== 0) {
writer.uint32(26).bytes(message.payload);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ChatMessage {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseChatMessage();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.timestamp = longToNumber(reader.uint64() as Long);
break;
case 2:
message.nick = reader.string();
break;
case 3:
message.payload = reader.bytes();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
return _codec;
};
fromJSON(object: any): ChatMessage {
const message = createBaseChatMessage();
message.timestamp =
object.timestamp !== undefined && object.timestamp !== null
? Number(object.timestamp)
: 0;
message.nick =
object.nick !== undefined && object.nick !== null
? String(object.nick)
: "";
message.payload =
object.payload !== undefined && object.payload !== null
? bytesFromBase64(object.payload)
: new Uint8Array();
return message;
},
export const encode = (obj: ChatMessage): Uint8Array => {
return encodeMessage(obj, ChatMessage.codec());
};
toJSON(message: ChatMessage): unknown {
const obj: any = {};
message.timestamp !== undefined &&
(obj.timestamp = Math.round(message.timestamp));
message.nick !== undefined && (obj.nick = message.nick);
message.payload !== undefined &&
(obj.payload = base64FromBytes(
message.payload !== undefined ? message.payload : new Uint8Array()
));
return obj;
},
fromPartial<I extends Exact<DeepPartial<ChatMessage>, I>>(
object: I
): ChatMessage {
const message = createBaseChatMessage();
message.timestamp = object.timestamp ?? 0;
message.nick = object.nick ?? "";
message.payload = object.payload ?? new Uint8Array();
return message;
},
};
declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var globalThis: any = (() => {
if (typeof globalThis !== "undefined") return globalThis;
if (typeof self !== "undefined") return self;
if (typeof window !== "undefined") return window;
if (typeof global !== "undefined") return global;
throw "Unable to locate global object";
})();
const atob: (b64: string) => string =
globalThis.atob ||
((b64) => globalThis.Buffer.from(b64, "base64").toString("binary"));
function bytesFromBase64(b64: string): Uint8Array {
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
const btoa: (bin: string) => string =
globalThis.btoa ||
((bin) => globalThis.Buffer.from(bin, "binary").toString("base64"));
function base64FromBytes(arr: Uint8Array): string {
const bin: string[] = [];
for (const byte of arr) {
bin.push(String.fromCharCode(byte));
}
return btoa(bin.join(""));
}
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<
Exclude<keyof I, KeysOfUnion<P>>,
never
>;
function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
return long.toNumber();
}
if (_m0.util.Long !== Long) {
_m0.util.Long = Long as any;
_m0.configure();
export const decode = (buf: Uint8Array | Uint8ArrayList): ChatMessage => {
return decodeMessage(buf, ChatMessage.codec());
};
}