@nuraly/lumenjs 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CallType, CallEndReason, SignalOffer, SignalIceCandidate, SignalIceRestart, ConnectionQualityReport } from './types.js';
|
|
2
|
+
import type { CommunicationStore } from './store.js';
|
|
3
|
+
/** Context for signaling handlers */
|
|
4
|
+
export interface SignalingContext {
|
|
5
|
+
userId: string;
|
|
6
|
+
store: CommunicationStore;
|
|
7
|
+
/** Emit to a specific socket by ID */
|
|
8
|
+
emitToSocket: (socketId: string, data: any) => void;
|
|
9
|
+
/** Broadcast to all sockets in a room */
|
|
10
|
+
broadcastAll: (room: string, data: any) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleCallInitiate(ctx: SignalingContext, data: {
|
|
13
|
+
conversationId: string;
|
|
14
|
+
type: CallType;
|
|
15
|
+
calleeIds: string[];
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function handleCallRespond(ctx: SignalingContext, data: {
|
|
18
|
+
callId: string;
|
|
19
|
+
action: 'accept' | 'reject';
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function handleCallHangup(ctx: SignalingContext, data: {
|
|
22
|
+
callId: string;
|
|
23
|
+
reason: CallEndReason;
|
|
24
|
+
}): void;
|
|
25
|
+
export declare function handleCallMediaToggle(ctx: SignalingContext, data: {
|
|
26
|
+
callId: string;
|
|
27
|
+
audio?: boolean;
|
|
28
|
+
video?: boolean;
|
|
29
|
+
screenShare?: boolean;
|
|
30
|
+
}): void;
|
|
31
|
+
export declare function handleCallAddParticipant(ctx: SignalingContext, data: {
|
|
32
|
+
callId: string;
|
|
33
|
+
userId: string;
|
|
34
|
+
}): void;
|
|
35
|
+
export declare function handleCallRemoveParticipant(ctx: SignalingContext, data: {
|
|
36
|
+
callId: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
}): void;
|
|
39
|
+
export declare function handleSignalOffer(ctx: SignalingContext, data: SignalOffer): void;
|
|
40
|
+
export declare function handleSignalAnswer(ctx: SignalingContext, data: SignalOffer): void;
|
|
41
|
+
export declare function handleSignalIceCandidate(ctx: SignalingContext, data: SignalIceCandidate): void;
|
|
42
|
+
export declare function handleSignalIceRestart(ctx: SignalingContext, data: SignalIceRestart): void;
|
|
43
|
+
export declare function handleCallQualityReport(ctx: SignalingContext, data: ConnectionQualityReport): void;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/** Emit data to all sockets belonging to a user */
|
|
2
|
+
function emitToUser(ctx, targetUserId, data) {
|
|
3
|
+
const sockets = ctx.store.getSocketsForUser(targetUserId);
|
|
4
|
+
for (const sid of sockets) {
|
|
5
|
+
ctx.emitToSocket(sid, data);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// ── Call Lifecycle ───────────────────────────────────────────────
|
|
9
|
+
export function handleCallInitiate(ctx, data) {
|
|
10
|
+
// Check if caller is already in a call
|
|
11
|
+
const existingCall = ctx.store.getActiveCallForUser(ctx.userId);
|
|
12
|
+
if (existingCall) {
|
|
13
|
+
emitToUser(ctx, ctx.userId, {
|
|
14
|
+
event: 'call:state-changed',
|
|
15
|
+
data: { callId: existingCall.id, state: existingCall.state, error: 'already_in_call' },
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const callId = crypto.randomUUID();
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
const call = {
|
|
22
|
+
id: callId,
|
|
23
|
+
conversationId: data.conversationId,
|
|
24
|
+
type: data.type,
|
|
25
|
+
state: 'initiating',
|
|
26
|
+
callerId: ctx.userId,
|
|
27
|
+
calleeIds: data.calleeIds,
|
|
28
|
+
startedAt: now,
|
|
29
|
+
participants: [{
|
|
30
|
+
userId: ctx.userId,
|
|
31
|
+
joinedAt: now,
|
|
32
|
+
audioMuted: false,
|
|
33
|
+
videoMuted: data.type === 'audio',
|
|
34
|
+
screenSharing: false,
|
|
35
|
+
}],
|
|
36
|
+
};
|
|
37
|
+
ctx.store.addCall(call);
|
|
38
|
+
// Notify caller that call is initiating
|
|
39
|
+
emitToUser(ctx, ctx.userId, {
|
|
40
|
+
event: 'call:state-changed',
|
|
41
|
+
data: { callId, state: 'initiating' },
|
|
42
|
+
});
|
|
43
|
+
// Notify each callee with incoming call
|
|
44
|
+
for (const calleeId of data.calleeIds) {
|
|
45
|
+
// Check if callee is busy (exclude the current call being initiated)
|
|
46
|
+
const calleeActiveCall = ctx.store.getActiveCallForUser(calleeId);
|
|
47
|
+
if (calleeActiveCall && calleeActiveCall.id !== callId) {
|
|
48
|
+
emitToUser(ctx, ctx.userId, {
|
|
49
|
+
event: 'call:state-changed',
|
|
50
|
+
data: { callId, state: 'ended', endReason: 'busy' },
|
|
51
|
+
});
|
|
52
|
+
ctx.store.updateCallState(callId, 'ended', 'busy');
|
|
53
|
+
ctx.store.removeCall(callId);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
emitToUser(ctx, calleeId, { event: 'call:incoming', data: call });
|
|
57
|
+
}
|
|
58
|
+
// Update state to ringing
|
|
59
|
+
ctx.store.updateCallState(callId, 'ringing');
|
|
60
|
+
}
|
|
61
|
+
export function handleCallRespond(ctx, data) {
|
|
62
|
+
const call = ctx.store.getCall(data.callId);
|
|
63
|
+
if (!call)
|
|
64
|
+
return;
|
|
65
|
+
if (data.action === 'reject') {
|
|
66
|
+
ctx.store.updateCallState(data.callId, 'ended', 'rejected');
|
|
67
|
+
// Notify all parties
|
|
68
|
+
emitToUser(ctx, call.callerId, {
|
|
69
|
+
event: 'call:state-changed',
|
|
70
|
+
data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
|
|
71
|
+
});
|
|
72
|
+
emitToUser(ctx, ctx.userId, {
|
|
73
|
+
event: 'call:state-changed',
|
|
74
|
+
data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
|
|
75
|
+
});
|
|
76
|
+
ctx.store.removeCall(data.callId);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Accept — add callee as participant
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const participant = {
|
|
82
|
+
userId: ctx.userId,
|
|
83
|
+
joinedAt: now,
|
|
84
|
+
audioMuted: false,
|
|
85
|
+
videoMuted: call.type === 'audio',
|
|
86
|
+
screenSharing: false,
|
|
87
|
+
};
|
|
88
|
+
ctx.store.addCallParticipant(data.callId, participant);
|
|
89
|
+
ctx.store.updateCallState(data.callId, 'connecting');
|
|
90
|
+
// Notify caller that callee accepted
|
|
91
|
+
emitToUser(ctx, call.callerId, {
|
|
92
|
+
event: 'call:participant-joined',
|
|
93
|
+
data: { callId: data.callId, participant },
|
|
94
|
+
});
|
|
95
|
+
emitToUser(ctx, call.callerId, {
|
|
96
|
+
event: 'call:state-changed',
|
|
97
|
+
data: { callId: data.callId, state: 'connecting' },
|
|
98
|
+
});
|
|
99
|
+
// Notify callee of connecting state
|
|
100
|
+
emitToUser(ctx, ctx.userId, {
|
|
101
|
+
event: 'call:state-changed',
|
|
102
|
+
data: { callId: data.callId, state: 'connecting' },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function handleCallHangup(ctx, data) {
|
|
106
|
+
const call = ctx.store.getCall(data.callId);
|
|
107
|
+
if (!call)
|
|
108
|
+
return;
|
|
109
|
+
// Remove the user who hung up
|
|
110
|
+
ctx.store.removeCallParticipant(data.callId, ctx.userId);
|
|
111
|
+
// Notify other participants
|
|
112
|
+
for (const p of call.participants) {
|
|
113
|
+
if (p.userId === ctx.userId)
|
|
114
|
+
continue;
|
|
115
|
+
emitToUser(ctx, p.userId, {
|
|
116
|
+
event: 'call:participant-left',
|
|
117
|
+
data: { callId: data.callId, userId: ctx.userId },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Also notify caller/callees who may not be in participants yet (e.g., still ringing)
|
|
121
|
+
const allUsers = new Set([call.callerId, ...call.calleeIds]);
|
|
122
|
+
allUsers.delete(ctx.userId);
|
|
123
|
+
// If no participants left (or only one), end the call
|
|
124
|
+
const remainingParticipants = call.participants.filter(p => p.userId !== ctx.userId);
|
|
125
|
+
if (remainingParticipants.length <= 1) {
|
|
126
|
+
ctx.store.updateCallState(data.callId, 'ended', data.reason);
|
|
127
|
+
for (const uid of allUsers) {
|
|
128
|
+
emitToUser(ctx, uid, {
|
|
129
|
+
event: 'call:state-changed',
|
|
130
|
+
data: { callId: data.callId, state: 'ended', endReason: data.reason },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
ctx.store.removeCall(data.callId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function handleCallMediaToggle(ctx, data) {
|
|
137
|
+
const call = ctx.store.getCall(data.callId);
|
|
138
|
+
if (!call)
|
|
139
|
+
return;
|
|
140
|
+
const participant = call.participants.find(p => p.userId === ctx.userId);
|
|
141
|
+
if (!participant)
|
|
142
|
+
return;
|
|
143
|
+
if (data.audio !== undefined)
|
|
144
|
+
participant.audioMuted = !data.audio;
|
|
145
|
+
if (data.video !== undefined)
|
|
146
|
+
participant.videoMuted = !data.video;
|
|
147
|
+
if (data.screenShare !== undefined)
|
|
148
|
+
participant.screenSharing = data.screenShare;
|
|
149
|
+
// Notify all other participants
|
|
150
|
+
for (const p of call.participants) {
|
|
151
|
+
if (p.userId === ctx.userId)
|
|
152
|
+
continue;
|
|
153
|
+
emitToUser(ctx, p.userId, {
|
|
154
|
+
event: 'call:media-changed',
|
|
155
|
+
data: {
|
|
156
|
+
callId: data.callId,
|
|
157
|
+
userId: ctx.userId,
|
|
158
|
+
audio: data.audio,
|
|
159
|
+
video: data.video,
|
|
160
|
+
screenShare: data.screenShare,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── Mid-Call Participant Management ──────────────────────────────
|
|
166
|
+
export function handleCallAddParticipant(ctx, data) {
|
|
167
|
+
const call = ctx.store.getCall(data.callId);
|
|
168
|
+
if (!call)
|
|
169
|
+
return;
|
|
170
|
+
// Only existing participants can add someone
|
|
171
|
+
const isParticipant = call.participants.some(p => p.userId === ctx.userId);
|
|
172
|
+
if (!isParticipant)
|
|
173
|
+
return;
|
|
174
|
+
// Don't add someone already in the call
|
|
175
|
+
if (call.participants.some(p => p.userId === data.userId))
|
|
176
|
+
return;
|
|
177
|
+
// Check if the target user is already in another call
|
|
178
|
+
const targetActiveCall = ctx.store.getActiveCallForUser(data.userId);
|
|
179
|
+
if (targetActiveCall) {
|
|
180
|
+
emitToUser(ctx, ctx.userId, {
|
|
181
|
+
event: 'call:state-changed',
|
|
182
|
+
data: { callId: data.callId, state: call.state, error: 'user_busy' },
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Add to calleeIds so they are tracked
|
|
187
|
+
if (!call.calleeIds.includes(data.userId)) {
|
|
188
|
+
call.calleeIds.push(data.userId);
|
|
189
|
+
}
|
|
190
|
+
// Send incoming call notification to the new user
|
|
191
|
+
emitToUser(ctx, data.userId, { event: 'call:incoming', data: call });
|
|
192
|
+
}
|
|
193
|
+
export function handleCallRemoveParticipant(ctx, data) {
|
|
194
|
+
const call = ctx.store.getCall(data.callId);
|
|
195
|
+
if (!call)
|
|
196
|
+
return;
|
|
197
|
+
// Only the caller (owner) or admins can kick participants
|
|
198
|
+
if (ctx.userId !== call.callerId)
|
|
199
|
+
return;
|
|
200
|
+
// Can't remove yourself (use hangup instead)
|
|
201
|
+
if (data.userId === ctx.userId)
|
|
202
|
+
return;
|
|
203
|
+
// Check the target is actually in the call
|
|
204
|
+
const targetParticipant = call.participants.find(p => p.userId === data.userId);
|
|
205
|
+
if (!targetParticipant)
|
|
206
|
+
return;
|
|
207
|
+
ctx.store.removeCallParticipant(data.callId, data.userId);
|
|
208
|
+
// Notify all remaining participants
|
|
209
|
+
for (const p of call.participants) {
|
|
210
|
+
if (p.userId === data.userId)
|
|
211
|
+
continue;
|
|
212
|
+
emitToUser(ctx, p.userId, {
|
|
213
|
+
event: 'call:participant-left',
|
|
214
|
+
data: { callId: data.callId, userId: data.userId },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Notify the removed user
|
|
218
|
+
emitToUser(ctx, data.userId, {
|
|
219
|
+
event: 'call:participant-left',
|
|
220
|
+
data: { callId: data.callId, userId: data.userId },
|
|
221
|
+
});
|
|
222
|
+
emitToUser(ctx, data.userId, {
|
|
223
|
+
event: 'call:state-changed',
|
|
224
|
+
data: { callId: data.callId, state: 'ended', endReason: 'completed' },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ── WebRTC Signal Relay ─────────────────────────────────────────
|
|
228
|
+
/** Verify the sender is part of an active call with the target user. */
|
|
229
|
+
function verifyCallMembership(ctx, fromUserId, toUserId) {
|
|
230
|
+
// Ensure fromUserId matches the authenticated user
|
|
231
|
+
if (fromUserId !== ctx.userId)
|
|
232
|
+
return false;
|
|
233
|
+
// Check both users are participants in the same active call
|
|
234
|
+
const call = ctx.store.getActiveCallForUser(ctx.userId);
|
|
235
|
+
if (!call)
|
|
236
|
+
return false;
|
|
237
|
+
const isTarget = call.callerId === toUserId || call.calleeIds.includes(toUserId) ||
|
|
238
|
+
call.participants.some(p => p.userId === toUserId);
|
|
239
|
+
return isTarget;
|
|
240
|
+
}
|
|
241
|
+
export function handleSignalOffer(ctx, data) {
|
|
242
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
243
|
+
return;
|
|
244
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:offer', data });
|
|
245
|
+
}
|
|
246
|
+
export function handleSignalAnswer(ctx, data) {
|
|
247
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
248
|
+
return;
|
|
249
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:answer', data });
|
|
250
|
+
}
|
|
251
|
+
export function handleSignalIceCandidate(ctx, data) {
|
|
252
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
253
|
+
return;
|
|
254
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:ice-candidate', data });
|
|
255
|
+
}
|
|
256
|
+
export function handleSignalIceRestart(ctx, data) {
|
|
257
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
258
|
+
return;
|
|
259
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:ice-restart', data });
|
|
260
|
+
}
|
|
261
|
+
export function handleCallQualityReport(ctx, data) {
|
|
262
|
+
const call = ctx.store.getCall(data.callId);
|
|
263
|
+
if (!call)
|
|
264
|
+
return;
|
|
265
|
+
// Broadcast quality report to all other participants in the call
|
|
266
|
+
for (const p of call.participants) {
|
|
267
|
+
if (p.userId === ctx.userId)
|
|
268
|
+
continue;
|
|
269
|
+
emitToUser(ctx, p.userId, { event: 'call:quality-changed', data });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PresenceStatus, Call, CallState, CallEndReason, CallParticipant, KeyBundle, PreKey, RateLimitConfig } from './types.js';
|
|
2
|
+
export interface PresenceEntry {
|
|
3
|
+
userId: string;
|
|
4
|
+
status: PresenceStatus;
|
|
5
|
+
lastSeen: string;
|
|
6
|
+
socketIds: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare class CommunicationStore {
|
|
9
|
+
/** userId → presence info */
|
|
10
|
+
private presence;
|
|
11
|
+
/** conversationId → map of userId → typing timer */
|
|
12
|
+
private typing;
|
|
13
|
+
/** callId → active call */
|
|
14
|
+
private calls;
|
|
15
|
+
/** userId → set of socket IDs (multi-device support) */
|
|
16
|
+
private userSockets;
|
|
17
|
+
/** socketId → userId (reverse lookup) */
|
|
18
|
+
private socketUser;
|
|
19
|
+
/** userId → set of conversation IDs the user has joined */
|
|
20
|
+
private userConversations;
|
|
21
|
+
/** userId → public key bundle for E2E encryption */
|
|
22
|
+
private keyBundles;
|
|
23
|
+
/** userId → rate limit tracking */
|
|
24
|
+
private rateLimits;
|
|
25
|
+
/** conversationId → set of user IDs currently in the room */
|
|
26
|
+
private conversationMembers;
|
|
27
|
+
/** Configurable typing timeout in ms */
|
|
28
|
+
typingTimeoutMs: number;
|
|
29
|
+
/** Max call age before auto-cleanup (1 hour) */
|
|
30
|
+
private static CALL_TTL_MS;
|
|
31
|
+
constructor();
|
|
32
|
+
mapUserSocket(userId: string, socketId: string): void;
|
|
33
|
+
unmapUserSocket(socketId: string): string | undefined;
|
|
34
|
+
getSocketsForUser(userId: string): Set<string>;
|
|
35
|
+
getUserForSocket(socketId: string): string | undefined;
|
|
36
|
+
isUserOnline(userId: string): boolean;
|
|
37
|
+
joinConversation(userId: string, conversationId: string): void;
|
|
38
|
+
leaveConversation(userId: string, conversationId: string): void;
|
|
39
|
+
getUserConversations(userId: string): Set<string>;
|
|
40
|
+
setPresence(userId: string, status: PresenceStatus): PresenceEntry;
|
|
41
|
+
getPresence(userId: string): PresenceEntry | undefined;
|
|
42
|
+
removePresence(userId: string): void;
|
|
43
|
+
setTyping(conversationId: string, userId: string, onExpire: () => void): void;
|
|
44
|
+
clearTyping(conversationId: string, userId: string): void;
|
|
45
|
+
clearAllTypingForUser(userId: string): string[];
|
|
46
|
+
getTypingUsers(conversationId: string): string[];
|
|
47
|
+
addCall(call: Call): void;
|
|
48
|
+
getCall(callId: string): Call | undefined;
|
|
49
|
+
updateCallState(callId: string, state: CallState, endReason?: CallEndReason): Call | undefined;
|
|
50
|
+
addCallParticipant(callId: string, participant: CallParticipant): Call | undefined;
|
|
51
|
+
removeCallParticipant(callId: string, userId: string): Call | undefined;
|
|
52
|
+
removeCall(callId: string): void;
|
|
53
|
+
getActiveCallForUser(userId: string): Call | undefined;
|
|
54
|
+
addConversationMember(conversationId: string, userId: string): void;
|
|
55
|
+
removeConversationMember(conversationId: string, userId: string): void;
|
|
56
|
+
getConversationMembers(conversationId: string): Set<string>;
|
|
57
|
+
removeUserFromAllConversations(userId: string): string[];
|
|
58
|
+
setKeyBundle(userId: string, bundle: KeyBundle): void;
|
|
59
|
+
getKeyBundle(userId: string): KeyBundle | undefined;
|
|
60
|
+
/** Pop one one-time pre-key (consumed once per session setup). Returns undefined if depleted. */
|
|
61
|
+
popOneTimePreKey(userId: string): PreKey | undefined;
|
|
62
|
+
/** Check how many one-time pre-keys remain */
|
|
63
|
+
getOneTimePreKeyCount(userId: string): number;
|
|
64
|
+
removeKeyBundle(userId: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Check whether a user is rate-limited. If not, records the message timestamp.
|
|
67
|
+
* Returns true if the message is allowed, false if rate-limited.
|
|
68
|
+
*/
|
|
69
|
+
checkRateLimit(userId: string, config: RateLimitConfig): boolean;
|
|
70
|
+
}
|
|
71
|
+
export declare function useCommunicationStore(): CommunicationStore;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// ── CommunicationStore ────────────────────────────────────────────
|
|
2
|
+
const DEFAULT_TYPING_TIMEOUT_MS = 5000;
|
|
3
|
+
export class CommunicationStore {
|
|
4
|
+
/** Max call age before auto-cleanup (1 hour) */
|
|
5
|
+
static { this.CALL_TTL_MS = 60 * 60 * 1000; }
|
|
6
|
+
constructor() {
|
|
7
|
+
/** userId → presence info */
|
|
8
|
+
this.presence = new Map();
|
|
9
|
+
/** conversationId → map of userId → typing timer */
|
|
10
|
+
this.typing = new Map();
|
|
11
|
+
/** callId → active call */
|
|
12
|
+
this.calls = new Map();
|
|
13
|
+
/** userId → set of socket IDs (multi-device support) */
|
|
14
|
+
this.userSockets = new Map();
|
|
15
|
+
/** socketId → userId (reverse lookup) */
|
|
16
|
+
this.socketUser = new Map();
|
|
17
|
+
/** userId → set of conversation IDs the user has joined */
|
|
18
|
+
this.userConversations = new Map();
|
|
19
|
+
/** userId → public key bundle for E2E encryption */
|
|
20
|
+
this.keyBundles = new Map();
|
|
21
|
+
/** userId → rate limit tracking */
|
|
22
|
+
this.rateLimits = new Map();
|
|
23
|
+
/** conversationId → set of user IDs currently in the room */
|
|
24
|
+
this.conversationMembers = new Map();
|
|
25
|
+
/** Configurable typing timeout in ms */
|
|
26
|
+
this.typingTimeoutMs = DEFAULT_TYPING_TIMEOUT_MS;
|
|
27
|
+
// Periodically clean up stale calls that were never properly ended
|
|
28
|
+
setInterval(() => {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
for (const [id, call] of this.calls) {
|
|
31
|
+
const startedAt = call.startedAt ? new Date(call.startedAt).getTime() : 0;
|
|
32
|
+
if (now - startedAt > CommunicationStore.CALL_TTL_MS) {
|
|
33
|
+
this.calls.delete(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, 5 * 60 * 1000).unref(); // every 5 min, unref so it doesn't keep process alive
|
|
37
|
+
}
|
|
38
|
+
// ── User-Socket Mapping ───────────────────────────────────────
|
|
39
|
+
mapUserSocket(userId, socketId) {
|
|
40
|
+
let sockets = this.userSockets.get(userId);
|
|
41
|
+
if (!sockets) {
|
|
42
|
+
sockets = new Set();
|
|
43
|
+
this.userSockets.set(userId, sockets);
|
|
44
|
+
}
|
|
45
|
+
sockets.add(socketId);
|
|
46
|
+
this.socketUser.set(socketId, userId);
|
|
47
|
+
}
|
|
48
|
+
unmapUserSocket(socketId) {
|
|
49
|
+
const userId = this.socketUser.get(socketId);
|
|
50
|
+
if (!userId)
|
|
51
|
+
return undefined;
|
|
52
|
+
this.socketUser.delete(socketId);
|
|
53
|
+
const sockets = this.userSockets.get(userId);
|
|
54
|
+
if (sockets) {
|
|
55
|
+
sockets.delete(socketId);
|
|
56
|
+
if (sockets.size === 0)
|
|
57
|
+
this.userSockets.delete(userId);
|
|
58
|
+
}
|
|
59
|
+
return userId;
|
|
60
|
+
}
|
|
61
|
+
getSocketsForUser(userId) {
|
|
62
|
+
return this.userSockets.get(userId) || new Set();
|
|
63
|
+
}
|
|
64
|
+
getUserForSocket(socketId) {
|
|
65
|
+
return this.socketUser.get(socketId);
|
|
66
|
+
}
|
|
67
|
+
isUserOnline(userId) {
|
|
68
|
+
const sockets = this.userSockets.get(userId);
|
|
69
|
+
return !!sockets && sockets.size > 0;
|
|
70
|
+
}
|
|
71
|
+
// ── Conversation Membership ────────────────────────────────────
|
|
72
|
+
joinConversation(userId, conversationId) {
|
|
73
|
+
let convs = this.userConversations.get(userId);
|
|
74
|
+
if (!convs) {
|
|
75
|
+
convs = new Set();
|
|
76
|
+
this.userConversations.set(userId, convs);
|
|
77
|
+
}
|
|
78
|
+
convs.add(conversationId);
|
|
79
|
+
}
|
|
80
|
+
leaveConversation(userId, conversationId) {
|
|
81
|
+
const convs = this.userConversations.get(userId);
|
|
82
|
+
if (!convs)
|
|
83
|
+
return;
|
|
84
|
+
convs.delete(conversationId);
|
|
85
|
+
if (convs.size === 0)
|
|
86
|
+
this.userConversations.delete(userId);
|
|
87
|
+
}
|
|
88
|
+
getUserConversations(userId) {
|
|
89
|
+
return this.userConversations.get(userId) || new Set();
|
|
90
|
+
}
|
|
91
|
+
// ── Presence ──────────────────────────────────────────────────
|
|
92
|
+
setPresence(userId, status) {
|
|
93
|
+
const existing = this.presence.get(userId);
|
|
94
|
+
const entry = {
|
|
95
|
+
userId,
|
|
96
|
+
status,
|
|
97
|
+
lastSeen: new Date().toISOString(),
|
|
98
|
+
socketIds: existing?.socketIds || this.getSocketsForUser(userId),
|
|
99
|
+
};
|
|
100
|
+
this.presence.set(userId, entry);
|
|
101
|
+
return entry;
|
|
102
|
+
}
|
|
103
|
+
getPresence(userId) {
|
|
104
|
+
return this.presence.get(userId);
|
|
105
|
+
}
|
|
106
|
+
removePresence(userId) {
|
|
107
|
+
this.presence.delete(userId);
|
|
108
|
+
}
|
|
109
|
+
// ── Typing ────────────────────────────────────────────────────
|
|
110
|
+
setTyping(conversationId, userId, onExpire) {
|
|
111
|
+
let convTyping = this.typing.get(conversationId);
|
|
112
|
+
if (!convTyping) {
|
|
113
|
+
convTyping = new Map();
|
|
114
|
+
this.typing.set(conversationId, convTyping);
|
|
115
|
+
}
|
|
116
|
+
const existing = convTyping.get(userId);
|
|
117
|
+
if (existing)
|
|
118
|
+
clearTimeout(existing.timer);
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
this.clearTyping(conversationId, userId);
|
|
121
|
+
onExpire();
|
|
122
|
+
}, this.typingTimeoutMs);
|
|
123
|
+
convTyping.set(userId, { userId, timer });
|
|
124
|
+
}
|
|
125
|
+
clearTyping(conversationId, userId) {
|
|
126
|
+
const convTyping = this.typing.get(conversationId);
|
|
127
|
+
if (!convTyping)
|
|
128
|
+
return;
|
|
129
|
+
const entry = convTyping.get(userId);
|
|
130
|
+
if (entry) {
|
|
131
|
+
clearTimeout(entry.timer);
|
|
132
|
+
convTyping.delete(userId);
|
|
133
|
+
}
|
|
134
|
+
if (convTyping.size === 0)
|
|
135
|
+
this.typing.delete(conversationId);
|
|
136
|
+
}
|
|
137
|
+
clearAllTypingForUser(userId) {
|
|
138
|
+
const cleared = [];
|
|
139
|
+
for (const [convId, convTyping] of this.typing) {
|
|
140
|
+
if (convTyping.has(userId)) {
|
|
141
|
+
const entry = convTyping.get(userId);
|
|
142
|
+
clearTimeout(entry.timer);
|
|
143
|
+
convTyping.delete(userId);
|
|
144
|
+
cleared.push(convId);
|
|
145
|
+
}
|
|
146
|
+
if (convTyping.size === 0)
|
|
147
|
+
this.typing.delete(convId);
|
|
148
|
+
}
|
|
149
|
+
return cleared;
|
|
150
|
+
}
|
|
151
|
+
getTypingUsers(conversationId) {
|
|
152
|
+
const convTyping = this.typing.get(conversationId);
|
|
153
|
+
if (!convTyping)
|
|
154
|
+
return [];
|
|
155
|
+
return Array.from(convTyping.keys());
|
|
156
|
+
}
|
|
157
|
+
// ── Calls ─────────────────────────────────────────────────────
|
|
158
|
+
addCall(call) {
|
|
159
|
+
this.calls.set(call.id, call);
|
|
160
|
+
}
|
|
161
|
+
getCall(callId) {
|
|
162
|
+
return this.calls.get(callId);
|
|
163
|
+
}
|
|
164
|
+
updateCallState(callId, state, endReason) {
|
|
165
|
+
const call = this.calls.get(callId);
|
|
166
|
+
if (!call)
|
|
167
|
+
return undefined;
|
|
168
|
+
call.state = state;
|
|
169
|
+
if (endReason)
|
|
170
|
+
call.endReason = endReason;
|
|
171
|
+
if (state === 'connected' && !call.answeredAt)
|
|
172
|
+
call.answeredAt = new Date().toISOString();
|
|
173
|
+
if (state === 'ended')
|
|
174
|
+
call.endedAt = new Date().toISOString();
|
|
175
|
+
return call;
|
|
176
|
+
}
|
|
177
|
+
addCallParticipant(callId, participant) {
|
|
178
|
+
const call = this.calls.get(callId);
|
|
179
|
+
if (!call)
|
|
180
|
+
return undefined;
|
|
181
|
+
const existing = call.participants.findIndex(p => p.userId === participant.userId);
|
|
182
|
+
if (existing >= 0)
|
|
183
|
+
call.participants[existing] = participant;
|
|
184
|
+
else
|
|
185
|
+
call.participants.push(participant);
|
|
186
|
+
return call;
|
|
187
|
+
}
|
|
188
|
+
removeCallParticipant(callId, userId) {
|
|
189
|
+
const call = this.calls.get(callId);
|
|
190
|
+
if (!call)
|
|
191
|
+
return undefined;
|
|
192
|
+
call.participants = call.participants.filter(p => p.userId !== userId);
|
|
193
|
+
return call;
|
|
194
|
+
}
|
|
195
|
+
removeCall(callId) {
|
|
196
|
+
this.calls.delete(callId);
|
|
197
|
+
}
|
|
198
|
+
getActiveCallForUser(userId) {
|
|
199
|
+
for (const call of this.calls.values()) {
|
|
200
|
+
if (call.state === 'ended')
|
|
201
|
+
continue;
|
|
202
|
+
if (call.callerId === userId || call.calleeIds.includes(userId))
|
|
203
|
+
return call;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
// ── Conversation Membership ─────────────────────────────────────
|
|
208
|
+
addConversationMember(conversationId, userId) {
|
|
209
|
+
let members = this.conversationMembers.get(conversationId);
|
|
210
|
+
if (!members) {
|
|
211
|
+
members = new Set();
|
|
212
|
+
this.conversationMembers.set(conversationId, members);
|
|
213
|
+
}
|
|
214
|
+
members.add(userId);
|
|
215
|
+
}
|
|
216
|
+
removeConversationMember(conversationId, userId) {
|
|
217
|
+
const members = this.conversationMembers.get(conversationId);
|
|
218
|
+
if (!members)
|
|
219
|
+
return;
|
|
220
|
+
members.delete(userId);
|
|
221
|
+
if (members.size === 0)
|
|
222
|
+
this.conversationMembers.delete(conversationId);
|
|
223
|
+
}
|
|
224
|
+
getConversationMembers(conversationId) {
|
|
225
|
+
return this.conversationMembers.get(conversationId) || new Set();
|
|
226
|
+
}
|
|
227
|
+
removeUserFromAllConversations(userId) {
|
|
228
|
+
const removed = [];
|
|
229
|
+
for (const [convId, members] of this.conversationMembers) {
|
|
230
|
+
if (members.has(userId)) {
|
|
231
|
+
members.delete(userId);
|
|
232
|
+
removed.push(convId);
|
|
233
|
+
}
|
|
234
|
+
if (members.size === 0)
|
|
235
|
+
this.conversationMembers.delete(convId);
|
|
236
|
+
}
|
|
237
|
+
return removed;
|
|
238
|
+
}
|
|
239
|
+
// ── E2E Encryption Key Bundles ──────────────────────────────────
|
|
240
|
+
setKeyBundle(userId, bundle) {
|
|
241
|
+
this.keyBundles.set(userId, bundle);
|
|
242
|
+
}
|
|
243
|
+
getKeyBundle(userId) {
|
|
244
|
+
return this.keyBundles.get(userId);
|
|
245
|
+
}
|
|
246
|
+
/** Pop one one-time pre-key (consumed once per session setup). Returns undefined if depleted. */
|
|
247
|
+
popOneTimePreKey(userId) {
|
|
248
|
+
const bundle = this.keyBundles.get(userId);
|
|
249
|
+
if (!bundle || bundle.oneTimePreKeys.length === 0)
|
|
250
|
+
return undefined;
|
|
251
|
+
return bundle.oneTimePreKeys.shift();
|
|
252
|
+
}
|
|
253
|
+
/** Check how many one-time pre-keys remain */
|
|
254
|
+
getOneTimePreKeyCount(userId) {
|
|
255
|
+
const bundle = this.keyBundles.get(userId);
|
|
256
|
+
return bundle?.oneTimePreKeys.length || 0;
|
|
257
|
+
}
|
|
258
|
+
removeKeyBundle(userId) {
|
|
259
|
+
this.keyBundles.delete(userId);
|
|
260
|
+
}
|
|
261
|
+
// ── Rate Limiting ──────────────────────────────────────────────
|
|
262
|
+
/**
|
|
263
|
+
* Check whether a user is rate-limited. If not, records the message timestamp.
|
|
264
|
+
* Returns true if the message is allowed, false if rate-limited.
|
|
265
|
+
*/
|
|
266
|
+
checkRateLimit(userId, config) {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const windowStart = now - config.windowSeconds * 1000;
|
|
269
|
+
let entry = this.rateLimits.get(userId);
|
|
270
|
+
if (!entry) {
|
|
271
|
+
entry = { timestamps: [] };
|
|
272
|
+
this.rateLimits.set(userId, entry);
|
|
273
|
+
}
|
|
274
|
+
// Prune timestamps outside the current window
|
|
275
|
+
entry.timestamps = entry.timestamps.filter(t => t > windowStart);
|
|
276
|
+
if (entry.timestamps.length >= config.maxMessages) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
entry.timestamps.push(now);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ── Singleton ─────────────────────────────────────────────────────
|
|
284
|
+
let _instance = null;
|
|
285
|
+
export function useCommunicationStore() {
|
|
286
|
+
if (!_instance)
|
|
287
|
+
_instance = new CommunicationStore();
|
|
288
|
+
return _instance;
|
|
289
|
+
}
|