@fluxy-chat/sdk 0.1.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +843 -0
- package/dist/message-stream.d.ts +34 -0
- package/dist/message-stream.js +150 -0
- package/dist/room-connection.d.ts +63 -0
- package/dist/room-connection.js +244 -0
- package/package.json +57 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FluxyChatRoomConnection } from "./room-connection";
|
|
2
|
+
export interface FluxyMessageStreamOptions {
|
|
3
|
+
/** Min interval between delta frames (default 120ms). */
|
|
4
|
+
flushIntervalMs?: number;
|
|
5
|
+
parentId?: number | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Streams text into a single persisted room message (Worker `type: "stream"`).
|
|
9
|
+
* Throttles DB writes; clients receive `message` + `edit` events with `streaming: true|false`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FluxyMessageStream {
|
|
12
|
+
private readonly connection;
|
|
13
|
+
private readonly agentId;
|
|
14
|
+
private readonly flushIntervalMs;
|
|
15
|
+
private readonly parentId;
|
|
16
|
+
private buffer;
|
|
17
|
+
private messageId;
|
|
18
|
+
private closed;
|
|
19
|
+
private started;
|
|
20
|
+
private flushTimer;
|
|
21
|
+
private lastFlushMs;
|
|
22
|
+
private captureBound;
|
|
23
|
+
constructor(connection: FluxyChatRoomConnection, agentId: string, options?: FluxyMessageStreamOptions);
|
|
24
|
+
get activeMessageId(): number | null;
|
|
25
|
+
push(chunk: string): void;
|
|
26
|
+
end(): void;
|
|
27
|
+
abort(): void;
|
|
28
|
+
private bindCapture;
|
|
29
|
+
private scheduleFlush;
|
|
30
|
+
private flush;
|
|
31
|
+
private resolveMessageId;
|
|
32
|
+
private clearFlushTimer;
|
|
33
|
+
private assertOpen;
|
|
34
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { FluxySendError } from "./errors";
|
|
2
|
+
const DEFAULT_FLUSH_MS = 120;
|
|
3
|
+
/**
|
|
4
|
+
* Streams text into a single persisted room message (Worker `type: "stream"`).
|
|
5
|
+
* Throttles DB writes; clients receive `message` + `edit` events with `streaming: true|false`.
|
|
6
|
+
*/
|
|
7
|
+
export class FluxyMessageStream {
|
|
8
|
+
constructor(connection, agentId, options = {}) {
|
|
9
|
+
this.buffer = "";
|
|
10
|
+
this.messageId = null;
|
|
11
|
+
this.closed = false;
|
|
12
|
+
this.started = false;
|
|
13
|
+
this.flushTimer = null;
|
|
14
|
+
this.lastFlushMs = 0;
|
|
15
|
+
this.captureBound = false;
|
|
16
|
+
this.connection = connection;
|
|
17
|
+
this.agentId = agentId;
|
|
18
|
+
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_MS;
|
|
19
|
+
this.parentId = options.parentId ?? null;
|
|
20
|
+
}
|
|
21
|
+
get activeMessageId() {
|
|
22
|
+
return this.messageId;
|
|
23
|
+
}
|
|
24
|
+
push(chunk) {
|
|
25
|
+
this.assertOpen("push");
|
|
26
|
+
if (!chunk)
|
|
27
|
+
return;
|
|
28
|
+
this.bindCapture();
|
|
29
|
+
this.buffer += chunk;
|
|
30
|
+
if (!this.started) {
|
|
31
|
+
this.started = true;
|
|
32
|
+
this.connection.sendJson({
|
|
33
|
+
type: "stream",
|
|
34
|
+
op: "start",
|
|
35
|
+
userId: this.agentId,
|
|
36
|
+
content: this.buffer,
|
|
37
|
+
parentId: this.parentId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
this.scheduleFlush();
|
|
41
|
+
}
|
|
42
|
+
end() {
|
|
43
|
+
this.assertOpen("end");
|
|
44
|
+
this.closed = true;
|
|
45
|
+
this.clearFlushTimer();
|
|
46
|
+
if (!this.started)
|
|
47
|
+
return;
|
|
48
|
+
void this.flush(true);
|
|
49
|
+
}
|
|
50
|
+
abort() {
|
|
51
|
+
if (this.closed)
|
|
52
|
+
return;
|
|
53
|
+
this.closed = true;
|
|
54
|
+
this.clearFlushTimer();
|
|
55
|
+
if (!this.messageId)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
this.connection.sendJson({
|
|
59
|
+
type: "stream",
|
|
60
|
+
op: "abort",
|
|
61
|
+
userId: this.agentId,
|
|
62
|
+
messageId: this.messageId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* socket closed */
|
|
67
|
+
}
|
|
68
|
+
this.messageId = null;
|
|
69
|
+
this.buffer = "";
|
|
70
|
+
}
|
|
71
|
+
bindCapture() {
|
|
72
|
+
if (this.captureBound)
|
|
73
|
+
return;
|
|
74
|
+
this.captureBound = true;
|
|
75
|
+
this.connection.addEventListener("message", (event) => {
|
|
76
|
+
if (this.messageId)
|
|
77
|
+
return;
|
|
78
|
+
if (event.type === "stream" && event.op === "started") {
|
|
79
|
+
this.messageId = event.id;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.type === "message" &&
|
|
83
|
+
event.streaming &&
|
|
84
|
+
event.userId === this.agentId &&
|
|
85
|
+
Number.isFinite(event.id)) {
|
|
86
|
+
this.messageId = event.id;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
scheduleFlush() {
|
|
91
|
+
if (this.closed)
|
|
92
|
+
return;
|
|
93
|
+
const elapsed = Date.now() - this.lastFlushMs;
|
|
94
|
+
if (elapsed >= this.flushIntervalMs) {
|
|
95
|
+
void this.flush(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (this.flushTimer)
|
|
99
|
+
return;
|
|
100
|
+
this.flushTimer = setTimeout(() => {
|
|
101
|
+
this.flushTimer = null;
|
|
102
|
+
void this.flush(false);
|
|
103
|
+
}, this.flushIntervalMs - elapsed);
|
|
104
|
+
}
|
|
105
|
+
async flush(isFinal) {
|
|
106
|
+
if (!this.started)
|
|
107
|
+
return;
|
|
108
|
+
const id = await this.resolveMessageId(isFinal);
|
|
109
|
+
if (!id)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
this.connection.sendJson({
|
|
113
|
+
type: "stream",
|
|
114
|
+
op: isFinal ? "end" : "delta",
|
|
115
|
+
userId: this.agentId,
|
|
116
|
+
messageId: id,
|
|
117
|
+
content: this.buffer,
|
|
118
|
+
});
|
|
119
|
+
this.lastFlushMs = Date.now();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* socket closed */
|
|
123
|
+
}
|
|
124
|
+
if (isFinal) {
|
|
125
|
+
this.buffer = "";
|
|
126
|
+
this.messageId = null;
|
|
127
|
+
this.started = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async resolveMessageId(isFinal) {
|
|
131
|
+
if (this.messageId)
|
|
132
|
+
return this.messageId;
|
|
133
|
+
const deadline = Date.now() + (isFinal ? 3000 : 500);
|
|
134
|
+
while (!this.messageId && Date.now() < deadline) {
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
136
|
+
}
|
|
137
|
+
return this.messageId;
|
|
138
|
+
}
|
|
139
|
+
clearFlushTimer() {
|
|
140
|
+
if (this.flushTimer) {
|
|
141
|
+
clearTimeout(this.flushTimer);
|
|
142
|
+
this.flushTimer = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
assertOpen(op) {
|
|
146
|
+
if (this.closed) {
|
|
147
|
+
throw new FluxySendError(`Cannot call ${op}() on a closed FluxyMessageStream.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { FluxyChatClient, FluxyChatEvent, FluxyChatMessage } from "./index";
|
|
2
|
+
import { FluxyAuthError } from "./errors";
|
|
3
|
+
export type FluxyRoomConnectionStatus = "idle" | "connecting" | "connected" | "reconnecting" | "disconnected";
|
|
4
|
+
export interface FluxyRoomConnectionOptions {
|
|
5
|
+
/** Max reconnect tries before staying disconnected (default 8). */
|
|
6
|
+
maxReconnectAttempts?: number;
|
|
7
|
+
/** First backoff step in ms (default 500). */
|
|
8
|
+
baseBackoffMs?: number;
|
|
9
|
+
/** Backoff cap in ms (default 20_000). */
|
|
10
|
+
maxBackoffMs?: number;
|
|
11
|
+
/** Refetch REST history after each successful reconnect (default true). */
|
|
12
|
+
replayHistoryOnReconnect?: boolean;
|
|
13
|
+
historyLimit?: number;
|
|
14
|
+
onAuthError?: (error: FluxyAuthError) => void;
|
|
15
|
+
onConnectionError?: (error: Error) => void;
|
|
16
|
+
onStatusChange?: (status: FluxyRoomConnectionStatus) => void;
|
|
17
|
+
/** Called when max reconnect attempts are exhausted (not on auth failure). */
|
|
18
|
+
onReconnectFailed?: () => void;
|
|
19
|
+
}
|
|
20
|
+
type MessageListener = (event: FluxyChatEvent) => void;
|
|
21
|
+
export interface FluxyWaitForOptions {
|
|
22
|
+
timeout?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class FluxyChatRoomConnection {
|
|
25
|
+
private readonly client;
|
|
26
|
+
private readonly roomId;
|
|
27
|
+
private readonly options;
|
|
28
|
+
private ws;
|
|
29
|
+
private status;
|
|
30
|
+
private reconnectAttempt;
|
|
31
|
+
private reconnectTimer;
|
|
32
|
+
private intentionallyClosed;
|
|
33
|
+
private hasConnectedOnce;
|
|
34
|
+
private pendingHistoryReplay;
|
|
35
|
+
private lastError;
|
|
36
|
+
private listeners;
|
|
37
|
+
private waitForEntries;
|
|
38
|
+
private seenIds;
|
|
39
|
+
private seenIdsSet;
|
|
40
|
+
constructor(client: FluxyChatClient, roomId: string, options?: FluxyRoomConnectionOptions);
|
|
41
|
+
get connectionStatus(): FluxyRoomConnectionStatus;
|
|
42
|
+
get reconnectAttempts(): number;
|
|
43
|
+
getLastError(): Error | null;
|
|
44
|
+
get readyState(): number;
|
|
45
|
+
addEventListener(_type: "message", listener: MessageListener): void;
|
|
46
|
+
removeEventListener(_type: "message", listener: MessageListener): void;
|
|
47
|
+
connect(): void;
|
|
48
|
+
close(code?: number): void;
|
|
49
|
+
sendJson(payload: Record<string, unknown>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Resolves when an incoming event matches `predicate` (typically a `message` event).
|
|
52
|
+
*/
|
|
53
|
+
waitFor(predicate: (event: FluxyChatEvent) => boolean, options?: FluxyWaitForOptions): Promise<FluxyChatMessage>;
|
|
54
|
+
private rejectAllWaitFor;
|
|
55
|
+
private setStatus;
|
|
56
|
+
private clearReconnectTimer;
|
|
57
|
+
private openSocket;
|
|
58
|
+
private scheduleReconnect;
|
|
59
|
+
private replayHistory;
|
|
60
|
+
private trackMessageId;
|
|
61
|
+
private deliver;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { FluxyAuthError, FluxyConnectionError, FluxySendError, FluxyTimeoutError, FLUXY_WS_CLOSE_NORMAL, computeReconnectBackoffMs, mapWebSocketCloseToError, } from "./errors";
|
|
2
|
+
const SEEN_IDS_MAX = 10000;
|
|
3
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30000;
|
|
4
|
+
export class FluxyChatRoomConnection {
|
|
5
|
+
constructor(client, roomId, options = {}) {
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.status = "idle";
|
|
8
|
+
this.reconnectAttempt = 0;
|
|
9
|
+
this.reconnectTimer = null;
|
|
10
|
+
this.intentionallyClosed = false;
|
|
11
|
+
this.hasConnectedOnce = false;
|
|
12
|
+
this.pendingHistoryReplay = false;
|
|
13
|
+
this.lastError = null;
|
|
14
|
+
this.listeners = [];
|
|
15
|
+
this.waitForEntries = [];
|
|
16
|
+
this.seenIds = [];
|
|
17
|
+
this.seenIdsSet = new Set();
|
|
18
|
+
this.client = client;
|
|
19
|
+
this.roomId = roomId;
|
|
20
|
+
this.options = {
|
|
21
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 8,
|
|
22
|
+
baseBackoffMs: options.baseBackoffMs ?? 500,
|
|
23
|
+
maxBackoffMs: options.maxBackoffMs ?? 20000,
|
|
24
|
+
replayHistoryOnReconnect: options.replayHistoryOnReconnect ?? true,
|
|
25
|
+
historyLimit: options.historyLimit ?? 50,
|
|
26
|
+
...options,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
get connectionStatus() {
|
|
30
|
+
return this.status;
|
|
31
|
+
}
|
|
32
|
+
get reconnectAttempts() {
|
|
33
|
+
return this.reconnectAttempt;
|
|
34
|
+
}
|
|
35
|
+
getLastError() {
|
|
36
|
+
return this.lastError;
|
|
37
|
+
}
|
|
38
|
+
get readyState() {
|
|
39
|
+
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
40
|
+
}
|
|
41
|
+
addEventListener(_type, listener) {
|
|
42
|
+
this.listeners.push(listener);
|
|
43
|
+
}
|
|
44
|
+
removeEventListener(_type, listener) {
|
|
45
|
+
this.listeners = this.listeners.filter((cb) => cb !== listener);
|
|
46
|
+
}
|
|
47
|
+
connect() {
|
|
48
|
+
this.intentionallyClosed = false;
|
|
49
|
+
this.openSocket();
|
|
50
|
+
}
|
|
51
|
+
close(code = FLUXY_WS_CLOSE_NORMAL) {
|
|
52
|
+
this.intentionallyClosed = true;
|
|
53
|
+
this.rejectAllWaitFor(new FluxySendError("Connection closed."));
|
|
54
|
+
this.clearReconnectTimer();
|
|
55
|
+
if (this.ws) {
|
|
56
|
+
try {
|
|
57
|
+
this.ws.close(code);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
this.ws = null;
|
|
63
|
+
}
|
|
64
|
+
this.setStatus("disconnected");
|
|
65
|
+
}
|
|
66
|
+
sendJson(payload) {
|
|
67
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
68
|
+
throw new FluxySendError();
|
|
69
|
+
}
|
|
70
|
+
this.ws.send(JSON.stringify(payload));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolves when an incoming event matches `predicate` (typically a `message` event).
|
|
74
|
+
*/
|
|
75
|
+
waitFor(predicate, options = {}) {
|
|
76
|
+
const timeoutMs = options.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
77
|
+
if (this.status !== "connected") {
|
|
78
|
+
return Promise.reject(new FluxySendError("waitFor requires an open connection. Call connect() and wait until connected."));
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
this.waitForEntries = this.waitForEntries.filter((entry) => entry.timer !== timer);
|
|
83
|
+
reject(new FluxyTimeoutError(timeoutMs));
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
this.waitForEntries.push({
|
|
86
|
+
predicate,
|
|
87
|
+
resolve,
|
|
88
|
+
reject,
|
|
89
|
+
timer,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
rejectAllWaitFor(error) {
|
|
94
|
+
const entries = [...this.waitForEntries];
|
|
95
|
+
this.waitForEntries = [];
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
clearTimeout(entry.timer);
|
|
98
|
+
entry.reject(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
setStatus(next) {
|
|
102
|
+
if (this.status === next)
|
|
103
|
+
return;
|
|
104
|
+
this.status = next;
|
|
105
|
+
this.options.onStatusChange?.(next);
|
|
106
|
+
}
|
|
107
|
+
clearReconnectTimer() {
|
|
108
|
+
if (this.reconnectTimer) {
|
|
109
|
+
clearTimeout(this.reconnectTimer);
|
|
110
|
+
this.reconnectTimer = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
openSocket() {
|
|
114
|
+
this.clearReconnectTimer();
|
|
115
|
+
this.setStatus(this.hasConnectedOnce && this.reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
116
|
+
const ws = this.client.connect(this.roomId);
|
|
117
|
+
this.ws = ws;
|
|
118
|
+
ws.addEventListener("open", () => {
|
|
119
|
+
this.hasConnectedOnce = true;
|
|
120
|
+
this.reconnectAttempt = 0;
|
|
121
|
+
this.lastError = null;
|
|
122
|
+
this.setStatus("connected");
|
|
123
|
+
if (this.pendingHistoryReplay && this.options.replayHistoryOnReconnect) {
|
|
124
|
+
void this.replayHistory();
|
|
125
|
+
}
|
|
126
|
+
this.pendingHistoryReplay = false;
|
|
127
|
+
});
|
|
128
|
+
ws.addEventListener("message", (event) => {
|
|
129
|
+
let data;
|
|
130
|
+
try {
|
|
131
|
+
data = JSON.parse(String(event.data));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (data.type === "error") {
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.error("[fluxychat] worker error:", data.message);
|
|
139
|
+
}
|
|
140
|
+
this.deliver(data);
|
|
141
|
+
});
|
|
142
|
+
ws.addEventListener("close", (event) => {
|
|
143
|
+
this.ws = null;
|
|
144
|
+
if (this.intentionallyClosed) {
|
|
145
|
+
this.setStatus("disconnected");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const mapped = mapWebSocketCloseToError(event.code, event.reason || "");
|
|
149
|
+
if (mapped instanceof FluxyAuthError) {
|
|
150
|
+
this.lastError = mapped;
|
|
151
|
+
this.options.onAuthError?.(mapped);
|
|
152
|
+
this.options.onConnectionError?.(mapped);
|
|
153
|
+
this.rejectAllWaitFor(mapped);
|
|
154
|
+
this.setStatus("disconnected");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (mapped) {
|
|
158
|
+
this.lastError = mapped;
|
|
159
|
+
this.options.onConnectionError?.(mapped);
|
|
160
|
+
}
|
|
161
|
+
this.scheduleReconnect();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
scheduleReconnect() {
|
|
165
|
+
this.pendingHistoryReplay = true;
|
|
166
|
+
this.reconnectAttempt += 1;
|
|
167
|
+
if (this.reconnectAttempt > this.options.maxReconnectAttempts) {
|
|
168
|
+
this.setStatus("disconnected");
|
|
169
|
+
this.rejectAllWaitFor(new FluxyConnectionError(0, "reconnect_failed", "WebSocket reconnect attempts exhausted."));
|
|
170
|
+
this.options.onReconnectFailed?.();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.setStatus("reconnecting");
|
|
174
|
+
const delay = computeReconnectBackoffMs(this.reconnectAttempt, this.options.baseBackoffMs, this.options.maxBackoffMs);
|
|
175
|
+
this.clearReconnectTimer();
|
|
176
|
+
this.reconnectTimer = setTimeout(() => {
|
|
177
|
+
this.reconnectTimer = null;
|
|
178
|
+
if (!this.intentionallyClosed) {
|
|
179
|
+
this.openSocket();
|
|
180
|
+
}
|
|
181
|
+
}, delay);
|
|
182
|
+
}
|
|
183
|
+
async replayHistory() {
|
|
184
|
+
try {
|
|
185
|
+
const messages = await this.client.fetchMessages(this.roomId, this.options.historyLimit);
|
|
186
|
+
this.deliver({ type: "history", messages });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* history replay is best-effort */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
trackMessageId(id) {
|
|
193
|
+
if (!Number.isFinite(id) || this.seenIdsSet.has(id))
|
|
194
|
+
return;
|
|
195
|
+
if (this.seenIds.length >= SEEN_IDS_MAX) {
|
|
196
|
+
const evicted = this.seenIds.shift();
|
|
197
|
+
if (evicted !== undefined)
|
|
198
|
+
this.seenIdsSet.delete(evicted);
|
|
199
|
+
}
|
|
200
|
+
this.seenIds.push(id);
|
|
201
|
+
this.seenIdsSet.add(id);
|
|
202
|
+
}
|
|
203
|
+
deliver(event) {
|
|
204
|
+
if (event.type === "history") {
|
|
205
|
+
this.seenIds = [];
|
|
206
|
+
this.seenIdsSet.clear();
|
|
207
|
+
for (const msg of event.messages) {
|
|
208
|
+
if (Number.isFinite(msg.id))
|
|
209
|
+
this.trackMessageId(msg.id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (event.type === "message" && Number.isFinite(event.id)) {
|
|
213
|
+
if (this.seenIdsSet.has(event.id) && !event.streaming)
|
|
214
|
+
return;
|
|
215
|
+
this.trackMessageId(event.id);
|
|
216
|
+
}
|
|
217
|
+
const satisfied = [];
|
|
218
|
+
for (const entry of this.waitForEntries) {
|
|
219
|
+
if (entry.predicate(event)) {
|
|
220
|
+
satisfied.push(entry);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (satisfied.length > 0) {
|
|
224
|
+
this.waitForEntries = this.waitForEntries.filter((entry) => !satisfied.includes(entry));
|
|
225
|
+
for (const entry of satisfied) {
|
|
226
|
+
clearTimeout(entry.timer);
|
|
227
|
+
if (event.type === "message") {
|
|
228
|
+
entry.resolve(event);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
entry.reject(new Error(`waitFor matched non-message event type "${event.type}"`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const listener of this.listeners) {
|
|
236
|
+
try {
|
|
237
|
+
listener(event);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
/* listener errors must not break the connection */
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxy-chat/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fluxychat JavaScript/TypeScript SDK — WebSocket rooms, REST messages, useChat hook",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Fluxychat",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/AlessandroFare/fluxychat.git",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/AlessandroFare/fluxychat/tree/main/packages/sdk#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/AlessandroFare/fluxychat/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"chat",
|
|
18
|
+
"websocket",
|
|
19
|
+
"realtime",
|
|
20
|
+
"cloudflare",
|
|
21
|
+
"fluxychat"
|
|
22
|
+
],
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/index.js",
|
|
28
|
+
"dist/index.d.ts",
|
|
29
|
+
"dist/room-connection.js",
|
|
30
|
+
"dist/room-connection.d.ts",
|
|
31
|
+
"dist/errors.js",
|
|
32
|
+
"dist/errors.d.ts",
|
|
33
|
+
"dist/message-stream.js",
|
|
34
|
+
"dist/message-stream.d.ts",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "node node_modules/typescript/bin/tsc -p tsconfig.json",
|
|
43
|
+
"prepublishOnly": "pnpm run build",
|
|
44
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
45
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
46
|
+
"test": "pnpm exec vitest run"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^24.3.0",
|
|
53
|
+
"@types/react": "^19.2.14",
|
|
54
|
+
"typescript": "latest",
|
|
55
|
+
"vitest": "^4.1.5"
|
|
56
|
+
}
|
|
57
|
+
}
|