@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,30 @@
|
|
|
1
|
+
import { getI18nConfig, getLocale } from './i18n.js';
|
|
2
|
+
const connections = new Map();
|
|
3
|
+
export async function connectSocket(routePath, params) {
|
|
4
|
+
const { io } = await import('socket.io-client');
|
|
5
|
+
const ns = `/nk${routePath === '/' ? '/index' : routePath}`;
|
|
6
|
+
const query = {};
|
|
7
|
+
if (Object.keys(params).length > 0) {
|
|
8
|
+
query.__params = JSON.stringify(params);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const config = getI18nConfig();
|
|
12
|
+
if (config) {
|
|
13
|
+
query.__locale = getLocale();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
// Disconnect existing socket for this route to prevent leaks
|
|
18
|
+
const existing = connections.get(routePath);
|
|
19
|
+
if (existing)
|
|
20
|
+
existing.disconnect();
|
|
21
|
+
const socket = io(ns, { path: '/__nk_socketio/', query });
|
|
22
|
+
connections.set(routePath, socket);
|
|
23
|
+
return socket;
|
|
24
|
+
}
|
|
25
|
+
export function disconnectAllSockets() {
|
|
26
|
+
for (const [, socket] of connections) {
|
|
27
|
+
socket.disconnect();
|
|
28
|
+
}
|
|
29
|
+
connections.clear();
|
|
30
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC peer connection manager.
|
|
3
|
+
* Wraps RTCPeerConnection and wires to the LumenJS communication SDK signaling.
|
|
4
|
+
*/
|
|
5
|
+
export type CallRole = 'caller' | 'callee';
|
|
6
|
+
export interface WebRTCCallbacks {
|
|
7
|
+
onRemoteStream: (stream: MediaStream) => void;
|
|
8
|
+
onLocalStream: (stream: MediaStream) => void;
|
|
9
|
+
onConnectionStateChange: (state: RTCPeerConnectionState) => void;
|
|
10
|
+
onIceCandidate: (candidate: RTCIceCandidate) => void;
|
|
11
|
+
onError: (error: Error) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class WebRTCManager {
|
|
14
|
+
private _pc;
|
|
15
|
+
private _localStream;
|
|
16
|
+
private _remoteStream;
|
|
17
|
+
private _callbacks;
|
|
18
|
+
private _pendingCandidates;
|
|
19
|
+
private _role;
|
|
20
|
+
private _screenStream;
|
|
21
|
+
constructor(callbacks: WebRTCCallbacks, iceServers?: RTCIceServer[]);
|
|
22
|
+
private _createPeerConnection;
|
|
23
|
+
get localStream(): MediaStream | null;
|
|
24
|
+
get remoteStream(): MediaStream | null;
|
|
25
|
+
get role(): CallRole;
|
|
26
|
+
get connectionState(): RTCPeerConnectionState | null;
|
|
27
|
+
/** Acquire local media (camera/mic) and add tracks to the peer connection */
|
|
28
|
+
startLocalMedia(video?: boolean, audio?: boolean): Promise<MediaStream>;
|
|
29
|
+
/** Create an SDP offer (caller side) */
|
|
30
|
+
createOffer(): Promise<string>;
|
|
31
|
+
/** Handle received SDP offer and create answer (callee side) */
|
|
32
|
+
handleOffer(sdp: string): Promise<string>;
|
|
33
|
+
/** Handle received SDP answer (caller side) */
|
|
34
|
+
handleAnswer(sdp: string): Promise<void>;
|
|
35
|
+
/** Add a received ICE candidate */
|
|
36
|
+
addIceCandidate(candidate: string, sdpMLineIndex: number | null, sdpMid: string | null): Promise<void>;
|
|
37
|
+
private _flushPendingCandidates;
|
|
38
|
+
/** Toggle audio mute */
|
|
39
|
+
setAudioEnabled(enabled: boolean): void;
|
|
40
|
+
/** Toggle video */
|
|
41
|
+
setVideoEnabled(enabled: boolean): void;
|
|
42
|
+
/** Replace video track with screen share */
|
|
43
|
+
startScreenShare(): Promise<MediaStream>;
|
|
44
|
+
/** Revert from screen share back to camera */
|
|
45
|
+
stopScreenShare(): Promise<void>;
|
|
46
|
+
/** Clean up everything */
|
|
47
|
+
destroy(): void;
|
|
48
|
+
}
|
|
49
|
+
export interface GroupWebRTCCallbacks {
|
|
50
|
+
onLocalStream: (stream: MediaStream) => void;
|
|
51
|
+
onRemoteStream: (userId: string, stream: MediaStream) => void;
|
|
52
|
+
onRemoteStreamRemoved: (userId: string) => void;
|
|
53
|
+
onConnectionStateChange: (userId: string, state: RTCPeerConnectionState) => void;
|
|
54
|
+
onIceCandidate: (toUserId: string, candidate: RTCIceCandidate) => void;
|
|
55
|
+
onError: (error: Error) => void;
|
|
56
|
+
}
|
|
57
|
+
export declare class GroupWebRTCManager {
|
|
58
|
+
private _peers;
|
|
59
|
+
private _localStream;
|
|
60
|
+
private _callbacks;
|
|
61
|
+
private _iceServers;
|
|
62
|
+
private _screenStream;
|
|
63
|
+
constructor(callbacks: GroupWebRTCCallbacks, iceServers?: RTCIceServer[]);
|
|
64
|
+
get localStream(): MediaStream | null;
|
|
65
|
+
get peerCount(): number;
|
|
66
|
+
getRemoteStream(userId: string): MediaStream | null;
|
|
67
|
+
getRemoteStreams(): Map<string, MediaStream>;
|
|
68
|
+
/** Acquire local media — call once before adding peers */
|
|
69
|
+
startLocalMedia(video?: boolean, audio?: boolean): Promise<MediaStream>;
|
|
70
|
+
/** Create a peer connection for a remote user and optionally create an offer */
|
|
71
|
+
addPeer(userId: string, isCaller: boolean): Promise<string | void>;
|
|
72
|
+
/** Remove and close a peer connection */
|
|
73
|
+
removePeer(userId: string): void;
|
|
74
|
+
/** Handle an SDP offer from a remote peer and return an answer */
|
|
75
|
+
handleOffer(fromUserId: string, sdp: string): Promise<string>;
|
|
76
|
+
/** Handle an SDP answer from a remote peer */
|
|
77
|
+
handleAnswer(fromUserId: string, sdp: string): Promise<void>;
|
|
78
|
+
/** Add an ICE candidate for a specific peer */
|
|
79
|
+
addIceCandidate(fromUserId: string, candidate: string, sdpMLineIndex: number | null, sdpMid: string | null): Promise<void>;
|
|
80
|
+
private _flushPendingCandidates;
|
|
81
|
+
/** Toggle audio for all peers */
|
|
82
|
+
setAudioEnabled(enabled: boolean): void;
|
|
83
|
+
/** Toggle video for all peers */
|
|
84
|
+
setVideoEnabled(enabled: boolean): void;
|
|
85
|
+
/** Replace camera track with screen share on all peers */
|
|
86
|
+
startScreenShare(): Promise<MediaStream>;
|
|
87
|
+
/** Revert from screen share back to camera on all peers */
|
|
88
|
+
stopScreenShare(): Promise<void>;
|
|
89
|
+
/** Clean up everything */
|
|
90
|
+
destroy(): void;
|
|
91
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC peer connection manager.
|
|
3
|
+
* Wraps RTCPeerConnection and wires to the LumenJS communication SDK signaling.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_ICE_SERVERS = [
|
|
6
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
7
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
8
|
+
];
|
|
9
|
+
function getIceServers(custom) {
|
|
10
|
+
return custom
|
|
11
|
+
|| (typeof window !== 'undefined' && window.__NK_ICE_SERVERS)
|
|
12
|
+
|| DEFAULT_ICE_SERVERS;
|
|
13
|
+
}
|
|
14
|
+
export class WebRTCManager {
|
|
15
|
+
constructor(callbacks, iceServers) {
|
|
16
|
+
this._pc = null;
|
|
17
|
+
this._localStream = null;
|
|
18
|
+
this._remoteStream = null;
|
|
19
|
+
this._pendingCandidates = [];
|
|
20
|
+
this._role = 'caller';
|
|
21
|
+
this._screenStream = null;
|
|
22
|
+
this._callbacks = callbacks;
|
|
23
|
+
this._createPeerConnection(getIceServers(iceServers));
|
|
24
|
+
}
|
|
25
|
+
_createPeerConnection(iceServers) {
|
|
26
|
+
this._pc = new RTCPeerConnection({ iceServers });
|
|
27
|
+
this._pc.onicecandidate = (event) => {
|
|
28
|
+
if (event.candidate) {
|
|
29
|
+
this._callbacks.onIceCandidate(event.candidate);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
this._pc.ontrack = (event) => {
|
|
33
|
+
if (!this._remoteStream) {
|
|
34
|
+
this._remoteStream = new MediaStream();
|
|
35
|
+
this._callbacks.onRemoteStream(this._remoteStream);
|
|
36
|
+
}
|
|
37
|
+
this._remoteStream.addTrack(event.track);
|
|
38
|
+
};
|
|
39
|
+
this._pc.onconnectionstatechange = () => {
|
|
40
|
+
if (this._pc) {
|
|
41
|
+
this._callbacks.onConnectionStateChange(this._pc.connectionState);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
get localStream() { return this._localStream; }
|
|
46
|
+
get remoteStream() { return this._remoteStream; }
|
|
47
|
+
get role() { return this._role; }
|
|
48
|
+
get connectionState() { return this._pc?.connectionState ?? null; }
|
|
49
|
+
/** Acquire local media (camera/mic) and add tracks to the peer connection */
|
|
50
|
+
async startLocalMedia(video = true, audio = true) {
|
|
51
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
52
|
+
const err = new Error('Media devices unavailable — HTTPS is required for calls');
|
|
53
|
+
this._callbacks.onError(err);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Always request video so both peers negotiate a video track in the SDP.
|
|
58
|
+
// For audio-only calls the video track is immediately disabled (black frame)
|
|
59
|
+
// but stays in the SDP as sendrecv, allowing replaceTrack for screen sharing.
|
|
60
|
+
this._localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio });
|
|
61
|
+
this._callbacks.onLocalStream(this._localStream);
|
|
62
|
+
if (!video) {
|
|
63
|
+
for (const vt of this._localStream.getVideoTracks()) {
|
|
64
|
+
vt.enabled = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const track of this._localStream.getTracks()) {
|
|
68
|
+
this._pc?.addTrack(track, this._localStream);
|
|
69
|
+
}
|
|
70
|
+
return this._localStream;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this._callbacks.onError(new Error(`Failed to access media: ${err.message}`));
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Create an SDP offer (caller side) */
|
|
78
|
+
async createOffer() {
|
|
79
|
+
this._role = 'caller';
|
|
80
|
+
if (!this._pc)
|
|
81
|
+
throw new Error('No peer connection');
|
|
82
|
+
const offer = await this._pc.createOffer();
|
|
83
|
+
await this._pc.setLocalDescription(offer);
|
|
84
|
+
return offer.sdp;
|
|
85
|
+
}
|
|
86
|
+
/** Handle received SDP offer and create answer (callee side) */
|
|
87
|
+
async handleOffer(sdp) {
|
|
88
|
+
this._role = 'callee';
|
|
89
|
+
if (!this._pc)
|
|
90
|
+
throw new Error('No peer connection');
|
|
91
|
+
await this._pc.setRemoteDescription({ type: 'offer', sdp });
|
|
92
|
+
// Flush pending ICE candidates
|
|
93
|
+
await this._flushPendingCandidates();
|
|
94
|
+
const answer = await this._pc.createAnswer();
|
|
95
|
+
await this._pc.setLocalDescription(answer);
|
|
96
|
+
return answer.sdp;
|
|
97
|
+
}
|
|
98
|
+
/** Handle received SDP answer (caller side) */
|
|
99
|
+
async handleAnswer(sdp) {
|
|
100
|
+
if (!this._pc)
|
|
101
|
+
throw new Error('No peer connection');
|
|
102
|
+
await this._pc.setRemoteDescription({ type: 'answer', sdp });
|
|
103
|
+
await this._flushPendingCandidates();
|
|
104
|
+
}
|
|
105
|
+
/** Add a received ICE candidate */
|
|
106
|
+
async addIceCandidate(candidate, sdpMLineIndex, sdpMid) {
|
|
107
|
+
const init = {
|
|
108
|
+
candidate,
|
|
109
|
+
sdpMLineIndex: sdpMLineIndex ?? undefined,
|
|
110
|
+
sdpMid: sdpMid ?? undefined,
|
|
111
|
+
};
|
|
112
|
+
if (!this._pc?.remoteDescription) {
|
|
113
|
+
// Queue candidates until remote description is set
|
|
114
|
+
this._pendingCandidates.push(init);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await this._pc.addIceCandidate(init);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.warn('[WebRTC] Failed to add ICE candidate:', err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async _flushPendingCandidates() {
|
|
125
|
+
for (const c of this._pendingCandidates) {
|
|
126
|
+
try {
|
|
127
|
+
await this._pc?.addIceCandidate(c);
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
}
|
|
131
|
+
this._pendingCandidates = [];
|
|
132
|
+
}
|
|
133
|
+
/** Toggle audio mute */
|
|
134
|
+
setAudioEnabled(enabled) {
|
|
135
|
+
if (this._localStream) {
|
|
136
|
+
for (const track of this._localStream.getAudioTracks()) {
|
|
137
|
+
track.enabled = enabled;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Toggle video */
|
|
142
|
+
setVideoEnabled(enabled) {
|
|
143
|
+
if (this._localStream) {
|
|
144
|
+
for (const track of this._localStream.getVideoTracks()) {
|
|
145
|
+
track.enabled = enabled;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Replace video track with screen share */
|
|
150
|
+
async startScreenShare() {
|
|
151
|
+
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
152
|
+
throw new Error('Screen sharing unavailable — HTTPS is required');
|
|
153
|
+
}
|
|
154
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
|
155
|
+
const screenTrack = stream.getVideoTracks()[0];
|
|
156
|
+
if (this._pc) {
|
|
157
|
+
const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
|
|
158
|
+
if (sender) {
|
|
159
|
+
await sender.replaceTrack(screenTrack);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this._screenStream = stream;
|
|
163
|
+
// When user stops sharing via browser UI
|
|
164
|
+
screenTrack.onended = () => {
|
|
165
|
+
this.stopScreenShare();
|
|
166
|
+
};
|
|
167
|
+
return stream;
|
|
168
|
+
}
|
|
169
|
+
/** Revert from screen share back to camera */
|
|
170
|
+
async stopScreenShare() {
|
|
171
|
+
// Stop all screen share tracks so the OS stops the sharing indicator
|
|
172
|
+
if (this._screenStream) {
|
|
173
|
+
for (const track of this._screenStream.getTracks())
|
|
174
|
+
track.stop();
|
|
175
|
+
this._screenStream = null;
|
|
176
|
+
}
|
|
177
|
+
if (!this._pc || !this._localStream)
|
|
178
|
+
return;
|
|
179
|
+
const cameraTrack = this._localStream.getVideoTracks()[0] || null;
|
|
180
|
+
const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
|
|
181
|
+
if (sender) {
|
|
182
|
+
await sender.replaceTrack(cameraTrack);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Clean up everything */
|
|
186
|
+
destroy() {
|
|
187
|
+
if (this._screenStream) {
|
|
188
|
+
for (const track of this._screenStream.getTracks())
|
|
189
|
+
track.stop();
|
|
190
|
+
this._screenStream = null;
|
|
191
|
+
}
|
|
192
|
+
if (this._localStream) {
|
|
193
|
+
for (const track of this._localStream.getTracks()) {
|
|
194
|
+
track.stop();
|
|
195
|
+
}
|
|
196
|
+
this._localStream = null;
|
|
197
|
+
}
|
|
198
|
+
if (this._remoteStream) {
|
|
199
|
+
for (const track of this._remoteStream.getTracks()) {
|
|
200
|
+
track.stop();
|
|
201
|
+
}
|
|
202
|
+
this._remoteStream = null;
|
|
203
|
+
}
|
|
204
|
+
if (this._pc) {
|
|
205
|
+
this._pc.close();
|
|
206
|
+
this._pc = null;
|
|
207
|
+
}
|
|
208
|
+
this._pendingCandidates = [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// ── Group WebRTC Manager (mesh topology) ──────────────────────────
|
|
212
|
+
const MAX_GROUP_PARTICIPANTS = 8;
|
|
213
|
+
export class GroupWebRTCManager {
|
|
214
|
+
constructor(callbacks, iceServers) {
|
|
215
|
+
this._peers = new Map();
|
|
216
|
+
this._localStream = null;
|
|
217
|
+
this._screenStream = null;
|
|
218
|
+
this._callbacks = callbacks;
|
|
219
|
+
this._iceServers = getIceServers(iceServers);
|
|
220
|
+
}
|
|
221
|
+
get localStream() { return this._localStream; }
|
|
222
|
+
get peerCount() { return this._peers.size; }
|
|
223
|
+
getRemoteStream(userId) {
|
|
224
|
+
return this._peers.get(userId)?.remoteStream ?? null;
|
|
225
|
+
}
|
|
226
|
+
getRemoteStreams() {
|
|
227
|
+
const result = new Map();
|
|
228
|
+
for (const [uid, entry] of this._peers) {
|
|
229
|
+
if (entry.remoteStream)
|
|
230
|
+
result.set(uid, entry.remoteStream);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
/** Acquire local media — call once before adding peers */
|
|
235
|
+
async startLocalMedia(video = true, audio = true) {
|
|
236
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
237
|
+
const err = new Error('Media devices unavailable — HTTPS is required for calls');
|
|
238
|
+
this._callbacks.onError(err);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
this._localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio });
|
|
243
|
+
this._callbacks.onLocalStream(this._localStream);
|
|
244
|
+
if (!video) {
|
|
245
|
+
for (const vt of this._localStream.getVideoTracks())
|
|
246
|
+
vt.enabled = false;
|
|
247
|
+
}
|
|
248
|
+
return this._localStream;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
this._callbacks.onError(new Error(`Failed to access media: ${err.message}`));
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Create a peer connection for a remote user and optionally create an offer */
|
|
256
|
+
async addPeer(userId, isCaller) {
|
|
257
|
+
if (this._peers.has(userId))
|
|
258
|
+
return;
|
|
259
|
+
if (this._peers.size >= MAX_GROUP_PARTICIPANTS - 1) {
|
|
260
|
+
this._callbacks.onError(new Error(`Group call limit reached (${MAX_GROUP_PARTICIPANTS} participants)`));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const pc = new RTCPeerConnection({ iceServers: this._iceServers });
|
|
264
|
+
const entry = { pc, remoteStream: null, pendingCandidates: [] };
|
|
265
|
+
this._peers.set(userId, entry);
|
|
266
|
+
pc.onicecandidate = (event) => {
|
|
267
|
+
if (event.candidate) {
|
|
268
|
+
this._callbacks.onIceCandidate(userId, event.candidate);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
pc.ontrack = (event) => {
|
|
272
|
+
if (!entry.remoteStream) {
|
|
273
|
+
entry.remoteStream = new MediaStream();
|
|
274
|
+
this._callbacks.onRemoteStream(userId, entry.remoteStream);
|
|
275
|
+
}
|
|
276
|
+
entry.remoteStream.addTrack(event.track);
|
|
277
|
+
};
|
|
278
|
+
pc.onconnectionstatechange = () => {
|
|
279
|
+
this._callbacks.onConnectionStateChange(userId, pc.connectionState);
|
|
280
|
+
if (pc.connectionState === 'failed') {
|
|
281
|
+
this.removePeer(userId);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
// Add local tracks
|
|
285
|
+
if (this._localStream) {
|
|
286
|
+
for (const track of this._localStream.getTracks()) {
|
|
287
|
+
pc.addTrack(track, this._localStream);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (isCaller) {
|
|
291
|
+
const offer = await pc.createOffer();
|
|
292
|
+
await pc.setLocalDescription(offer);
|
|
293
|
+
return offer.sdp;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Remove and close a peer connection */
|
|
297
|
+
removePeer(userId) {
|
|
298
|
+
const entry = this._peers.get(userId);
|
|
299
|
+
if (!entry)
|
|
300
|
+
return;
|
|
301
|
+
entry.pc.close();
|
|
302
|
+
if (entry.remoteStream) {
|
|
303
|
+
for (const track of entry.remoteStream.getTracks())
|
|
304
|
+
track.stop();
|
|
305
|
+
}
|
|
306
|
+
this._peers.delete(userId);
|
|
307
|
+
this._callbacks.onRemoteStreamRemoved(userId);
|
|
308
|
+
}
|
|
309
|
+
/** Handle an SDP offer from a remote peer and return an answer */
|
|
310
|
+
async handleOffer(fromUserId, sdp) {
|
|
311
|
+
let entry = this._peers.get(fromUserId);
|
|
312
|
+
if (!entry) {
|
|
313
|
+
// Auto-create peer for the offerer
|
|
314
|
+
await this.addPeer(fromUserId, false);
|
|
315
|
+
entry = this._peers.get(fromUserId);
|
|
316
|
+
}
|
|
317
|
+
const { pc } = entry;
|
|
318
|
+
await pc.setRemoteDescription({ type: 'offer', sdp });
|
|
319
|
+
await this._flushPendingCandidates(fromUserId);
|
|
320
|
+
const answer = await pc.createAnswer();
|
|
321
|
+
await pc.setLocalDescription(answer);
|
|
322
|
+
return answer.sdp;
|
|
323
|
+
}
|
|
324
|
+
/** Handle an SDP answer from a remote peer */
|
|
325
|
+
async handleAnswer(fromUserId, sdp) {
|
|
326
|
+
const entry = this._peers.get(fromUserId);
|
|
327
|
+
if (!entry)
|
|
328
|
+
return;
|
|
329
|
+
await entry.pc.setRemoteDescription({ type: 'answer', sdp });
|
|
330
|
+
await this._flushPendingCandidates(fromUserId);
|
|
331
|
+
}
|
|
332
|
+
/** Add an ICE candidate for a specific peer */
|
|
333
|
+
async addIceCandidate(fromUserId, candidate, sdpMLineIndex, sdpMid) {
|
|
334
|
+
const init = {
|
|
335
|
+
candidate,
|
|
336
|
+
sdpMLineIndex: sdpMLineIndex ?? undefined,
|
|
337
|
+
sdpMid: sdpMid ?? undefined,
|
|
338
|
+
};
|
|
339
|
+
const entry = this._peers.get(fromUserId);
|
|
340
|
+
if (!entry)
|
|
341
|
+
return;
|
|
342
|
+
if (!entry.pc.remoteDescription) {
|
|
343
|
+
entry.pendingCandidates.push(init);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await entry.pc.addIceCandidate(init);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.warn(`[GroupWebRTC] Failed to add ICE candidate for ${fromUserId}:`, err);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async _flushPendingCandidates(userId) {
|
|
354
|
+
const entry = this._peers.get(userId);
|
|
355
|
+
if (!entry)
|
|
356
|
+
return;
|
|
357
|
+
for (const c of entry.pendingCandidates) {
|
|
358
|
+
try {
|
|
359
|
+
await entry.pc.addIceCandidate(c);
|
|
360
|
+
}
|
|
361
|
+
catch { }
|
|
362
|
+
}
|
|
363
|
+
entry.pendingCandidates = [];
|
|
364
|
+
}
|
|
365
|
+
/** Toggle audio for all peers */
|
|
366
|
+
setAudioEnabled(enabled) {
|
|
367
|
+
if (this._localStream) {
|
|
368
|
+
for (const track of this._localStream.getAudioTracks())
|
|
369
|
+
track.enabled = enabled;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Toggle video for all peers */
|
|
373
|
+
setVideoEnabled(enabled) {
|
|
374
|
+
if (this._localStream) {
|
|
375
|
+
for (const track of this._localStream.getVideoTracks())
|
|
376
|
+
track.enabled = enabled;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Replace camera track with screen share on all peers */
|
|
380
|
+
async startScreenShare() {
|
|
381
|
+
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
382
|
+
throw new Error('Screen sharing unavailable — HTTPS is required');
|
|
383
|
+
}
|
|
384
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
|
385
|
+
const screenTrack = stream.getVideoTracks()[0];
|
|
386
|
+
for (const [, entry] of this._peers) {
|
|
387
|
+
const sender = entry.pc.getSenders().find(s => s.track?.kind === 'video');
|
|
388
|
+
if (sender)
|
|
389
|
+
await sender.replaceTrack(screenTrack);
|
|
390
|
+
}
|
|
391
|
+
this._screenStream = stream;
|
|
392
|
+
screenTrack.onended = () => { this.stopScreenShare(); };
|
|
393
|
+
return stream;
|
|
394
|
+
}
|
|
395
|
+
/** Revert from screen share back to camera on all peers */
|
|
396
|
+
async stopScreenShare() {
|
|
397
|
+
if (this._screenStream) {
|
|
398
|
+
for (const track of this._screenStream.getTracks())
|
|
399
|
+
track.stop();
|
|
400
|
+
this._screenStream = null;
|
|
401
|
+
}
|
|
402
|
+
if (!this._localStream)
|
|
403
|
+
return;
|
|
404
|
+
const cameraTrack = this._localStream.getVideoTracks()[0] || null;
|
|
405
|
+
for (const [, entry] of this._peers) {
|
|
406
|
+
const sender = entry.pc.getSenders().find(s => s.track?.kind === 'video');
|
|
407
|
+
if (sender)
|
|
408
|
+
await sender.replaceTrack(cameraTrack);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** Clean up everything */
|
|
412
|
+
destroy() {
|
|
413
|
+
if (this._screenStream) {
|
|
414
|
+
for (const track of this._screenStream.getTracks())
|
|
415
|
+
track.stop();
|
|
416
|
+
this._screenStream = null;
|
|
417
|
+
}
|
|
418
|
+
const userIds = [...this._peers.keys()];
|
|
419
|
+
for (const userId of userIds) {
|
|
420
|
+
this.removePeer(userId);
|
|
421
|
+
}
|
|
422
|
+
if (this._localStream) {
|
|
423
|
+
for (const track of this._localStream.getTracks())
|
|
424
|
+
track.stop();
|
|
425
|
+
this._localStream = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
package/dist/shared/dom-shims.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Use the proper SSR HTMLElement shim so that @lit-labs/ssr's LitElementRenderer.renderAttributes()
|
|
2
|
+
// finds a working `element.attributes` property. A bare `class HTMLElement {}` leaves it undefined.
|
|
3
|
+
import { HTMLElement as SSRHTMLElement } from '@lit-labs/ssr-dom-shim';
|
|
1
4
|
/**
|
|
2
5
|
* Install DOM shims needed for SSR rendering of Lit/NuralyUI components.
|
|
3
6
|
* Consolidates the various partial shim implementations across the codebase.
|
|
@@ -6,8 +9,7 @@ export function installDomShims() {
|
|
|
6
9
|
const g = globalThis;
|
|
7
10
|
const noop = () => null;
|
|
8
11
|
if (!g.HTMLElement) {
|
|
9
|
-
g.HTMLElement =
|
|
10
|
-
};
|
|
12
|
+
g.HTMLElement = SSRHTMLElement;
|
|
11
13
|
}
|
|
12
14
|
if (!g.customElements) {
|
|
13
15
|
const registry = new Map();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server } from 'http';
|
|
2
|
+
export interface ShutdownConfig {
|
|
3
|
+
/** Max time to wait for connections to drain (ms). Default: 30000. */
|
|
4
|
+
timeout?: number;
|
|
5
|
+
/** Extra cleanup functions to run before exit. */
|
|
6
|
+
onShutdown?: () => Promise<void> | void;
|
|
7
|
+
}
|
|
8
|
+
export declare function setupGracefulShutdown(server: Server, config?: ShutdownConfig): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
export function setupGracefulShutdown(server, config) {
|
|
3
|
+
const timeout = config?.timeout ?? 30_000;
|
|
4
|
+
let isShuttingDown = false;
|
|
5
|
+
const shutdown = async (signal) => {
|
|
6
|
+
if (isShuttingDown)
|
|
7
|
+
return;
|
|
8
|
+
isShuttingDown = true;
|
|
9
|
+
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
10
|
+
// Stop accepting new connections
|
|
11
|
+
server.close(() => {
|
|
12
|
+
logger.info('All connections drained.');
|
|
13
|
+
});
|
|
14
|
+
// Force-close after timeout
|
|
15
|
+
const forceTimer = setTimeout(() => {
|
|
16
|
+
logger.warn('Shutdown timeout reached, forcing exit.', { timeout });
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}, timeout);
|
|
19
|
+
forceTimer.unref();
|
|
20
|
+
// Run custom cleanup
|
|
21
|
+
if (config?.onShutdown) {
|
|
22
|
+
try {
|
|
23
|
+
await config.onShutdown();
|
|
24
|
+
logger.info('Custom cleanup completed.');
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.error('Error during custom cleanup.', { error: err?.message });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Close idle keep-alive connections
|
|
31
|
+
server.closeIdleConnections();
|
|
32
|
+
logger.info('Graceful shutdown complete.');
|
|
33
|
+
};
|
|
34
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
35
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
export interface HealthCheckConfig {
|
|
3
|
+
/** Endpoint path. Default: '/__health'. */
|
|
4
|
+
path?: string;
|
|
5
|
+
/** App version string. Default: reads from package.json or 'unknown'. */
|
|
6
|
+
version?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createHealthCheckHandler(config?: HealthCheckConfig): (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const startTime = Date.now();
|
|
2
|
+
export function createHealthCheckHandler(config) {
|
|
3
|
+
const healthPath = config?.path || '/__health';
|
|
4
|
+
const version = config?.version || process.env.npm_package_version || 'unknown';
|
|
5
|
+
return (req, res, next) => {
|
|
6
|
+
if (req.url?.split('?')[0] !== healthPath)
|
|
7
|
+
return next();
|
|
8
|
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
|
9
|
+
const body = JSON.stringify({
|
|
10
|
+
status: 'ok',
|
|
11
|
+
uptime,
|
|
12
|
+
version,
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
memory: {
|
|
15
|
+
rss: Math.round(process.memoryUsage().rss / 1024 / 1024),
|
|
16
|
+
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
res.writeHead(200, {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
'Cache-Control': 'no-store',
|
|
22
|
+
});
|
|
23
|
+
res.end(body);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface LlmsTxtPage {
|
|
2
|
+
path: string;
|
|
3
|
+
hasLoader: boolean;
|
|
4
|
+
hasSubscribe: boolean;
|
|
5
|
+
hasSocket: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface LlmsTxtApiRoute {
|
|
8
|
+
path: string;
|
|
9
|
+
methods: string[];
|
|
10
|
+
}
|
|
11
|
+
interface LlmsTxtConfig {
|
|
12
|
+
title: string;
|
|
13
|
+
integrations: string[];
|
|
14
|
+
i18n?: {
|
|
15
|
+
locales: string[];
|
|
16
|
+
defaultLocale: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate the /llms.txt content for a LumenJS project.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateLlmsTxt(options: {
|
|
23
|
+
pages: LlmsTxtPage[];
|
|
24
|
+
apiRoutes: LlmsTxtApiRoute[];
|
|
25
|
+
config: LlmsTxtConfig;
|
|
26
|
+
}): string;
|
|
27
|
+
/**
|
|
28
|
+
* Detect exported HTTP methods from an API route file.
|
|
29
|
+
*/
|
|
30
|
+
export declare function detectApiMethods(filePath: string): string[];
|
|
31
|
+
export {};
|