vg-x07df 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/index.cjs +915 -0
- package/dist/channel/index.cjs.map +1 -0
- package/dist/channel/index.d.cts +144 -0
- package/dist/channel/index.d.ts +144 -0
- package/dist/channel/index.mjs +903 -0
- package/dist/channel/index.mjs.map +1 -0
- package/dist/index.cjs +2232 -1832
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +373 -145
- package/dist/index.d.ts +373 -145
- package/dist/index.mjs +2220 -1835
- package/dist/index.mjs.map +1 -1
- package/dist/types-CLweuTEn.d.cts +63 -0
- package/dist/types-CLweuTEn.d.ts +63 -0
- package/dist/utils/index.cjs +151 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +39 -0
- package/dist/utils/index.d.ts +39 -0
- package/dist/utils/index.mjs +147 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +13 -1
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var livekitClient = require('livekit-client');
|
|
5
|
+
var zustand = require('zustand');
|
|
6
|
+
var immer = require('zustand/middleware/immer');
|
|
7
|
+
var nanoid = require('nanoid');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
// src/channel/DataChannelProvider.tsx
|
|
11
|
+
var DataChannelContext = react.createContext(null);
|
|
12
|
+
function useDataChannelContext() {
|
|
13
|
+
const context = react.useContext(DataChannelContext);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error("useDataChannelContext must be used within DataChannelProvider");
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
function useFeatureService(featureName) {
|
|
20
|
+
const { services } = useDataChannelContext();
|
|
21
|
+
const service = services.get(featureName);
|
|
22
|
+
if (!service) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Feature service "${featureName}" not found. Make sure it's enabled in DataChannelProvider.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return service;
|
|
28
|
+
}
|
|
29
|
+
var defaultChatState = {
|
|
30
|
+
byId: {},
|
|
31
|
+
order: [],
|
|
32
|
+
pendingOps: {},
|
|
33
|
+
participantCache: {},
|
|
34
|
+
maxEntries: 1e3
|
|
35
|
+
};
|
|
36
|
+
function applyReactionToEntry(entry, emoji, participantId, op) {
|
|
37
|
+
const emojiSet = entry.reactions[emoji];
|
|
38
|
+
if (!emojiSet) {
|
|
39
|
+
entry.reactions[emoji] = /* @__PURE__ */ new Set();
|
|
40
|
+
}
|
|
41
|
+
if (op === "add") {
|
|
42
|
+
entry.reactions[emoji]?.add(participantId);
|
|
43
|
+
} else {
|
|
44
|
+
entry.reactions[emoji]?.delete(participantId);
|
|
45
|
+
if (entry.reactions[emoji]?.size === 0) {
|
|
46
|
+
delete entry.reactions[emoji];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function trimEntriesIfNeeded(state) {
|
|
51
|
+
if (Object.keys(state.byId).length > state.maxEntries) {
|
|
52
|
+
const entriesToRemove = Object.keys(state.byId).length - state.maxEntries;
|
|
53
|
+
for (let i = 0; i < entriesToRemove; i++) {
|
|
54
|
+
const oldestId = state.order[i];
|
|
55
|
+
if (oldestId) {
|
|
56
|
+
delete state.byId[oldestId];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
state.order = state.order.slice(entriesToRemove);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var useChatStore = zustand.create()(
|
|
63
|
+
immer.immer((set) => ({
|
|
64
|
+
...defaultChatState,
|
|
65
|
+
patch: (fn) => set((state) => {
|
|
66
|
+
fn(state);
|
|
67
|
+
}),
|
|
68
|
+
applyIncoming: (envelope) => set((state) => {
|
|
69
|
+
if (envelope.kind === "entry") {
|
|
70
|
+
if (state.byId[envelope.entryId]) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (envelope.sender.info) {
|
|
74
|
+
state.participantCache[envelope.sender.id] = envelope.sender.info;
|
|
75
|
+
}
|
|
76
|
+
const entry = {
|
|
77
|
+
id: envelope.entryId,
|
|
78
|
+
content: envelope.payload.content,
|
|
79
|
+
sender: {
|
|
80
|
+
id: envelope.sender.id
|
|
81
|
+
},
|
|
82
|
+
createdAt: envelope.ts,
|
|
83
|
+
version: 1,
|
|
84
|
+
reactions: {},
|
|
85
|
+
status: "sent"
|
|
86
|
+
};
|
|
87
|
+
state.byId[envelope.entryId] = entry;
|
|
88
|
+
state.order.push(envelope.entryId);
|
|
89
|
+
trimEntriesIfNeeded(state);
|
|
90
|
+
const pendingOps = state.pendingOps[envelope.entryId];
|
|
91
|
+
if (pendingOps) {
|
|
92
|
+
delete state.pendingOps[envelope.entryId];
|
|
93
|
+
for (const op of pendingOps) {
|
|
94
|
+
if (op.kind === "edit") {
|
|
95
|
+
const entry2 = state.byId[op.entryId];
|
|
96
|
+
if (entry2 && op.payload.version > entry2.version) {
|
|
97
|
+
entry2.content = op.payload.newContent;
|
|
98
|
+
entry2.version = op.payload.version;
|
|
99
|
+
entry2.editedAt = op.ts;
|
|
100
|
+
}
|
|
101
|
+
} else if (op.kind === "remove") {
|
|
102
|
+
const entry2 = state.byId[op.entryId];
|
|
103
|
+
if (entry2 && !entry2.removedAt) {
|
|
104
|
+
entry2.removedAt = op.ts;
|
|
105
|
+
}
|
|
106
|
+
} else if (op.kind === "reaction") {
|
|
107
|
+
if (op.sender.info) {
|
|
108
|
+
state.participantCache[op.sender.id] = op.sender.info;
|
|
109
|
+
}
|
|
110
|
+
const entry2 = state.byId[op.entryId];
|
|
111
|
+
if (entry2) {
|
|
112
|
+
applyReactionToEntry(
|
|
113
|
+
entry2,
|
|
114
|
+
op.payload.emoji,
|
|
115
|
+
op.sender.id,
|
|
116
|
+
op.payload.op
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (envelope.kind === "edit") {
|
|
123
|
+
const entry = state.byId[envelope.entryId];
|
|
124
|
+
if (entry) {
|
|
125
|
+
if (envelope.payload.version > entry.version) {
|
|
126
|
+
entry.content = envelope.payload.newContent;
|
|
127
|
+
entry.version = envelope.payload.version;
|
|
128
|
+
entry.editedAt = envelope.ts;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
if (!state.pendingOps[envelope.entryId]) {
|
|
132
|
+
state.pendingOps[envelope.entryId] = [];
|
|
133
|
+
}
|
|
134
|
+
state.pendingOps[envelope.entryId]?.push(envelope);
|
|
135
|
+
}
|
|
136
|
+
} else if (envelope.kind === "remove") {
|
|
137
|
+
const entry = state.byId[envelope.entryId];
|
|
138
|
+
if (entry) {
|
|
139
|
+
if (!entry.removedAt) {
|
|
140
|
+
entry.removedAt = envelope.ts;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (!state.pendingOps[envelope.entryId]) {
|
|
144
|
+
state.pendingOps[envelope.entryId] = [];
|
|
145
|
+
}
|
|
146
|
+
state.pendingOps[envelope.entryId]?.push(envelope);
|
|
147
|
+
}
|
|
148
|
+
} else if (envelope.kind === "reaction") {
|
|
149
|
+
if (envelope.sender.info) {
|
|
150
|
+
state.participantCache[envelope.sender.id] = envelope.sender.info;
|
|
151
|
+
}
|
|
152
|
+
const entry = state.byId[envelope.entryId];
|
|
153
|
+
if (entry) {
|
|
154
|
+
applyReactionToEntry(
|
|
155
|
+
entry,
|
|
156
|
+
envelope.payload.emoji,
|
|
157
|
+
envelope.sender.id,
|
|
158
|
+
envelope.payload.op
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
if (!state.pendingOps[envelope.entryId]) {
|
|
162
|
+
state.pendingOps[envelope.entryId] = [];
|
|
163
|
+
}
|
|
164
|
+
state.pendingOps[envelope.entryId]?.push(envelope);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
addEntryOptimistic: (entry) => set((state) => {
|
|
169
|
+
state.byId[entry.id] = entry;
|
|
170
|
+
state.order.push(entry.id);
|
|
171
|
+
trimEntriesIfNeeded(state);
|
|
172
|
+
}),
|
|
173
|
+
markEntrySent: (entryId) => set((state) => {
|
|
174
|
+
const entry = state.byId[entryId];
|
|
175
|
+
if (entry) {
|
|
176
|
+
entry.status = "sent";
|
|
177
|
+
}
|
|
178
|
+
}),
|
|
179
|
+
markEntryFailed: (entryId) => set((state) => {
|
|
180
|
+
const entry = state.byId[entryId];
|
|
181
|
+
if (entry) {
|
|
182
|
+
entry.status = "failed";
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
applyEdit: (entryId, newContent, version) => set((state) => {
|
|
186
|
+
const entry = state.byId[entryId];
|
|
187
|
+
if (entry && version > entry.version) {
|
|
188
|
+
entry.content = newContent;
|
|
189
|
+
entry.version = version;
|
|
190
|
+
entry.editedAt = Date.now();
|
|
191
|
+
}
|
|
192
|
+
}),
|
|
193
|
+
applyRemove: (entryId) => set((state) => {
|
|
194
|
+
const entry = state.byId[entryId];
|
|
195
|
+
if (entry && !entry.removedAt) {
|
|
196
|
+
entry.removedAt = Date.now();
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
applyReaction: (entryId, emoji, participantId, op) => set((state) => {
|
|
200
|
+
const entry = state.byId[entryId];
|
|
201
|
+
if (entry) {
|
|
202
|
+
applyReactionToEntry(entry, emoji, participantId, op);
|
|
203
|
+
}
|
|
204
|
+
}),
|
|
205
|
+
queuePendingOp: (entryId, envelope) => set((state) => {
|
|
206
|
+
if (!state.pendingOps[entryId]) {
|
|
207
|
+
state.pendingOps[entryId] = [];
|
|
208
|
+
}
|
|
209
|
+
state.pendingOps[entryId].push(envelope);
|
|
210
|
+
}),
|
|
211
|
+
processPendingOps: (entryId) => set((state) => {
|
|
212
|
+
const pendingOps = state.pendingOps[entryId];
|
|
213
|
+
if (!pendingOps) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
delete state.pendingOps[entryId];
|
|
217
|
+
for (const envelope of pendingOps) {
|
|
218
|
+
if (envelope.kind === "edit") {
|
|
219
|
+
const entry = state.byId[envelope.entryId];
|
|
220
|
+
if (entry && envelope.payload.version > entry.version) {
|
|
221
|
+
entry.content = envelope.payload.newContent;
|
|
222
|
+
entry.version = envelope.payload.version;
|
|
223
|
+
entry.editedAt = envelope.ts;
|
|
224
|
+
}
|
|
225
|
+
} else if (envelope.kind === "remove") {
|
|
226
|
+
const entry = state.byId[envelope.entryId];
|
|
227
|
+
if (entry && !entry.removedAt) {
|
|
228
|
+
entry.removedAt = envelope.ts;
|
|
229
|
+
}
|
|
230
|
+
} else if (envelope.kind === "reaction") {
|
|
231
|
+
if (envelope.sender.info) {
|
|
232
|
+
state.participantCache[envelope.sender.id] = envelope.sender.info;
|
|
233
|
+
}
|
|
234
|
+
const entry = state.byId[envelope.entryId];
|
|
235
|
+
if (entry) {
|
|
236
|
+
applyReactionToEntry(
|
|
237
|
+
entry,
|
|
238
|
+
envelope.payload.emoji,
|
|
239
|
+
envelope.sender.id,
|
|
240
|
+
envelope.payload.op
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}),
|
|
246
|
+
trimOldEntries: () => set((state) => {
|
|
247
|
+
trimEntriesIfNeeded(state);
|
|
248
|
+
}),
|
|
249
|
+
clearChat: () => set(() => defaultChatState)
|
|
250
|
+
}))
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// src/state/types.ts
|
|
254
|
+
var defaultState = {
|
|
255
|
+
initiated: false,
|
|
256
|
+
session: null,
|
|
257
|
+
incomingInvite: null,
|
|
258
|
+
outgoingInvites: {},
|
|
259
|
+
errors: []
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/state/store.ts
|
|
263
|
+
var useRtcStore = zustand.create()(
|
|
264
|
+
immer.immer((set) => ({
|
|
265
|
+
...defaultState,
|
|
266
|
+
patch: (fn) => set((state) => {
|
|
267
|
+
fn(state);
|
|
268
|
+
}),
|
|
269
|
+
reset: () => set(() => defaultState),
|
|
270
|
+
addError: (error) => set((state) => {
|
|
271
|
+
state.errors.push(error);
|
|
272
|
+
}),
|
|
273
|
+
clearErrors: () => set((state) => {
|
|
274
|
+
state.errors = [];
|
|
275
|
+
})
|
|
276
|
+
}))
|
|
277
|
+
);
|
|
278
|
+
function compareEntries(a, b) {
|
|
279
|
+
if (a.createdAt !== b.createdAt) {
|
|
280
|
+
return a.createdAt - b.createdAt;
|
|
281
|
+
}
|
|
282
|
+
if (a.sender.id !== b.sender.id) {
|
|
283
|
+
return a.sender.id.localeCompare(b.sender.id);
|
|
284
|
+
}
|
|
285
|
+
return a.id.localeCompare(b.id);
|
|
286
|
+
}
|
|
287
|
+
function validateContent(content) {
|
|
288
|
+
const trimmed = content.trim();
|
|
289
|
+
if (trimmed.length === 0) {
|
|
290
|
+
return { valid: false, error: "Content cannot be empty" };
|
|
291
|
+
}
|
|
292
|
+
const sizeInBytes = new TextEncoder().encode(content).length;
|
|
293
|
+
if (sizeInBytes > 16384) {
|
|
294
|
+
return { valid: false, error: "Content exceeds 16KB limit" };
|
|
295
|
+
}
|
|
296
|
+
return { valid: true };
|
|
297
|
+
}
|
|
298
|
+
function isValidEnvelope(data) {
|
|
299
|
+
if (!data || typeof data !== "object") {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (data.v !== 1) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (!["entry", "edit", "remove", "reaction"].includes(data.kind)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (typeof data.roomId !== "string" || typeof data.entryId !== "string" || typeof data.ts !== "number") {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
if (!data.sender || typeof data.sender.id !== "string") {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (data.kind === "entry" || data.kind === "edit" || data.kind === "reaction") {
|
|
315
|
+
if (!data.payload || typeof data.payload !== "object") {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
function generateEntryId() {
|
|
322
|
+
return nanoid.nanoid();
|
|
323
|
+
}
|
|
324
|
+
function getCurrentTimestamp() {
|
|
325
|
+
return Date.now();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/utils/logger.ts
|
|
329
|
+
var LOG_LEVELS = {
|
|
330
|
+
debug: 0,
|
|
331
|
+
info: 1,
|
|
332
|
+
warn: 2,
|
|
333
|
+
error: 3
|
|
334
|
+
};
|
|
335
|
+
var CallpadLoggerImpl = class _CallpadLoggerImpl {
|
|
336
|
+
constructor(namespace = "callpad", options = {}) {
|
|
337
|
+
this.namespace = namespace;
|
|
338
|
+
this.level = options.level ?? this.getDefaultLevel();
|
|
339
|
+
this.enableDebug = options.enableDebug ?? this.shouldEnableDebug();
|
|
340
|
+
if (options.customLogger) {
|
|
341
|
+
this.customLogger = options.customLogger;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
getDefaultLevel() {
|
|
345
|
+
const envLevel = typeof window !== "undefined" ? window.__CALLPAD_LOG_LEVEL__ : typeof globalThis !== "undefined" && globalThis.process?.env?.CALLPAD_LOG_LEVEL;
|
|
346
|
+
if (envLevel && this.isValidLogLevel(envLevel)) {
|
|
347
|
+
return envLevel;
|
|
348
|
+
}
|
|
349
|
+
const isProduction = typeof globalThis !== "undefined" && globalThis.process?.env?.NODE_ENV === "production";
|
|
350
|
+
return isProduction ? "warn" : "info";
|
|
351
|
+
}
|
|
352
|
+
shouldEnableDebug() {
|
|
353
|
+
const debugEnv = typeof window !== "undefined" ? window.__DEBUG__ : typeof globalThis !== "undefined" && globalThis.process?.env?.DEBUG;
|
|
354
|
+
if (!debugEnv) return false;
|
|
355
|
+
const debugPatterns = debugEnv.split(/[\s,]+/);
|
|
356
|
+
return debugPatterns.some((pattern) => {
|
|
357
|
+
if (pattern === "*") return true;
|
|
358
|
+
if (pattern.endsWith("*")) {
|
|
359
|
+
const prefix = pattern.slice(0, -1);
|
|
360
|
+
return this.namespace.startsWith(prefix);
|
|
361
|
+
}
|
|
362
|
+
return this.namespace === pattern;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
isValidLogLevel(level) {
|
|
366
|
+
return Object.keys(LOG_LEVELS).includes(level);
|
|
367
|
+
}
|
|
368
|
+
shouldLog(level) {
|
|
369
|
+
if (level === "debug" && !this.enableDebug) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
373
|
+
}
|
|
374
|
+
formatMessage(level, message, meta) {
|
|
375
|
+
if (!this.shouldLog(level)) return;
|
|
376
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
377
|
+
const prefix = `[${timestamp}] [${this.namespace}] [${level.toUpperCase()}]`;
|
|
378
|
+
if (this.customLogger) {
|
|
379
|
+
this.customLogger(level, message, meta);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const logMethod = level === "error" ? console.error : level === "warn" ? console.warn : level === "info" ? console.info : console.log;
|
|
383
|
+
if (meta !== void 0) {
|
|
384
|
+
logMethod(`${prefix} ${message}`, meta);
|
|
385
|
+
} else {
|
|
386
|
+
logMethod(`${prefix} ${message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
debug(message, meta) {
|
|
390
|
+
this.formatMessage("debug", message, meta);
|
|
391
|
+
}
|
|
392
|
+
info(message, meta) {
|
|
393
|
+
this.formatMessage("info", message, meta);
|
|
394
|
+
}
|
|
395
|
+
warn(message, meta) {
|
|
396
|
+
this.formatMessage("warn", message, meta);
|
|
397
|
+
}
|
|
398
|
+
error(message, meta) {
|
|
399
|
+
this.formatMessage("error", message, meta);
|
|
400
|
+
}
|
|
401
|
+
child(namespace) {
|
|
402
|
+
const childNamespace = `${this.namespace}:${namespace}`;
|
|
403
|
+
const childOptions = {
|
|
404
|
+
level: this.level,
|
|
405
|
+
enableDebug: this.enableDebug
|
|
406
|
+
};
|
|
407
|
+
if (this.customLogger) {
|
|
408
|
+
childOptions.customLogger = this.customLogger;
|
|
409
|
+
}
|
|
410
|
+
return new _CallpadLoggerImpl(childNamespace, childOptions);
|
|
411
|
+
}
|
|
412
|
+
setLevel(level) {
|
|
413
|
+
this.level = level;
|
|
414
|
+
}
|
|
415
|
+
isLevelEnabled(level) {
|
|
416
|
+
return this.shouldLog(level);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var rootLogger = null;
|
|
420
|
+
function createLogger(namespace, options) {
|
|
421
|
+
if (!namespace) {
|
|
422
|
+
if (!rootLogger) {
|
|
423
|
+
rootLogger = new CallpadLoggerImpl("callpad", options);
|
|
424
|
+
}
|
|
425
|
+
return rootLogger;
|
|
426
|
+
}
|
|
427
|
+
return new CallpadLoggerImpl(`callpad:${namespace}`, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/channel/chat/service.ts
|
|
431
|
+
var logger = createLogger("chat");
|
|
432
|
+
var ChatService = class {
|
|
433
|
+
constructor(room) {
|
|
434
|
+
this.isSubscribed = false;
|
|
435
|
+
this.room = room;
|
|
436
|
+
}
|
|
437
|
+
isRoomReady() {
|
|
438
|
+
if (!this.room) {
|
|
439
|
+
logger.warn("Room not initialized");
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
if (this.room.state !== livekitClient.ConnectionState.Connected) {
|
|
443
|
+
logger.warn("Room not connected", { state: this.room.state });
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (!this.room.localParticipant) {
|
|
447
|
+
logger.warn("Local participant not available");
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
subscribe() {
|
|
453
|
+
if (this.isSubscribed) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.room.registerTextStreamHandler(
|
|
457
|
+
"chat:v1",
|
|
458
|
+
async (reader, participantInfo) => {
|
|
459
|
+
try {
|
|
460
|
+
const text = await reader.readAll();
|
|
461
|
+
this.handleIncomingMessage(text);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
logger.error("Error reading text stream", error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
this.isSubscribed = true;
|
|
468
|
+
}
|
|
469
|
+
unsubscribe() {
|
|
470
|
+
this.isSubscribed = false;
|
|
471
|
+
}
|
|
472
|
+
getLocalParticipantId() {
|
|
473
|
+
return this.room.localParticipant.identity;
|
|
474
|
+
}
|
|
475
|
+
async send(content) {
|
|
476
|
+
if (!this.isRoomReady()) {
|
|
477
|
+
useRtcStore.getState().addError({
|
|
478
|
+
code: "CHAT_ROOM_NOT_READY",
|
|
479
|
+
message: "Cannot send message: room not connected",
|
|
480
|
+
timestamp: Date.now()
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const validation = validateContent(content);
|
|
485
|
+
if (!validation.valid) {
|
|
486
|
+
useRtcStore.getState().addError({
|
|
487
|
+
code: "CHAT_INVALID_CONTENT",
|
|
488
|
+
message: validation.error || "Invalid content",
|
|
489
|
+
timestamp: Date.now()
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const entryId = generateEntryId();
|
|
494
|
+
const chatStore = useChatStore.getState();
|
|
495
|
+
const senderInfo = this.getSenderInfo();
|
|
496
|
+
if (senderInfo.info) {
|
|
497
|
+
chatStore.patch((state) => {
|
|
498
|
+
state.participantCache[senderInfo.id] = senderInfo.info;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const entry = {
|
|
502
|
+
id: entryId,
|
|
503
|
+
content,
|
|
504
|
+
sender: {
|
|
505
|
+
id: senderInfo.id
|
|
506
|
+
},
|
|
507
|
+
createdAt: getCurrentTimestamp(),
|
|
508
|
+
version: 1,
|
|
509
|
+
reactions: {},
|
|
510
|
+
status: "sending"
|
|
511
|
+
};
|
|
512
|
+
chatStore.addEntryOptimistic(entry);
|
|
513
|
+
try {
|
|
514
|
+
const envelope = {
|
|
515
|
+
v: 1,
|
|
516
|
+
kind: "entry",
|
|
517
|
+
roomId: this.room.name,
|
|
518
|
+
entryId,
|
|
519
|
+
ts: entry.createdAt,
|
|
520
|
+
sender: senderInfo,
|
|
521
|
+
payload: {
|
|
522
|
+
content
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
526
|
+
topic: "chat:v1"
|
|
527
|
+
});
|
|
528
|
+
chatStore.markEntrySent(entryId);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
chatStore.markEntryFailed(entryId);
|
|
531
|
+
useRtcStore.getState().addError({
|
|
532
|
+
code: "CHAT_SEND_FAILED",
|
|
533
|
+
message: error instanceof Error ? error.message : "Failed to send message",
|
|
534
|
+
timestamp: Date.now(),
|
|
535
|
+
context: { entryId }
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async edit(entryId, newContent) {
|
|
540
|
+
if (!this.isRoomReady()) {
|
|
541
|
+
useRtcStore.getState().addError({
|
|
542
|
+
code: "CHAT_ROOM_NOT_READY",
|
|
543
|
+
message: "Cannot edit message: room not connected",
|
|
544
|
+
timestamp: Date.now()
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const validation = validateContent(newContent);
|
|
549
|
+
if (!validation.valid) {
|
|
550
|
+
useRtcStore.getState().addError({
|
|
551
|
+
code: "CHAT_INVALID_CONTENT",
|
|
552
|
+
message: validation.error || "Invalid content",
|
|
553
|
+
timestamp: Date.now()
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const chatStore = useChatStore.getState();
|
|
558
|
+
const entry = chatStore.byId[entryId];
|
|
559
|
+
if (!entry) {
|
|
560
|
+
useRtcStore.getState().addError({
|
|
561
|
+
code: "CHAT_ENTRY_NOT_FOUND",
|
|
562
|
+
message: "Entry not found",
|
|
563
|
+
timestamp: Date.now(),
|
|
564
|
+
context: { entryId }
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const senderInfo = this.getSenderInfo();
|
|
569
|
+
if (entry.sender.id !== senderInfo.id) {
|
|
570
|
+
useRtcStore.getState().addError({
|
|
571
|
+
code: "CHAT_UNAUTHORIZED",
|
|
572
|
+
message: "You can only edit your own messages",
|
|
573
|
+
timestamp: Date.now(),
|
|
574
|
+
context: { entryId }
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const newVersion = entry.version + 1;
|
|
579
|
+
chatStore.applyEdit(entryId, newContent, newVersion);
|
|
580
|
+
try {
|
|
581
|
+
const envelope = {
|
|
582
|
+
v: 1,
|
|
583
|
+
kind: "edit",
|
|
584
|
+
roomId: this.room.name,
|
|
585
|
+
entryId,
|
|
586
|
+
ts: getCurrentTimestamp(),
|
|
587
|
+
sender: senderInfo,
|
|
588
|
+
payload: {
|
|
589
|
+
newContent,
|
|
590
|
+
version: newVersion
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
594
|
+
topic: "chat:v1"
|
|
595
|
+
});
|
|
596
|
+
} catch (error) {
|
|
597
|
+
useRtcStore.getState().addError({
|
|
598
|
+
code: "CHAT_EDIT_FAILED",
|
|
599
|
+
message: error instanceof Error ? error.message : "Failed to edit message",
|
|
600
|
+
timestamp: Date.now(),
|
|
601
|
+
context: { entryId }
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async remove(entryId) {
|
|
606
|
+
if (!this.isRoomReady()) {
|
|
607
|
+
useRtcStore.getState().addError({
|
|
608
|
+
code: "CHAT_ROOM_NOT_READY",
|
|
609
|
+
message: "Cannot remove message: room not connected",
|
|
610
|
+
timestamp: Date.now()
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const chatStore = useChatStore.getState();
|
|
615
|
+
const entry = chatStore.byId[entryId];
|
|
616
|
+
if (!entry) {
|
|
617
|
+
useRtcStore.getState().addError({
|
|
618
|
+
code: "CHAT_ENTRY_NOT_FOUND",
|
|
619
|
+
message: "Entry not found",
|
|
620
|
+
timestamp: Date.now(),
|
|
621
|
+
context: { entryId }
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const senderInfo = this.getSenderInfo();
|
|
626
|
+
if (entry.sender.id !== senderInfo.id) {
|
|
627
|
+
useRtcStore.getState().addError({
|
|
628
|
+
code: "CHAT_UNAUTHORIZED",
|
|
629
|
+
message: "You can only remove your own messages",
|
|
630
|
+
timestamp: Date.now(),
|
|
631
|
+
context: { entryId }
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
chatStore.applyRemove(entryId);
|
|
636
|
+
try {
|
|
637
|
+
const envelope = {
|
|
638
|
+
v: 1,
|
|
639
|
+
kind: "remove",
|
|
640
|
+
roomId: this.room.name,
|
|
641
|
+
entryId,
|
|
642
|
+
ts: getCurrentTimestamp(),
|
|
643
|
+
sender: senderInfo
|
|
644
|
+
};
|
|
645
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
646
|
+
topic: "chat:v1"
|
|
647
|
+
});
|
|
648
|
+
} catch (error) {
|
|
649
|
+
useRtcStore.getState().addError({
|
|
650
|
+
code: "CHAT_REMOVE_FAILED",
|
|
651
|
+
message: error instanceof Error ? error.message : "Failed to remove message",
|
|
652
|
+
timestamp: Date.now(),
|
|
653
|
+
context: { entryId }
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async react(entryId, emoji) {
|
|
658
|
+
if (!this.isRoomReady()) {
|
|
659
|
+
useRtcStore.getState().addError({
|
|
660
|
+
code: "CHAT_ROOM_NOT_READY",
|
|
661
|
+
message: "Cannot add reaction: room not connected",
|
|
662
|
+
timestamp: Date.now()
|
|
663
|
+
});
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const chatStore = useChatStore.getState();
|
|
667
|
+
const entry = chatStore.byId[entryId];
|
|
668
|
+
if (!entry) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const senderInfo = this.getSenderInfo();
|
|
672
|
+
if (senderInfo.info) {
|
|
673
|
+
chatStore.patch((state) => {
|
|
674
|
+
state.participantCache[senderInfo.id] = senderInfo.info;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
chatStore.applyReaction(entryId, emoji, senderInfo.id, "add");
|
|
678
|
+
try {
|
|
679
|
+
const envelope = {
|
|
680
|
+
v: 1,
|
|
681
|
+
kind: "reaction",
|
|
682
|
+
roomId: this.room.name,
|
|
683
|
+
entryId,
|
|
684
|
+
ts: getCurrentTimestamp(),
|
|
685
|
+
sender: senderInfo,
|
|
686
|
+
payload: {
|
|
687
|
+
emoji,
|
|
688
|
+
op: "add"
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
692
|
+
topic: "chat:v1"
|
|
693
|
+
});
|
|
694
|
+
} catch (error) {
|
|
695
|
+
useRtcStore.getState().addError({
|
|
696
|
+
code: "CHAT_REACTION_FAILED",
|
|
697
|
+
message: error instanceof Error ? error.message : "Failed to add reaction",
|
|
698
|
+
timestamp: Date.now(),
|
|
699
|
+
context: { entryId, emoji }
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async unreact(entryId, emoji) {
|
|
704
|
+
if (!this.isRoomReady()) {
|
|
705
|
+
useRtcStore.getState().addError({
|
|
706
|
+
code: "CHAT_ROOM_NOT_READY",
|
|
707
|
+
message: "Cannot remove reaction: room not connected",
|
|
708
|
+
timestamp: Date.now()
|
|
709
|
+
});
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const chatStore = useChatStore.getState();
|
|
713
|
+
const entry = chatStore.byId[entryId];
|
|
714
|
+
if (!entry) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const senderInfo = this.getSenderInfo();
|
|
718
|
+
if (senderInfo.info) {
|
|
719
|
+
chatStore.patch((state) => {
|
|
720
|
+
state.participantCache[senderInfo.id] = senderInfo.info;
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
chatStore.applyReaction(entryId, emoji, senderInfo.id, "remove");
|
|
724
|
+
try {
|
|
725
|
+
const envelope = {
|
|
726
|
+
v: 1,
|
|
727
|
+
kind: "reaction",
|
|
728
|
+
roomId: this.room.name,
|
|
729
|
+
entryId,
|
|
730
|
+
ts: getCurrentTimestamp(),
|
|
731
|
+
sender: senderInfo,
|
|
732
|
+
payload: {
|
|
733
|
+
emoji,
|
|
734
|
+
op: "remove"
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
await this.room.localParticipant.sendText(JSON.stringify(envelope), {
|
|
738
|
+
topic: "chat:v1"
|
|
739
|
+
});
|
|
740
|
+
} catch (error) {
|
|
741
|
+
useRtcStore.getState().addError({
|
|
742
|
+
code: "CHAT_REACTION_FAILED",
|
|
743
|
+
message: error instanceof Error ? error.message : "Failed to remove reaction",
|
|
744
|
+
timestamp: Date.now(),
|
|
745
|
+
context: { entryId, emoji }
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
handleIncomingMessage(text) {
|
|
750
|
+
try {
|
|
751
|
+
const parsed = JSON.parse(text);
|
|
752
|
+
if (!isValidEnvelope(parsed)) {
|
|
753
|
+
logger.warn("Invalid envelope received", parsed);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
useChatStore.getState().applyIncoming(parsed);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
logger.error("Error parsing incoming message", error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
getSenderInfo() {
|
|
762
|
+
const localParticipant = this.room.localParticipant;
|
|
763
|
+
const sender = {
|
|
764
|
+
id: localParticipant.identity
|
|
765
|
+
};
|
|
766
|
+
if (localParticipant.metadata) {
|
|
767
|
+
try {
|
|
768
|
+
sender.info = JSON.parse(localParticipant.metadata);
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return sender;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
function useChat() {
|
|
776
|
+
const service = useFeatureService("chat");
|
|
777
|
+
const byId = useChatStore((state) => state.byId);
|
|
778
|
+
const order = useChatStore((state) => state.order);
|
|
779
|
+
const participantCache = useChatStore((state) => state.participantCache);
|
|
780
|
+
const entries = react.useMemo(() => {
|
|
781
|
+
return order.map((id) => byId[id]).filter((entry) => entry !== void 0).sort(compareEntries);
|
|
782
|
+
}, [byId, order]);
|
|
783
|
+
const getParticipantInfo = react.useCallback(
|
|
784
|
+
(id) => {
|
|
785
|
+
return participantCache[id] || null;
|
|
786
|
+
},
|
|
787
|
+
[participantCache]
|
|
788
|
+
);
|
|
789
|
+
const send = react.useCallback(
|
|
790
|
+
async (content) => service.send(content),
|
|
791
|
+
[service]
|
|
792
|
+
);
|
|
793
|
+
const edit = react.useCallback(
|
|
794
|
+
async (id, content) => service.edit(id, content),
|
|
795
|
+
[service]
|
|
796
|
+
);
|
|
797
|
+
const remove = react.useCallback(
|
|
798
|
+
async (id) => service.remove(id),
|
|
799
|
+
[service]
|
|
800
|
+
);
|
|
801
|
+
const react$1 = react.useCallback(
|
|
802
|
+
async (id, emoji) => service.react(id, emoji),
|
|
803
|
+
[service]
|
|
804
|
+
);
|
|
805
|
+
const unreact = react.useCallback(
|
|
806
|
+
async (id, emoji) => service.unreact(id, emoji),
|
|
807
|
+
[service]
|
|
808
|
+
);
|
|
809
|
+
const isOwnEntry = react.useCallback(
|
|
810
|
+
(entry) => entry.sender.id === service.getLocalParticipantId(),
|
|
811
|
+
[service]
|
|
812
|
+
);
|
|
813
|
+
return {
|
|
814
|
+
entries,
|
|
815
|
+
isReady: true,
|
|
816
|
+
getParticipantInfo,
|
|
817
|
+
isOwnEntry,
|
|
818
|
+
send,
|
|
819
|
+
edit,
|
|
820
|
+
remove,
|
|
821
|
+
react: react$1,
|
|
822
|
+
unreact
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/channel/registry.ts
|
|
827
|
+
var FEATURES = {
|
|
828
|
+
chat: {
|
|
829
|
+
name: "chat",
|
|
830
|
+
createService: (room) => new ChatService(room),
|
|
831
|
+
cleanupStore: () => useChatStore.getState().clearChat()
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var DEFAULT_FEATURES = ["chat"];
|
|
835
|
+
var logger2 = createLogger("channels:provider");
|
|
836
|
+
function DataChannelProvider({
|
|
837
|
+
room,
|
|
838
|
+
features = DEFAULT_FEATURES,
|
|
839
|
+
children
|
|
840
|
+
}) {
|
|
841
|
+
const services = react.useRef(/* @__PURE__ */ new Map());
|
|
842
|
+
const [isReady, setIsReady] = react.useState(false);
|
|
843
|
+
react.useEffect(() => {
|
|
844
|
+
if (!room) {
|
|
845
|
+
logger2.warn("DataChannelProvider mounted without room");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
logger2.debug("Initializing features", { features });
|
|
849
|
+
for (const featureName of features) {
|
|
850
|
+
const feature = FEATURES[featureName];
|
|
851
|
+
if (!feature) {
|
|
852
|
+
logger2.warn(`Feature "${featureName}" not found in registry`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
logger2.debug(`Initializing feature: ${featureName}`);
|
|
857
|
+
const service = feature.createService(room);
|
|
858
|
+
service.subscribe();
|
|
859
|
+
services.current.set(featureName, service);
|
|
860
|
+
logger2.info(`Feature "${featureName}" initialized`);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
logger2.error(`Failed to initialize feature "${featureName}"`, error);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
setIsReady(true);
|
|
866
|
+
logger2.info("All features initialized");
|
|
867
|
+
return () => {
|
|
868
|
+
logger2.debug("Cleaning up features");
|
|
869
|
+
services.current.forEach((service, name) => {
|
|
870
|
+
try {
|
|
871
|
+
logger2.debug(`Unsubscribing feature: ${name}`);
|
|
872
|
+
service.unsubscribe();
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger2.error(`Failed to unsubscribe feature "${name}"`, error);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
for (const featureName of features) {
|
|
878
|
+
const feature = FEATURES[featureName];
|
|
879
|
+
if (feature?.cleanupStore) {
|
|
880
|
+
try {
|
|
881
|
+
logger2.debug(`Cleaning store for feature: ${featureName}`);
|
|
882
|
+
feature.cleanupStore();
|
|
883
|
+
} catch (error) {
|
|
884
|
+
logger2.error(`Failed to cleanup store for "${featureName}"`, error);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
services.current.clear();
|
|
889
|
+
setIsReady(false);
|
|
890
|
+
logger2.info("Features cleanup complete");
|
|
891
|
+
};
|
|
892
|
+
}, [room, features]);
|
|
893
|
+
const contextValue = react.useMemo(
|
|
894
|
+
() => ({
|
|
895
|
+
services: services.current,
|
|
896
|
+
isReady
|
|
897
|
+
}),
|
|
898
|
+
[isReady]
|
|
899
|
+
);
|
|
900
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DataChannelContext.Provider, { value: contextValue, children });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
exports.ChatService = ChatService;
|
|
904
|
+
exports.DataChannelProvider = DataChannelProvider;
|
|
905
|
+
exports.compareEntries = compareEntries;
|
|
906
|
+
exports.generateEntryId = generateEntryId;
|
|
907
|
+
exports.getCurrentTimestamp = getCurrentTimestamp;
|
|
908
|
+
exports.isValidEnvelope = isValidEnvelope;
|
|
909
|
+
exports.useChat = useChat;
|
|
910
|
+
exports.useChatStore = useChatStore;
|
|
911
|
+
exports.useDataChannelContext = useDataChannelContext;
|
|
912
|
+
exports.useFeatureService = useFeatureService;
|
|
913
|
+
exports.validateContent = validateContent;
|
|
914
|
+
//# sourceMappingURL=index.cjs.map
|
|
915
|
+
//# sourceMappingURL=index.cjs.map
|