@meshagent/meshagent-react 0.4.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.
@@ -0,0 +1,66 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ /**
3
+ * Connects to a Mesh document inside an existing RoomClient and keeps it in sync.
4
+ *
5
+ * The function retries with an exponential back‑off (capped at 60 s) until the
6
+ * document becomes available or the component unmounts.
7
+ *
8
+ * @param room An already‑connected RoomClient.
9
+ * @param path Path to the document inside the room.
10
+ */
11
+ export function useDocumentConnection({ room, path }) {
12
+ const [document, setDocument] = useState(null);
13
+ const [error, setError] = useState(null);
14
+ const retryCountRef = useRef(0);
15
+ const timeoutRef = useRef(null);
16
+ useEffect(() => {
17
+ let cancelled = false;
18
+ const openDocument = async () => {
19
+ try {
20
+ const doc = await room.sync.open(path);
21
+ if (cancelled)
22
+ return;
23
+ // sleep for 100 ms to ensure the document is ready
24
+ await new Promise(resolve => setTimeout(resolve, 100));
25
+ setDocument(doc);
26
+ setError(null);
27
+ }
28
+ catch (err) {
29
+ if (cancelled)
30
+ return;
31
+ setError(err);
32
+ // Exponential back‑off: 500 ms, 1 s, 2 s, … up to 60 s.
33
+ const delay = Math.min(60000, 500 * 2 ** retryCountRef.current);
34
+ retryCountRef.current += 1;
35
+ timeoutRef.current = setTimeout(openDocument, delay);
36
+ }
37
+ };
38
+ openDocument();
39
+ return () => {
40
+ cancelled = true;
41
+ if (timeoutRef.current !== null) {
42
+ clearTimeout(timeoutRef.current);
43
+ timeoutRef.current = null;
44
+ }
45
+ room.sync.close(path);
46
+ setDocument(null);
47
+ retryCountRef.current = 0;
48
+ };
49
+ }, [path]);
50
+ return {
51
+ document,
52
+ error,
53
+ loading: document === null && error == null,
54
+ };
55
+ }
56
+ export function useDocumentChanged({ document, onChanged }) {
57
+ useEffect(() => {
58
+ if (document) {
59
+ const s = document.listen(() => {
60
+ setTimeout(() => onChanged(document), 40);
61
+ });
62
+ onChanged(document);
63
+ return () => s.unsubscribe();
64
+ }
65
+ }, [document]);
66
+ }
@@ -0,0 +1,43 @@
1
+ import { EventEmitter, RoomClient } from "@meshagent/meshagent";
2
+ export declare enum UploadStatus {
3
+ Initial = "initial",
4
+ Uploading = "uploading",
5
+ Completed = "completed",
6
+ Failed = "failed"
7
+ }
8
+ interface UploadStatusEvent {
9
+ status: UploadStatus;
10
+ progress?: number;
11
+ }
12
+ export declare abstract class FileUpload extends EventEmitter<UploadStatusEvent> {
13
+ path: string;
14
+ size: number;
15
+ protected _status: UploadStatus;
16
+ protected constructor(path: string, size?: number);
17
+ get status(): UploadStatus;
18
+ protected set status(value: UploadStatus);
19
+ abstract get bytesUploaded(): number;
20
+ abstract get done(): Promise<void>;
21
+ get filename(): string;
22
+ abstract startUpload(): void;
23
+ }
24
+ export declare class MeshagentFileUpload extends FileUpload {
25
+ readonly room: RoomClient;
26
+ readonly dataStream: AsyncIterable<Uint8Array>;
27
+ private _bytesUploaded;
28
+ private _done;
29
+ private _resolveDone;
30
+ private _rejectDone;
31
+ private _downloadUrl;
32
+ private _resolveUrl;
33
+ private _rejectUrl;
34
+ constructor(room: RoomClient, path: string, dataStream: AsyncIterable<Uint8Array>, size?: number, autoStart?: boolean);
35
+ static deferred(room: RoomClient, path: string, dataStream: AsyncIterable<Uint8Array>, size?: number): MeshagentFileUpload;
36
+ get bytesUploaded(): number;
37
+ get done(): Promise<void>;
38
+ /** Resolves to the server’s public download URL – like Dart version. */
39
+ get downloadUrl(): Promise<URL>;
40
+ startUpload(): void;
41
+ private _upload;
42
+ }
43
+ export {};
@@ -0,0 +1,162 @@
1
+ import { EventEmitter } from "@meshagent/meshagent";
2
+ export var UploadStatus;
3
+ (function (UploadStatus) {
4
+ UploadStatus["Initial"] = "initial";
5
+ UploadStatus["Uploading"] = "uploading";
6
+ UploadStatus["Completed"] = "completed";
7
+ UploadStatus["Failed"] = "failed";
8
+ })(UploadStatus || (UploadStatus = {}));
9
+ export class FileUpload extends EventEmitter {
10
+ constructor(path, size = 0) {
11
+ super();
12
+ Object.defineProperty(this, "path", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: path
17
+ });
18
+ Object.defineProperty(this, "size", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: size
23
+ });
24
+ Object.defineProperty(this, "_status", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: UploadStatus.Initial
29
+ });
30
+ }
31
+ get status() {
32
+ return this._status;
33
+ }
34
+ set status(value) {
35
+ if (this._status !== value) {
36
+ this._status = value;
37
+ this.emit("status", {
38
+ status: value,
39
+ progress: this.bytesUploaded,
40
+ });
41
+ }
42
+ }
43
+ get filename() {
44
+ return this.path.split("/").pop() ?? "";
45
+ }
46
+ }
47
+ export class MeshagentFileUpload extends FileUpload {
48
+ constructor(room, path, dataStream, size = 0, autoStart = true) {
49
+ super(path, size);
50
+ Object.defineProperty(this, "room", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: room
55
+ });
56
+ Object.defineProperty(this, "dataStream", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: dataStream
61
+ });
62
+ Object.defineProperty(this, "_bytesUploaded", {
63
+ enumerable: true,
64
+ configurable: true,
65
+ writable: true,
66
+ value: 0
67
+ });
68
+ Object.defineProperty(this, "_done", {
69
+ enumerable: true,
70
+ configurable: true,
71
+ writable: true,
72
+ value: void 0
73
+ });
74
+ Object.defineProperty(this, "_resolveDone", {
75
+ enumerable: true,
76
+ configurable: true,
77
+ writable: true,
78
+ value: void 0
79
+ });
80
+ Object.defineProperty(this, "_rejectDone", {
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true,
84
+ value: void 0
85
+ });
86
+ Object.defineProperty(this, "_downloadUrl", {
87
+ enumerable: true,
88
+ configurable: true,
89
+ writable: true,
90
+ value: void 0
91
+ });
92
+ Object.defineProperty(this, "_resolveUrl", {
93
+ enumerable: true,
94
+ configurable: true,
95
+ writable: true,
96
+ value: void 0
97
+ });
98
+ Object.defineProperty(this, "_rejectUrl", {
99
+ enumerable: true,
100
+ configurable: true,
101
+ writable: true,
102
+ value: void 0
103
+ });
104
+ this._done = new Promise((res, rej) => {
105
+ this._resolveDone = res;
106
+ this._rejectDone = rej;
107
+ });
108
+ this._downloadUrl = new Promise((res, rej) => {
109
+ this._resolveUrl = res;
110
+ this._rejectUrl = rej;
111
+ });
112
+ if (autoStart)
113
+ this._upload();
114
+ }
115
+ static deferred(room, path, dataStream, size = 0) {
116
+ return new MeshagentFileUpload(room, path, dataStream, size, false);
117
+ }
118
+ get bytesUploaded() {
119
+ return this._bytesUploaded;
120
+ }
121
+ get done() {
122
+ return this._done;
123
+ }
124
+ /** Resolves to the server’s public download URL – like Dart version. */
125
+ get downloadUrl() {
126
+ return this._downloadUrl;
127
+ }
128
+ startUpload() {
129
+ this._upload(); // idempotent guard inside _upload()
130
+ }
131
+ async _upload() {
132
+ if (this.status !== UploadStatus.Initial) {
133
+ throw new Error("upload already started or completed");
134
+ }
135
+ try {
136
+ const handle = await this.room.storage.open(this.path, { overwrite: true });
137
+ try {
138
+ this.status = UploadStatus.Uploading;
139
+ for await (const chunk of this.dataStream) {
140
+ await this.room.storage.write(handle, chunk);
141
+ this._bytesUploaded += chunk.length;
142
+ this.emit("progress", {
143
+ status: UploadStatus.Uploading,
144
+ progress: this.bytesUploaded / this.size,
145
+ });
146
+ }
147
+ }
148
+ finally {
149
+ await this.room.storage.close(handle);
150
+ }
151
+ this._resolveDone();
152
+ this.status = UploadStatus.Completed;
153
+ const urlStr = await this.room.storage.downloadUrl(this.path);
154
+ this._resolveUrl(new URL(urlStr));
155
+ }
156
+ catch (err) {
157
+ this.status = UploadStatus.Failed;
158
+ this._rejectDone(err);
159
+ this._rejectUrl(err);
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,5 @@
1
+ export * from './chat';
2
+ export * from './client-toolkits';
3
+ export * from './document-connection-scope';
4
+ export * from './file-upload';
5
+ export * from './room-connection-scope';
@@ -0,0 +1,5 @@
1
+ export * from './chat';
2
+ export * from './client-toolkits';
3
+ export * from './document-connection-scope';
4
+ export * from './file-upload';
5
+ export * from './room-connection-scope';
@@ -0,0 +1,47 @@
1
+ import { RoomClient } from '@meshagent/meshagent';
2
+ export interface RoomConnectionInfo {
3
+ url: string;
4
+ jwt: string;
5
+ }
6
+ export declare const developmentAuthorization: ({ url, projectId, apiKeyId, participantName, roomName, secret, }: {
7
+ url: string;
8
+ projectId: string;
9
+ apiKeyId: string;
10
+ participantName: string;
11
+ roomName: string;
12
+ secret: string;
13
+ }) => (() => Promise<RoomConnectionInfo>);
14
+ export declare const staticAuthorization: ({ url, jwt, }: {
15
+ url: string;
16
+ jwt: string;
17
+ }) => (() => Promise<RoomConnectionInfo>);
18
+ export interface UseRoomConnectionOptions {
19
+ /** Async function that returns `{ url, jwt }` for the room. */
20
+ authorization: () => Promise<{
21
+ url: string;
22
+ jwt: string;
23
+ }>;
24
+ /** Enable the optional messaging layer (default = `true`). */
25
+ enableMessaging?: boolean;
26
+ }
27
+ /**
28
+ * Shape of the object returned by the hook.
29
+ */
30
+ export interface UseRoomConnectionResult {
31
+ client: RoomClient | null;
32
+ state: 'authorizing' | 'connecting' | 'ready' | 'done';
33
+ ready: boolean;
34
+ done: boolean;
35
+ error: unknown;
36
+ dispose: () => void;
37
+ }
38
+ export declare function useRoomConnection(props: UseRoomConnectionOptions): UseRoomConnectionResult;
39
+ export interface UseRoomIndicatorsResult {
40
+ typing: boolean;
41
+ thinking: boolean;
42
+ }
43
+ export interface UseRoomIndicatorsProps {
44
+ room: RoomClient | null;
45
+ path: string;
46
+ }
47
+ export declare function useRoomIndicators({ room, path }: UseRoomIndicatorsProps): UseRoomIndicatorsResult;
@@ -0,0 +1,141 @@
1
+ import { useEffect, useState, useRef } from 'react';
2
+ import { subscribe } from './subscribe-async-gen';
3
+ import { RoomMessageEvent } from '@meshagent/meshagent';
4
+ import { ParticipantToken, Protocol, RoomClient, WebSocketProtocolChannel, } from '@meshagent/meshagent';
5
+ /* -------------------------------------------------
6
+ * Authorization helpers
7
+ * ------------------------------------------------- */
8
+ export const developmentAuthorization = ({ url, projectId, apiKeyId, participantName, roomName, secret, }) => async () => {
9
+ const token = new ParticipantToken({
10
+ name: participantName,
11
+ projectId,
12
+ apiKeyId,
13
+ });
14
+ token.addRoomGrant(roomName);
15
+ token.addRoleGrant('user');
16
+ const jwt = await token.toJwt({ token: secret });
17
+ return { url, jwt };
18
+ };
19
+ export const staticAuthorization = ({ url, jwt, }) => async () => ({ url, jwt });
20
+ export function useRoomConnection(props) {
21
+ const { authorization, enableMessaging = true, } = props;
22
+ const [client, setClient] = useState(null);
23
+ const [ready, setReady] = useState(false);
24
+ const [state, setState] = useState('authorizing');
25
+ const [error, setError] = useState(null);
26
+ // Keep the latest client in a ref so we can call `dispose` in cleanup.
27
+ const clientRef = useRef(null);
28
+ clientRef.current = client;
29
+ // Instance method exposed to consumers (rarely needed).
30
+ const dispose = () => {
31
+ clientRef.current?.dispose();
32
+ setState('done');
33
+ };
34
+ useEffect(() => {
35
+ let cancelled = false;
36
+ const connect = async () => {
37
+ try {
38
+ // 1️⃣ Get connection credentials
39
+ const { url, jwt } = await authorization();
40
+ if (cancelled)
41
+ return;
42
+ const room = new RoomClient({
43
+ protocol: new Protocol({
44
+ channel: new WebSocketProtocolChannel({ url, jwt }),
45
+ }),
46
+ });
47
+ setClient(room);
48
+ setState('connecting');
49
+ await room.start({
50
+ onDone: () => {
51
+ if (cancelled)
52
+ return;
53
+ setState('done');
54
+ },
55
+ onError: (e) => {
56
+ if (cancelled)
57
+ return;
58
+ setError(e);
59
+ setState('done');
60
+ },
61
+ });
62
+ if (enableMessaging) {
63
+ await room.messaging.enable();
64
+ }
65
+ if (cancelled)
66
+ return;
67
+ setState('ready');
68
+ setReady(true);
69
+ }
70
+ catch (e) {
71
+ if (cancelled)
72
+ return;
73
+ setError(e);
74
+ setState('done');
75
+ }
76
+ };
77
+ connect();
78
+ return () => {
79
+ // React unmount or deps change → cancel & dispose
80
+ cancelled = true;
81
+ dispose();
82
+ };
83
+ // eslint‑disable‑next‑line react-hooks/exhaustive-deps
84
+ }, []); // run once, just like componentDidMount
85
+ return {
86
+ client,
87
+ state,
88
+ ready,
89
+ done: state === 'done',
90
+ error,
91
+ dispose,
92
+ };
93
+ }
94
+ export function useRoomIndicators({ room, path }) {
95
+ const typingMap = useRef({});
96
+ const thinkingSet = useRef(new Set());
97
+ const [typing, setState] = useState(false);
98
+ const [thinking, setThinking] = useState(false);
99
+ useEffect(() => {
100
+ if (!room)
101
+ return;
102
+ const s = subscribe(room.listen(), {
103
+ next: (event) => {
104
+ if (event instanceof RoomMessageEvent) {
105
+ const { message } = event;
106
+ // Ignore messages from ourselves
107
+ if (message.fromParticipantId === room.localParticipant?.id) {
108
+ return;
109
+ }
110
+ // Ignore messages not for this path
111
+ if (message.message.path !== path) {
112
+ return;
113
+ }
114
+ if (message.type === "typing") {
115
+ // Clear any existing timer for this participant
116
+ clearTimeout(typingMap.current[message.fromParticipantId]);
117
+ // Set a new timer to remove typing after 1 second
118
+ typingMap.current[message.fromParticipantId] = setTimeout(() => {
119
+ delete typingMap.current[message.fromParticipantId];
120
+ setState(Object.keys(typingMap.current).length > 0);
121
+ }, 1000);
122
+ // Update typing state
123
+ setState(Object.keys(typingMap.current).length > 0);
124
+ }
125
+ else if (message.type === "thinking") {
126
+ if (message.message.thinking) {
127
+ thinkingSet.current.add(message.fromParticipantId);
128
+ }
129
+ else {
130
+ thinkingSet.current.delete(message.fromParticipantId);
131
+ }
132
+ // Update thinking state
133
+ setThinking(thinkingSet.current.size > 0);
134
+ }
135
+ }
136
+ },
137
+ });
138
+ return () => s.unsubscribe();
139
+ }, [room, path]);
140
+ return { typing, thinking };
141
+ }
@@ -0,0 +1,10 @@
1
+ type Subscriber<T> = {
2
+ next: (value: T) => void;
3
+ error?: (err: unknown) => void;
4
+ complete?: () => void;
5
+ };
6
+ export interface Subscription {
7
+ unsubscribe: () => void;
8
+ }
9
+ export declare function subscribe<T>(iterable: AsyncIterable<T>, { next, error, complete }: Subscriber<T>): Subscription;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ function abortableNext(iterator, signal) {
2
+ const nextP = iterator.next();
3
+ const abortP = new Promise((_, rej) => {
4
+ signal.addEventListener("abort", () => rej(new Error("aborted")), { once: true });
5
+ });
6
+ return Promise.race([nextP, abortP]);
7
+ }
8
+ export function subscribe(iterable, { next, error, complete }) {
9
+ const controller = new AbortController();
10
+ const it = iterable[Symbol.asyncIterator]();
11
+ (async () => {
12
+ try {
13
+ while (!controller.signal.aborted) {
14
+ const { value, done } = await abortableNext(it, controller.signal);
15
+ if (done)
16
+ break;
17
+ next(value);
18
+ }
19
+ if (!controller.signal.aborted && complete) {
20
+ complete();
21
+ }
22
+ }
23
+ catch (err) {
24
+ if (!controller.signal.aborted && error) {
25
+ error(err);
26
+ }
27
+ }
28
+ })();
29
+ return {
30
+ unsubscribe: () => controller.abort(),
31
+ };
32
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@meshagent/meshagent-react",
3
+ "version": "0.4.0",
4
+ "description": "Meshagent React Client",
5
+ "homepage": "https://www.meshagent.com",
6
+ "scripts": {
7
+ "build": "mkdir -p dist && ./scripts/build.sh"
8
+ },
9
+ "author": "Meshagent Software",
10
+ "license": "Apache-2.0",
11
+ "type": "module",
12
+ "module": "dist/esm/index.js",
13
+ "main": "./dist/esm/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "module": "./dist/esm/index.js",
18
+ "default": "./dist/cjs/index.js"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/**/test",
25
+ "LICENSE",
26
+ "README.md",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "devDependencies": {
30
+ "@types/react": "^19.1.8",
31
+ "@types/react-dom": "^19.1.6"
32
+ },
33
+ "dependencies": {
34
+ "@meshagent/meshagent": "^0.4.0",
35
+ "react-dom": "^19.1.0",
36
+ "typescript": "^5.8.3",
37
+ "uuid": "^11.1.0"
38
+ }
39
+ }