@nuraly/lumenjs 0.1.3 → 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 +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 +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 +48 -120
- package/dist/build/scan.d.ts +17 -0
- package/dist/build/scan.js +76 -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 +218 -15
- 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 +23 -20
- 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.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +15 -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 +111 -2
- package/dist/dev-server/server.js +127 -13
- 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 +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +25 -0
- package/dist/runtime/router.d.ts +16 -1
- package/dist/runtime/router.js +188 -42
- 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 +14 -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 +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 +19 -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
- 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, connectSubscribe, connectLayoutSubscribe, 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.
|
|
@@ -15,25 +15,76 @@ export class NkRouter {
|
|
|
15
15
|
this.subscriptions = [];
|
|
16
16
|
this.params = {};
|
|
17
17
|
this.outlet = outlet;
|
|
18
|
+
this.siteTitle = document.title || 'LumenJS App';
|
|
18
19
|
this.routes = routes.map(r => ({
|
|
19
20
|
...r,
|
|
20
21
|
...this.compilePattern(r.path),
|
|
21
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
|
+
}
|
|
22
34
|
window.addEventListener('popstate', () => {
|
|
23
35
|
const path = this.stripLocale(location.pathname);
|
|
24
36
|
this.navigate(path, false);
|
|
25
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
|
+
});
|
|
26
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
|
+
};
|
|
27
59
|
if (hydrate) {
|
|
28
60
|
hydrateInitialRoute(this.routes, this.outlet, (p) => this.matchRoute(p), (tag, layoutTags, params) => {
|
|
29
61
|
this.currentTag = tag;
|
|
30
62
|
this.currentLayoutTags = layoutTags;
|
|
31
63
|
this.params = params;
|
|
32
64
|
});
|
|
65
|
+
// Wire up SSE subscriptions after hydration
|
|
66
|
+
const path = this.stripLocale(location.pathname);
|
|
67
|
+
this.setupSubscriptions(path);
|
|
33
68
|
}
|
|
34
69
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
88
|
}
|
|
38
89
|
}
|
|
39
90
|
compilePattern(path) {
|
|
@@ -50,8 +101,9 @@ export class NkRouter {
|
|
|
50
101
|
}
|
|
51
102
|
this.subscriptions = [];
|
|
52
103
|
}
|
|
53
|
-
async navigate(
|
|
104
|
+
async navigate(fullPath, pushState = true) {
|
|
54
105
|
this.cleanupSubscriptions();
|
|
106
|
+
const pathname = fullPath.split('?')[0];
|
|
55
107
|
const match = this.matchRoute(pathname);
|
|
56
108
|
if (!match) {
|
|
57
109
|
if (this.outlet)
|
|
@@ -61,51 +113,53 @@ export class NkRouter {
|
|
|
61
113
|
return;
|
|
62
114
|
}
|
|
63
115
|
if (pushState) {
|
|
64
|
-
const localePath = this.withLocale(
|
|
116
|
+
const localePath = this.withLocale(fullPath);
|
|
65
117
|
history.pushState(null, '', localePath);
|
|
66
118
|
window.scrollTo(0, 0);
|
|
67
119
|
}
|
|
68
120
|
this.params = match.params;
|
|
69
|
-
//
|
|
70
|
-
if (match.route.
|
|
71
|
-
await match.route.load();
|
|
72
|
-
}
|
|
73
|
-
// Load layout components
|
|
74
|
-
const layouts = match.route.layouts || [];
|
|
75
|
-
for (const layout of layouts) {
|
|
76
|
-
if (layout.load && !customElements.get(layout.tagName)) {
|
|
77
|
-
await layout.load();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Fetch loader data for page
|
|
81
|
-
let loaderData = undefined;
|
|
82
|
-
if (match.route.hasLoader) {
|
|
121
|
+
// Auth guard: SPA-navigate unauthenticated users to login page
|
|
122
|
+
if (match.route.__nk_has_auth) {
|
|
83
123
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const layoutDataList = [];
|
|
92
|
-
for (const layout of layouts) {
|
|
93
|
-
if (layout.hasLoader) {
|
|
94
|
-
try {
|
|
95
|
-
const data = await fetchLayoutLoaderData(layout.loaderPath || '');
|
|
96
|
-
layoutDataList.push(data);
|
|
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;
|
|
97
131
|
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.error('[NkRouter] Layout loader fetch failed:', err);
|
|
100
|
-
layoutDataList.push(undefined);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
layoutDataList.push(undefined);
|
|
105
132
|
}
|
|
133
|
+
catch { }
|
|
106
134
|
}
|
|
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);
|
|
107
151
|
this.renderRoute(match.route, loaderData, layouts, layoutDataList);
|
|
108
|
-
//
|
|
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
|
|
109
163
|
if (match.route.hasSubscribe) {
|
|
110
164
|
const es = connectSubscribe(pathname, match.params);
|
|
111
165
|
es.onmessage = (e) => {
|
|
@@ -115,7 +169,7 @@ export class NkRouter {
|
|
|
115
169
|
};
|
|
116
170
|
this.subscriptions.push(es);
|
|
117
171
|
}
|
|
118
|
-
//
|
|
172
|
+
// Layout subscriptions
|
|
119
173
|
for (const layout of layouts) {
|
|
120
174
|
if (layout.hasSubscribe) {
|
|
121
175
|
const es = connectLayoutSubscribe(layout.loaderPath || '');
|
|
@@ -177,6 +231,7 @@ export class NkRouter {
|
|
|
177
231
|
}
|
|
178
232
|
if (layoutDataList && layoutDataList[i] !== undefined) {
|
|
179
233
|
layoutEl.loaderData = layoutDataList[i];
|
|
234
|
+
this.spreadData(layoutEl, layoutDataList[i]);
|
|
180
235
|
}
|
|
181
236
|
parentEl = layoutEl;
|
|
182
237
|
}
|
|
@@ -201,12 +256,14 @@ export class NkRouter {
|
|
|
201
256
|
const outerLayout = document.createElement(layouts[0].tagName);
|
|
202
257
|
if (layoutDataList[0] !== undefined) {
|
|
203
258
|
outerLayout.loaderData = layoutDataList[0];
|
|
259
|
+
this.spreadData(outerLayout, layoutDataList[0]);
|
|
204
260
|
}
|
|
205
261
|
let current = outerLayout;
|
|
206
262
|
for (let i = 1; i < layouts.length; i++) {
|
|
207
263
|
const inner = document.createElement(layouts[i].tagName);
|
|
208
264
|
if (layoutDataList[i] !== undefined) {
|
|
209
265
|
inner.loaderData = layoutDataList[i];
|
|
266
|
+
this.spreadData(inner, layoutDataList[i]);
|
|
210
267
|
}
|
|
211
268
|
current.appendChild(inner);
|
|
212
269
|
current = inner;
|
|
@@ -215,6 +272,19 @@ export class NkRouter {
|
|
|
215
272
|
current.appendChild(pageEl);
|
|
216
273
|
return outerLayout;
|
|
217
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
|
+
}
|
|
218
288
|
createPageElement(route, loaderData) {
|
|
219
289
|
const el = document.createElement(route.tagName);
|
|
220
290
|
for (const [key, value] of Object.entries(this.params)) {
|
|
@@ -222,6 +292,7 @@ export class NkRouter {
|
|
|
222
292
|
}
|
|
223
293
|
if (loaderData !== undefined) {
|
|
224
294
|
el.loaderData = loaderData;
|
|
295
|
+
this.spreadData(el, loaderData);
|
|
225
296
|
}
|
|
226
297
|
return el;
|
|
227
298
|
}
|
|
@@ -230,6 +301,47 @@ export class NkRouter {
|
|
|
230
301
|
return null;
|
|
231
302
|
return this.outlet.querySelector(tagName) ?? this.outlet.querySelector(`${tagName}:last-child`);
|
|
232
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
|
+
}
|
|
233
345
|
handleLinkClick(event) {
|
|
234
346
|
const path = event.composedPath();
|
|
235
347
|
const anchor = path.find((el) => el instanceof HTMLElement && el.tagName === 'A');
|
|
@@ -238,9 +350,27 @@ export class NkRouter {
|
|
|
238
350
|
const href = anchor.getAttribute('href');
|
|
239
351
|
if (!href || href.startsWith('http') || href.startsWith('#') || anchor.hasAttribute('target'))
|
|
240
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;
|
|
241
356
|
event.preventDefault();
|
|
242
357
|
this.navigate(this.stripLocale(href));
|
|
243
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
|
+
}
|
|
244
374
|
/** Strip locale prefix from a path for internal route matching. */
|
|
245
375
|
stripLocale(path) {
|
|
246
376
|
const config = getI18nConfig();
|
|
@@ -252,3 +382,19 @@ export class NkRouter {
|
|
|
252
382
|
return config ? buildLocalePath(getLocale(), path) : path;
|
|
253
383
|
}
|
|
254
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;
|
|
@@ -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
|
+
}
|