@nuraly/lumenjs 0.1.2 → 0.1.4
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 +76 -235
- 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 +73 -0
- package/dist/auth/native-auth.js +293 -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 +98 -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/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 +110 -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 +17 -0
- package/dist/build/build-server.js +98 -0
- package/dist/build/build.js +52 -120
- package/dist/build/scan.d.ts +19 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -2
- package/dist/build/serve-loaders.js +128 -10
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +229 -14
- 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/context.d.ts +2 -0
- package/dist/db/context.js +9 -0
- package/dist/db/index.d.ts +23 -0
- package/dist/db/index.js +258 -0
- 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 +14 -0
- package/dist/dev-server/config.js +26 -9
- package/dist/dev-server/index-html.d.ts +3 -0
- package/dist/dev-server/index-html.js +18 -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.d.ts +0 -1
- package/dist/dev-server/plugins/vite-plugin-loaders.js +311 -42
- package/dist/dev-server/plugins/vite-plugin-routes.js +18 -6
- 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 +111 -2
- package/dist/dev-server/server.js +128 -12
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +107 -48
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +104 -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/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +125 -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 +587 -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 +17 -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 +75 -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.js +163 -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/router-data.d.ts +5 -0
- package/dist/runtime/router-data.js +121 -16
- package/dist/runtime/router-hydration.js +25 -0
- package/dist/runtime/router.d.ts +21 -1
- package/dist/runtime/router.js +221 -39
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +47 -0
- package/dist/runtime/webrtc.js +178 -0
- 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 +16 -0
- package/dist/shared/utils.d.ts +37 -7
- package/dist/shared/utils.js +175 -26
- 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 +116 -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 +20 -1
- package/templates/blog/api/posts.ts +6 -0
- package/templates/blog/data/migrations/001_init.sql +13 -0
- 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 +65 -0
- package/templates/blog/pages/posts/[slug].ts +60 -0
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +10 -0
- package/templates/dashboard/data/migrations/001_init.sql +13 -0
- 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 +72 -0
- 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
- package/templates/social/api/posts/[id].ts +14 -0
- package/templates/social/api/posts.ts +11 -0
- package/templates/social/api/profile/[username].ts +10 -0
- package/templates/social/api/upload.ts +19 -0
- package/templates/social/data/migrations/001_init.sql +78 -0
- package/templates/social/data/migrations/002_add_image_url.sql +1 -0
- package/templates/social/data/migrations/003_auth.sql +7 -0
- package/templates/social/docs/architecture.md +76 -0
- package/templates/social/docs/components.md +100 -0
- package/templates/social/docs/data.md +89 -0
- package/templates/social/docs/pages.md +96 -0
- package/templates/social/docs/theming.md +52 -0
- package/templates/social/lib/media.ts +130 -0
- package/templates/social/lumenjs.auth.ts +21 -0
- package/templates/social/lumenjs.config.ts +3 -0
- package/templates/social/package.json +5 -0
- package/templates/social/pages/_layout.ts +239 -0
- package/templates/social/pages/apps/[id].ts +173 -0
- package/templates/social/pages/apps/index.ts +116 -0
- package/templates/social/pages/auth/login.ts +92 -0
- package/templates/social/pages/bookmarks.ts +57 -0
- package/templates/social/pages/explore.ts +73 -0
- package/templates/social/pages/index.ts +351 -0
- package/templates/social/pages/messages.ts +298 -0
- package/templates/social/pages/new.ts +77 -0
- package/templates/social/pages/notifications.ts +73 -0
- package/templates/social/pages/post/[id].ts +124 -0
- package/templates/social/pages/profile/[username].ts +100 -0
- package/templates/social/pages/settings/accessibility.ts +153 -0
- package/templates/social/pages/settings/account.ts +260 -0
- package/templates/social/pages/settings/help.ts +141 -0
- package/templates/social/pages/settings/language.ts +103 -0
- package/templates/social/pages/settings/privacy.ts +183 -0
- package/templates/social/pages/settings/security.ts +133 -0
- package/templates/social/pages/settings.ts +185 -0
package/dist/runtime/router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchLoaderData, fetchLayoutLoaderData, render404 } from './router-data.js';
|
|
1
|
+
import { fetchLoaderData, fetchLayoutLoaderData, prefetchLoaderData, prefetchLayoutLoaderData, connectSubscribe, connectLayoutSubscribe, render404 } from './router-data.js';
|
|
2
2
|
import { hydrateInitialRoute } from './router-hydration.js';
|
|
3
|
-
import { getI18nConfig, getLocale, stripLocalePrefix, buildLocalePath } from './i18n.js';
|
|
3
|
+
import { getI18nConfig, getLocale, initI18n, stripLocalePrefix, buildLocalePath } from './i18n.js';
|
|
4
4
|
/**
|
|
5
5
|
* Simple client-side router for LumenJS pages.
|
|
6
6
|
* Handles popstate and link clicks for SPA navigation.
|
|
@@ -12,27 +12,79 @@ export class NkRouter {
|
|
|
12
12
|
this.outlet = null;
|
|
13
13
|
this.currentTag = null;
|
|
14
14
|
this.currentLayoutTags = [];
|
|
15
|
+
this.subscriptions = [];
|
|
15
16
|
this.params = {};
|
|
16
17
|
this.outlet = outlet;
|
|
18
|
+
this.siteTitle = document.title || 'LumenJS App';
|
|
17
19
|
this.routes = routes.map(r => ({
|
|
18
20
|
...r,
|
|
19
21
|
...this.compilePattern(r.path),
|
|
20
22
|
}));
|
|
23
|
+
// Initialize i18n from inlined data before any rendering
|
|
24
|
+
const i18nScript = document.getElementById('__nk_i18n__');
|
|
25
|
+
if (i18nScript) {
|
|
26
|
+
try {
|
|
27
|
+
const i18nData = JSON.parse(i18nScript.textContent || '');
|
|
28
|
+
initI18n(i18nData.config, i18nData.locale, i18nData.translations);
|
|
29
|
+
}
|
|
30
|
+
catch { /* ignore */ }
|
|
31
|
+
if (!hydrate)
|
|
32
|
+
i18nScript.remove();
|
|
33
|
+
}
|
|
21
34
|
window.addEventListener('popstate', () => {
|
|
22
35
|
const path = this.stripLocale(location.pathname);
|
|
23
36
|
this.navigate(path, false);
|
|
24
37
|
});
|
|
38
|
+
// Re-run loader when page is restored from bfcache (back/forward on mobile Safari, etc.)
|
|
39
|
+
window.addEventListener('pageshow', (e) => {
|
|
40
|
+
if (e.persisted) {
|
|
41
|
+
const path = this.stripLocale(location.pathname);
|
|
42
|
+
this.navigate(path, false);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
25
45
|
document.addEventListener('click', (e) => this.handleLinkClick(e));
|
|
46
|
+
window.__nk_navigate = (href) => {
|
|
47
|
+
const path = this.stripLocale(href);
|
|
48
|
+
if (this.matchRoute(path.split('?')[0])) {
|
|
49
|
+
this.navigate(path);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
window.location.href = href;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
window.__nk_prefetch = (href) => {
|
|
56
|
+
const path = this.stripLocale(href);
|
|
57
|
+
this.prefetch(path);
|
|
58
|
+
};
|
|
26
59
|
if (hydrate) {
|
|
27
60
|
hydrateInitialRoute(this.routes, this.outlet, (p) => this.matchRoute(p), (tag, layoutTags, params) => {
|
|
28
61
|
this.currentTag = tag;
|
|
29
62
|
this.currentLayoutTags = layoutTags;
|
|
30
63
|
this.params = params;
|
|
31
64
|
});
|
|
65
|
+
// Wire up SSE subscriptions after hydration
|
|
66
|
+
const path = this.stripLocale(location.pathname);
|
|
67
|
+
this.setupSubscriptions(path);
|
|
32
68
|
}
|
|
33
69
|
else {
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
// Initialize auth from inlined data before navigating (CSR path)
|
|
71
|
+
const authScript = document.getElementById('__nk_auth__');
|
|
72
|
+
if (authScript) {
|
|
73
|
+
import('@lumenjs/auth').then(({ initAuth }) => {
|
|
74
|
+
try {
|
|
75
|
+
initAuth(JSON.parse(authScript.textContent || ''));
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
authScript.remove();
|
|
79
|
+
}).catch(() => { }).finally(() => {
|
|
80
|
+
const path = this.stripLocale(location.pathname);
|
|
81
|
+
this.navigate(path, false);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const path = this.stripLocale(location.pathname);
|
|
86
|
+
this.navigate(path, false);
|
|
87
|
+
}
|
|
36
88
|
}
|
|
37
89
|
}
|
|
38
90
|
compilePattern(path) {
|
|
@@ -43,7 +95,15 @@ export class NkRouter {
|
|
|
43
95
|
});
|
|
44
96
|
return { pattern: new RegExp(`^${pattern}$`), paramNames };
|
|
45
97
|
}
|
|
46
|
-
|
|
98
|
+
cleanupSubscriptions() {
|
|
99
|
+
for (const es of this.subscriptions) {
|
|
100
|
+
es.close();
|
|
101
|
+
}
|
|
102
|
+
this.subscriptions = [];
|
|
103
|
+
}
|
|
104
|
+
async navigate(fullPath, pushState = true) {
|
|
105
|
+
this.cleanupSubscriptions();
|
|
106
|
+
const pathname = fullPath.split('?')[0];
|
|
47
107
|
const match = this.matchRoute(pathname);
|
|
48
108
|
if (!match) {
|
|
49
109
|
if (this.outlet)
|
|
@@ -53,49 +113,74 @@ export class NkRouter {
|
|
|
53
113
|
return;
|
|
54
114
|
}
|
|
55
115
|
if (pushState) {
|
|
56
|
-
const localePath = this.withLocale(
|
|
116
|
+
const localePath = this.withLocale(fullPath);
|
|
57
117
|
history.pushState(null, '', localePath);
|
|
118
|
+
window.scrollTo(0, 0);
|
|
58
119
|
}
|
|
59
120
|
this.params = match.params;
|
|
60
|
-
//
|
|
61
|
-
if (match.route.
|
|
62
|
-
await match.route.load();
|
|
63
|
-
}
|
|
64
|
-
// Load layout components
|
|
65
|
-
const layouts = match.route.layouts || [];
|
|
66
|
-
for (const layout of layouts) {
|
|
67
|
-
if (layout.load && !customElements.get(layout.tagName)) {
|
|
68
|
-
await layout.load();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Fetch loader data for page
|
|
72
|
-
let loaderData = undefined;
|
|
73
|
-
if (match.route.hasLoader) {
|
|
121
|
+
// Auth guard: SPA-navigate unauthenticated users to login page
|
|
122
|
+
if (match.route.__nk_has_auth) {
|
|
74
123
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
const { isAuthenticated } = await import('@lumenjs/auth');
|
|
125
|
+
if (!isAuthenticated()) {
|
|
126
|
+
const loginPath = '/auth/login';
|
|
127
|
+
const loginUrl = `${loginPath}?returnTo=${encodeURIComponent(pathname)}`;
|
|
128
|
+
history.pushState(null, '', loginUrl);
|
|
129
|
+
this.navigate(loginPath, false);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
79
132
|
}
|
|
133
|
+
catch { }
|
|
80
134
|
}
|
|
81
|
-
|
|
82
|
-
|
|
135
|
+
const layouts = match.route.layouts || [];
|
|
136
|
+
// Load all component JS chunks in parallel
|
|
137
|
+
await Promise.all([
|
|
138
|
+
match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
|
|
139
|
+
...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
|
|
140
|
+
]);
|
|
141
|
+
// Fetch all loader data in parallel
|
|
142
|
+
const loaderPromises = [
|
|
143
|
+
match.route.hasLoader
|
|
144
|
+
? fetchLoaderData(pathname, match.params).catch(err => { console.error('[NkRouter] Loader fetch failed:', err); return undefined; })
|
|
145
|
+
: Promise.resolve(undefined),
|
|
146
|
+
...layouts.map(layout => layout.hasLoader
|
|
147
|
+
? fetchLayoutLoaderData(layout.loaderPath || '').catch(err => { console.error('[NkRouter] Layout loader fetch failed:', err); return undefined; })
|
|
148
|
+
: Promise.resolve(undefined)),
|
|
149
|
+
];
|
|
150
|
+
const [loaderData, ...layoutDataList] = await Promise.all(loaderPromises);
|
|
151
|
+
this.renderRoute(match.route, loaderData, layouts, layoutDataList);
|
|
152
|
+
// Update document.title and announce route change for screen readers
|
|
153
|
+
this.updatePageMeta(match.route, loaderData);
|
|
154
|
+
// Set up SSE subscriptions
|
|
155
|
+
this.setupSubscriptions(pathname);
|
|
156
|
+
}
|
|
157
|
+
setupSubscriptions(pathname) {
|
|
158
|
+
const match = this.matchRoute(pathname);
|
|
159
|
+
if (!match)
|
|
160
|
+
return;
|
|
161
|
+
const layouts = match.route.layouts || [];
|
|
162
|
+
// Page subscription
|
|
163
|
+
if (match.route.hasSubscribe) {
|
|
164
|
+
const es = connectSubscribe(pathname, match.params);
|
|
165
|
+
es.onmessage = (e) => {
|
|
166
|
+
const pageEl = this.findPageElement(match.route.tagName);
|
|
167
|
+
if (pageEl)
|
|
168
|
+
pageEl.liveData = JSON.parse(e.data);
|
|
169
|
+
};
|
|
170
|
+
this.subscriptions.push(es);
|
|
171
|
+
}
|
|
172
|
+
// Layout subscriptions
|
|
83
173
|
for (const layout of layouts) {
|
|
84
|
-
if (layout.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
layoutDataList.push(undefined);
|
|
174
|
+
if (layout.hasSubscribe) {
|
|
175
|
+
const es = connectLayoutSubscribe(layout.loaderPath || '');
|
|
176
|
+
es.onmessage = (e) => {
|
|
177
|
+
const layoutEl = this.outlet?.querySelector(layout.tagName);
|
|
178
|
+
if (layoutEl)
|
|
179
|
+
layoutEl.liveData = JSON.parse(e.data);
|
|
180
|
+
};
|
|
181
|
+
this.subscriptions.push(es);
|
|
96
182
|
}
|
|
97
183
|
}
|
|
98
|
-
this.renderRoute(match.route, loaderData, layouts, layoutDataList);
|
|
99
184
|
}
|
|
100
185
|
matchRoute(pathname) {
|
|
101
186
|
for (const route of this.routes) {
|
|
@@ -146,6 +231,7 @@ export class NkRouter {
|
|
|
146
231
|
}
|
|
147
232
|
if (layoutDataList && layoutDataList[i] !== undefined) {
|
|
148
233
|
layoutEl.loaderData = layoutDataList[i];
|
|
234
|
+
this.spreadData(layoutEl, layoutDataList[i]);
|
|
149
235
|
}
|
|
150
236
|
parentEl = layoutEl;
|
|
151
237
|
}
|
|
@@ -170,12 +256,14 @@ export class NkRouter {
|
|
|
170
256
|
const outerLayout = document.createElement(layouts[0].tagName);
|
|
171
257
|
if (layoutDataList[0] !== undefined) {
|
|
172
258
|
outerLayout.loaderData = layoutDataList[0];
|
|
259
|
+
this.spreadData(outerLayout, layoutDataList[0]);
|
|
173
260
|
}
|
|
174
261
|
let current = outerLayout;
|
|
175
262
|
for (let i = 1; i < layouts.length; i++) {
|
|
176
263
|
const inner = document.createElement(layouts[i].tagName);
|
|
177
264
|
if (layoutDataList[i] !== undefined) {
|
|
178
265
|
inner.loaderData = layoutDataList[i];
|
|
266
|
+
this.spreadData(inner, layoutDataList[i]);
|
|
179
267
|
}
|
|
180
268
|
current.appendChild(inner);
|
|
181
269
|
current = inner;
|
|
@@ -184,6 +272,19 @@ export class NkRouter {
|
|
|
184
272
|
current.appendChild(pageEl);
|
|
185
273
|
return outerLayout;
|
|
186
274
|
}
|
|
275
|
+
/** Spread loader data as individual properties on an element. */
|
|
276
|
+
spreadData(el, data) {
|
|
277
|
+
if (data && typeof data === 'object') {
|
|
278
|
+
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype',
|
|
279
|
+
'innerHTML', 'outerHTML', 'textContent',
|
|
280
|
+
'render', 'connectedCallback', 'disconnectedCallback']);
|
|
281
|
+
for (const [key, value] of Object.entries(data)) {
|
|
282
|
+
if (!BLOCKED.has(key)) {
|
|
283
|
+
el[key] = value;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
187
288
|
createPageElement(route, loaderData) {
|
|
188
289
|
const el = document.createElement(route.tagName);
|
|
189
290
|
for (const [key, value] of Object.entries(this.params)) {
|
|
@@ -191,9 +292,56 @@ export class NkRouter {
|
|
|
191
292
|
}
|
|
192
293
|
if (loaderData !== undefined) {
|
|
193
294
|
el.loaderData = loaderData;
|
|
295
|
+
this.spreadData(el, loaderData);
|
|
194
296
|
}
|
|
195
297
|
return el;
|
|
196
298
|
}
|
|
299
|
+
findPageElement(tagName) {
|
|
300
|
+
if (!this.outlet)
|
|
301
|
+
return null;
|
|
302
|
+
return this.outlet.querySelector(tagName) ?? this.outlet.querySelector(`${tagName}:last-child`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Resolve the page title from the route's meta export and update
|
|
306
|
+
* document.title, the aria-live announcer, and focus.
|
|
307
|
+
*/
|
|
308
|
+
async updatePageMeta(route, loaderData) {
|
|
309
|
+
let pageTitle;
|
|
310
|
+
if (route.hasMeta && route.load) {
|
|
311
|
+
try {
|
|
312
|
+
const mod = await route.load();
|
|
313
|
+
if (mod) {
|
|
314
|
+
let meta;
|
|
315
|
+
if (typeof mod.meta === 'function') {
|
|
316
|
+
meta = mod.meta({ data: loaderData, params: this.params });
|
|
317
|
+
}
|
|
318
|
+
else if (mod.meta && typeof mod.meta === 'object') {
|
|
319
|
+
meta = mod.meta;
|
|
320
|
+
}
|
|
321
|
+
if (meta?.title) {
|
|
322
|
+
pageTitle = `${meta.title} | ${this.siteTitle}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { /* fall back to site title */ }
|
|
327
|
+
}
|
|
328
|
+
const title = pageTitle || this.siteTitle;
|
|
329
|
+
document.title = title;
|
|
330
|
+
// Announce route change to screen readers
|
|
331
|
+
const announcer = document.getElementById('nk-route-announcer');
|
|
332
|
+
if (announcer) {
|
|
333
|
+
announcer.textContent = '';
|
|
334
|
+
// Use a microtask delay so aria-live picks up the change
|
|
335
|
+
requestAnimationFrame(() => { announcer.textContent = title; });
|
|
336
|
+
}
|
|
337
|
+
// Move focus to the router outlet for keyboard/screen reader users
|
|
338
|
+
if (this.outlet) {
|
|
339
|
+
if (!this.outlet.hasAttribute('tabindex')) {
|
|
340
|
+
this.outlet.setAttribute('tabindex', '-1');
|
|
341
|
+
}
|
|
342
|
+
this.outlet.focus({ preventScroll: true });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
197
345
|
handleLinkClick(event) {
|
|
198
346
|
const path = event.composedPath();
|
|
199
347
|
const anchor = path.find((el) => el instanceof HTMLElement && el.tagName === 'A');
|
|
@@ -202,9 +350,27 @@ export class NkRouter {
|
|
|
202
350
|
const href = anchor.getAttribute('href');
|
|
203
351
|
if (!href || href.startsWith('http') || href.startsWith('#') || anchor.hasAttribute('target'))
|
|
204
352
|
return;
|
|
353
|
+
// Allow modifier-key clicks to behave normally (Ctrl+Click = new tab, etc.)
|
|
354
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
355
|
+
return;
|
|
205
356
|
event.preventDefault();
|
|
206
357
|
this.navigate(this.stripLocale(href));
|
|
207
358
|
}
|
|
359
|
+
async prefetch(fullPath) {
|
|
360
|
+
const pathname = fullPath.split('?')[0];
|
|
361
|
+
const match = this.matchRoute(pathname);
|
|
362
|
+
if (!match)
|
|
363
|
+
return;
|
|
364
|
+
const layouts = match.route.layouts || [];
|
|
365
|
+
await Promise.all([
|
|
366
|
+
// Preload component JS chunks
|
|
367
|
+
match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
|
|
368
|
+
...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
|
|
369
|
+
// Prefetch loader data (cached)
|
|
370
|
+
match.route.hasLoader ? prefetchLoaderData(pathname, match.params).catch(() => { }) : undefined,
|
|
371
|
+
...layouts.map(l => l.hasLoader ? prefetchLayoutLoaderData(l.loaderPath || '').catch(() => { }) : undefined),
|
|
372
|
+
]);
|
|
373
|
+
}
|
|
208
374
|
/** Strip locale prefix from a path for internal route matching. */
|
|
209
375
|
stripLocale(path) {
|
|
210
376
|
const config = getI18nConfig();
|
|
@@ -216,3 +382,19 @@ export class NkRouter {
|
|
|
216
382
|
return config ? buildLocalePath(getLocale(), path) : path;
|
|
217
383
|
}
|
|
218
384
|
}
|
|
385
|
+
/** Navigate via the client-side router. Falls back to full reload for unknown routes. */
|
|
386
|
+
export function navigate(href) {
|
|
387
|
+
const nav = window.__nk_navigate;
|
|
388
|
+
if (nav) {
|
|
389
|
+
nav(href);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
window.location.href = href;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Programmatically prefetch a route's JS chunks and loader data. */
|
|
396
|
+
export function prefetch(href) {
|
|
397
|
+
const pf = window.__nk_prefetch;
|
|
398
|
+
if (pf)
|
|
399
|
+
pf(href);
|
|
400
|
+
}
|
|
@@ -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,47 @@
|
|
|
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
|
+
constructor(callbacks: WebRTCCallbacks, iceServers?: RTCIceServer[]);
|
|
21
|
+
private _createPeerConnection;
|
|
22
|
+
get localStream(): MediaStream | null;
|
|
23
|
+
get remoteStream(): MediaStream | null;
|
|
24
|
+
get role(): CallRole;
|
|
25
|
+
get connectionState(): RTCPeerConnectionState | null;
|
|
26
|
+
/** Acquire local media (camera/mic) and add tracks to the peer connection */
|
|
27
|
+
startLocalMedia(video?: boolean, audio?: boolean): Promise<MediaStream>;
|
|
28
|
+
/** Create an SDP offer (caller side) */
|
|
29
|
+
createOffer(): Promise<string>;
|
|
30
|
+
/** Handle received SDP offer and create answer (callee side) */
|
|
31
|
+
handleOffer(sdp: string): Promise<string>;
|
|
32
|
+
/** Handle received SDP answer (caller side) */
|
|
33
|
+
handleAnswer(sdp: string): Promise<void>;
|
|
34
|
+
/** Add a received ICE candidate */
|
|
35
|
+
addIceCandidate(candidate: string, sdpMLineIndex: number | null, sdpMid: string | null): Promise<void>;
|
|
36
|
+
private _flushPendingCandidates;
|
|
37
|
+
/** Toggle audio mute */
|
|
38
|
+
setAudioEnabled(enabled: boolean): void;
|
|
39
|
+
/** Toggle video */
|
|
40
|
+
setVideoEnabled(enabled: boolean): void;
|
|
41
|
+
/** Replace video track with screen share */
|
|
42
|
+
startScreenShare(): Promise<MediaStream>;
|
|
43
|
+
/** Revert from screen share back to camera */
|
|
44
|
+
stopScreenShare(): Promise<void>;
|
|
45
|
+
/** Clean up everything */
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebRTC peer connection manager.
|
|
3
|
+
* Wraps RTCPeerConnection and wires to the LumenJS communication SDK signaling.
|
|
4
|
+
*/
|
|
5
|
+
const ICE_SERVERS = [
|
|
6
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
7
|
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
8
|
+
];
|
|
9
|
+
export class WebRTCManager {
|
|
10
|
+
constructor(callbacks, iceServers) {
|
|
11
|
+
this._pc = null;
|
|
12
|
+
this._localStream = null;
|
|
13
|
+
this._remoteStream = null;
|
|
14
|
+
this._pendingCandidates = [];
|
|
15
|
+
this._role = 'caller';
|
|
16
|
+
this._callbacks = callbacks;
|
|
17
|
+
this._createPeerConnection(iceServers || ICE_SERVERS);
|
|
18
|
+
}
|
|
19
|
+
_createPeerConnection(iceServers) {
|
|
20
|
+
this._pc = new RTCPeerConnection({ iceServers });
|
|
21
|
+
this._pc.onicecandidate = (event) => {
|
|
22
|
+
if (event.candidate) {
|
|
23
|
+
this._callbacks.onIceCandidate(event.candidate);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
this._pc.ontrack = (event) => {
|
|
27
|
+
if (!this._remoteStream) {
|
|
28
|
+
this._remoteStream = new MediaStream();
|
|
29
|
+
this._callbacks.onRemoteStream(this._remoteStream);
|
|
30
|
+
}
|
|
31
|
+
this._remoteStream.addTrack(event.track);
|
|
32
|
+
};
|
|
33
|
+
this._pc.onconnectionstatechange = () => {
|
|
34
|
+
if (this._pc) {
|
|
35
|
+
this._callbacks.onConnectionStateChange(this._pc.connectionState);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
get localStream() { return this._localStream; }
|
|
40
|
+
get remoteStream() { return this._remoteStream; }
|
|
41
|
+
get role() { return this._role; }
|
|
42
|
+
get connectionState() { return this._pc?.connectionState ?? null; }
|
|
43
|
+
/** Acquire local media (camera/mic) and add tracks to the peer connection */
|
|
44
|
+
async startLocalMedia(video = true, audio = true) {
|
|
45
|
+
try {
|
|
46
|
+
this._localStream = await navigator.mediaDevices.getUserMedia({ video, audio });
|
|
47
|
+
this._callbacks.onLocalStream(this._localStream);
|
|
48
|
+
for (const track of this._localStream.getTracks()) {
|
|
49
|
+
this._pc?.addTrack(track, this._localStream);
|
|
50
|
+
}
|
|
51
|
+
return this._localStream;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
this._callbacks.onError(new Error(`Failed to access media: ${err.message}`));
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Create an SDP offer (caller side) */
|
|
59
|
+
async createOffer() {
|
|
60
|
+
this._role = 'caller';
|
|
61
|
+
if (!this._pc)
|
|
62
|
+
throw new Error('No peer connection');
|
|
63
|
+
const offer = await this._pc.createOffer();
|
|
64
|
+
await this._pc.setLocalDescription(offer);
|
|
65
|
+
return offer.sdp;
|
|
66
|
+
}
|
|
67
|
+
/** Handle received SDP offer and create answer (callee side) */
|
|
68
|
+
async handleOffer(sdp) {
|
|
69
|
+
this._role = 'callee';
|
|
70
|
+
if (!this._pc)
|
|
71
|
+
throw new Error('No peer connection');
|
|
72
|
+
await this._pc.setRemoteDescription({ type: 'offer', sdp });
|
|
73
|
+
// Flush pending ICE candidates
|
|
74
|
+
await this._flushPendingCandidates();
|
|
75
|
+
const answer = await this._pc.createAnswer();
|
|
76
|
+
await this._pc.setLocalDescription(answer);
|
|
77
|
+
return answer.sdp;
|
|
78
|
+
}
|
|
79
|
+
/** Handle received SDP answer (caller side) */
|
|
80
|
+
async handleAnswer(sdp) {
|
|
81
|
+
if (!this._pc)
|
|
82
|
+
throw new Error('No peer connection');
|
|
83
|
+
await this._pc.setRemoteDescription({ type: 'answer', sdp });
|
|
84
|
+
await this._flushPendingCandidates();
|
|
85
|
+
}
|
|
86
|
+
/** Add a received ICE candidate */
|
|
87
|
+
async addIceCandidate(candidate, sdpMLineIndex, sdpMid) {
|
|
88
|
+
const init = {
|
|
89
|
+
candidate,
|
|
90
|
+
sdpMLineIndex: sdpMLineIndex ?? undefined,
|
|
91
|
+
sdpMid: sdpMid ?? undefined,
|
|
92
|
+
};
|
|
93
|
+
if (!this._pc?.remoteDescription) {
|
|
94
|
+
// Queue candidates until remote description is set
|
|
95
|
+
this._pendingCandidates.push(init);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await this._pc.addIceCandidate(init);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn('[WebRTC] Failed to add ICE candidate:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async _flushPendingCandidates() {
|
|
106
|
+
for (const c of this._pendingCandidates) {
|
|
107
|
+
try {
|
|
108
|
+
await this._pc?.addIceCandidate(c);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
this._pendingCandidates = [];
|
|
113
|
+
}
|
|
114
|
+
/** Toggle audio mute */
|
|
115
|
+
setAudioEnabled(enabled) {
|
|
116
|
+
if (this._localStream) {
|
|
117
|
+
for (const track of this._localStream.getAudioTracks()) {
|
|
118
|
+
track.enabled = enabled;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Toggle video */
|
|
123
|
+
setVideoEnabled(enabled) {
|
|
124
|
+
if (this._localStream) {
|
|
125
|
+
for (const track of this._localStream.getVideoTracks()) {
|
|
126
|
+
track.enabled = enabled;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Replace video track with screen share */
|
|
131
|
+
async startScreenShare() {
|
|
132
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
|
133
|
+
const screenTrack = stream.getVideoTracks()[0];
|
|
134
|
+
if (this._pc && this._localStream) {
|
|
135
|
+
const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
|
|
136
|
+
if (sender) {
|
|
137
|
+
await sender.replaceTrack(screenTrack);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// When user stops sharing via browser UI
|
|
141
|
+
screenTrack.onended = () => {
|
|
142
|
+
this.stopScreenShare();
|
|
143
|
+
};
|
|
144
|
+
return stream;
|
|
145
|
+
}
|
|
146
|
+
/** Revert from screen share back to camera */
|
|
147
|
+
async stopScreenShare() {
|
|
148
|
+
if (this._localStream && this._pc) {
|
|
149
|
+
const cameraTrack = this._localStream.getVideoTracks()[0];
|
|
150
|
+
if (cameraTrack) {
|
|
151
|
+
const sender = this._pc.getSenders().find(s => s.track?.kind === 'video');
|
|
152
|
+
if (sender) {
|
|
153
|
+
await sender.replaceTrack(cameraTrack);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Clean up everything */
|
|
159
|
+
destroy() {
|
|
160
|
+
if (this._localStream) {
|
|
161
|
+
for (const track of this._localStream.getTracks()) {
|
|
162
|
+
track.stop();
|
|
163
|
+
}
|
|
164
|
+
this._localStream = null;
|
|
165
|
+
}
|
|
166
|
+
if (this._remoteStream) {
|
|
167
|
+
for (const track of this._remoteStream.getTracks()) {
|
|
168
|
+
track.stop();
|
|
169
|
+
}
|
|
170
|
+
this._remoteStream = null;
|
|
171
|
+
}
|
|
172
|
+
if (this._pc) {
|
|
173
|
+
this._pc.close();
|
|
174
|
+
this._pc = null;
|
|
175
|
+
}
|
|
176
|
+
this._pendingCandidates = [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -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;
|