nodefony 7.0.2 → 10.0.0-alpha.2
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/.ai/symbols.json +36135 -0
- package/LICENSE +544 -0
- package/README.md +316 -694
- package/bin/nodefony +2183 -85
- package/dist/client/Container.js +367 -0
- package/dist/client/Event.js +198 -0
- package/dist/client/Service.js +344 -0
- package/dist/client/Tools.js +162 -0
- package/dist/client/client/ClientKernel.js +276 -0
- package/dist/client/client/angular/index.js +352 -0
- package/dist/client/client/announce.js +212 -0
- package/dist/client/client/debugbar/DebugBar.js +1624 -0
- package/dist/client/client/debugbar/format.js +103 -0
- package/dist/client/client/debugbar/hmr.js +37 -0
- package/dist/client/client/debugbar/index.js +29 -0
- package/dist/client/client/debugbar/model.js +133 -0
- package/dist/client/client/debugbar/network.js +173 -0
- package/dist/client/client/debugbar/profile.js +114 -0
- package/dist/client/client/index.js +34 -0
- package/dist/client/client/react/index.js +268 -0
- package/dist/client/client/realtime/AdaptiveRate.js +240 -0
- package/dist/client/client/realtime/BrowserWsTransport.js +57 -0
- package/dist/client/client/realtime/RealtimeClient.js +898 -0
- package/dist/client/client/realtime/localEvents.js +56 -0
- package/dist/client/client/realtime/notice.js +125 -0
- package/dist/client/client/realtime/observe.js +280 -0
- package/dist/client/client/roles/index.js +3 -0
- package/dist/client/client/roles/registry.js +97 -0
- package/dist/client/client/roles/roles.js +91 -0
- package/dist/client/client/shim/events.js +88 -0
- package/dist/client/client/shim/util.js +20 -0
- package/dist/client/client/svelte/index.js +319 -0
- package/dist/client/client/syslog/context.js +122 -0
- package/dist/client/client/syslog/errors.js +49 -0
- package/dist/client/client/syslog/uplink.js +106 -0
- package/dist/client/client/vue/index.js +321 -0
- package/dist/client/colors.js +63 -0
- package/dist/client/debugbar.standalone.js +4274 -0
- package/dist/client/realtime/IRealtimeTransport.js +32 -0
- package/dist/client/realtime/JsonRpcPeer.js +251 -0
- package/dist/client/realtime/channelRate.js +39 -0
- package/dist/client/realtime/platformChannels.js +155 -0
- package/dist/client/syslog/Pdu.js +224 -0
- package/dist/client/syslog/Syslog.js +1159 -0
- package/dist/client/syslog/drivers/pduFlow.js +106 -0
- package/dist/client/syslog/drivers/pduProtocol.js +28 -0
- package/dist/client/syslog/logColor.js +80 -0
- package/dist/client/types/src/Container.d.ts +221 -0
- package/dist/client/types/src/Event.d.ts +171 -0
- package/dist/client/types/src/FileClass.d.ts +184 -0
- package/dist/client/types/src/Service.d.ts +165 -0
- package/dist/client/types/src/Tools.d.ts +116 -0
- package/dist/client/types/src/client/ClientKernel.d.ts +90 -0
- package/dist/client/types/src/client/IClientKernel.d.ts +195 -0
- package/dist/client/types/src/client/angular/index.d.ts +273 -0
- package/dist/client/types/src/client/announce.d.ts +74 -0
- package/dist/client/types/src/client/debugbar/DebugBar.d.ts +279 -0
- package/dist/client/types/src/client/debugbar/format.d.ts +48 -0
- package/dist/client/types/src/client/debugbar/hmr.d.ts +16 -0
- package/dist/client/types/src/client/debugbar/index.d.ts +30 -0
- package/dist/client/types/src/client/debugbar/model.d.ts +158 -0
- package/dist/client/types/src/client/debugbar/network.d.ts +55 -0
- package/dist/client/types/src/client/debugbar/profile.d.ts +99 -0
- package/dist/client/types/src/client/index.d.ts +47 -0
- package/dist/client/types/src/client/react/index.d.ts +207 -0
- package/dist/client/types/src/client/realtime/AdaptiveRate.d.ts +138 -0
- package/dist/client/types/src/client/realtime/BrowserWsTransport.d.ts +27 -0
- package/dist/client/types/src/client/realtime/RealtimeClient.d.ts +573 -0
- package/dist/client/types/src/client/realtime/localEvents.d.ts +53 -0
- package/dist/client/types/src/client/realtime/notice.d.ts +70 -0
- package/dist/client/types/src/client/realtime/observe.d.ts +284 -0
- package/dist/client/types/src/client/roles/index.d.ts +15 -0
- package/dist/client/types/src/client/roles/registry.d.ts +72 -0
- package/dist/client/types/src/client/roles/roles.d.ts +68 -0
- package/dist/client/types/src/client/shim/cli-color.d.ts +2 -0
- package/dist/client/types/src/client/shim/events.d.ts +39 -0
- package/dist/client/types/src/client/shim/util.d.ts +14 -0
- package/dist/client/types/src/client/svelte/index.d.ts +216 -0
- package/dist/client/types/src/client/syslog/context.d.ts +76 -0
- package/dist/client/types/src/client/syslog/errors.d.ts +46 -0
- package/dist/client/types/src/client/syslog/uplink.d.ts +89 -0
- package/dist/client/types/src/client/transport/websocket.d.ts +5 -0
- package/dist/client/types/src/client/vue/index.d.ts +234 -0
- package/dist/client/types/src/colors.d.ts +32 -0
- package/dist/client/types/src/config/infra.d.ts +171 -0
- package/dist/client/types/src/kernel/bootReport.d.ts +112 -0
- package/dist/client/types/src/kernel/readinessRegistry.d.ts +104 -0
- package/dist/client/types/src/realtime/IRealtimeSocket.d.ts +142 -0
- package/dist/client/types/src/realtime/IRealtimeTransport.d.ts +50 -0
- package/dist/client/types/src/realtime/JsonRpcPeer.d.ts +251 -0
- package/dist/client/types/src/realtime/RealtimeEventMap.d.ts +269 -0
- package/dist/client/types/src/realtime/channelRate.d.ts +55 -0
- package/dist/client/types/src/realtime/platformChannels.d.ts +147 -0
- package/dist/client/types/src/syslog/Pdu.d.ts +134 -0
- package/dist/client/types/src/syslog/Syslog.d.ts +482 -0
- package/dist/client/types/src/syslog/drivers/ILogDriver.d.ts +177 -0
- package/dist/client/types/src/syslog/drivers/pduFlow.d.ts +53 -0
- package/dist/client/types/src/syslog/drivers/pduProtocol.d.ts +20 -0
- package/dist/client/types/src/syslog/logColor.d.ts +59 -0
- package/dist/client/types/src/types/ICliKernel.d.ts +28 -0
- package/dist/client/types/src/types/ICommand.d.ts +14 -0
- package/dist/client/types/src/types/IContainer.d.ts +35 -0
- package/dist/client/types/src/types/IKernel.d.ts +138 -0
- package/dist/client/types/src/types/IMcpTool.d.ts +157 -0
- package/dist/client/types/src/types/IModule.d.ts +73 -0
- package/dist/client/types/src/types/IPage.d.ts +119 -0
- package/dist/client/types/src/types/IService.d.ts +62 -0
- package/dist/client/types/src/types/ISyslog.d.ts +58 -0
- package/dist/client/types/src/types/ITransport.d.ts +5 -0
- package/dist/client/types/src/types/globals.d.ts +47 -0
- package/dist/node/Cli.js +750 -0
- package/dist/node/Container.js +367 -0
- package/dist/node/Error.js +309 -0
- package/dist/node/Event.js +198 -0
- package/dist/node/FileClass.js +308 -0
- package/dist/node/Nodefony.js +78 -0
- package/dist/node/Service.js +344 -0
- package/dist/node/Tools.js +205 -0
- package/dist/node/bin/resolveLocalCli.js +10 -0
- package/dist/node/bundler/index.js +103 -0
- package/dist/node/cli/agentTargets.js +461 -0
- package/dist/node/cli/aiMcp.js +553 -0
- package/dist/node/cli/aiMcpReport.js +129 -0
- package/dist/node/cli/aiSync.js +323 -0
- package/dist/node/cli/aiSyncReport.js +142 -0
- package/dist/node/cli/card.js +186 -0
- package/dist/node/cli/cardReport.js +110 -0
- package/dist/node/cli/completion.js +415 -0
- package/dist/node/cli/create.js +805 -0
- package/dist/node/cli/env.js +332 -0
- package/dist/node/cli/envReport.js +159 -0
- package/dist/node/cli/execPortable.js +60 -0
- package/dist/node/cli/gitHooks.js +191 -0
- package/dist/node/cli/gitHooksReport.js +119 -0
- package/dist/node/cli/globalFlags.js +67 -0
- package/dist/node/cli/helpReport.js +288 -0
- package/dist/node/cli/manPage.js +165 -0
- package/dist/node/cli/nodefonyBin.js +61 -0
- package/dist/node/cli/outdated.js +144 -0
- package/dist/node/cli/progress.js +555 -0
- package/dist/node/cli/projectRoot.js +32 -0
- package/dist/node/cli/promptPassword.js +44 -0
- package/dist/node/cli/prompts.js +86 -0
- package/dist/node/cli/scaffold/destination.js +92 -0
- package/dist/node/cli/scaffold/engine.js +2156 -0
- package/dist/node/cli/scaffold/entityFields.js +589 -0
- package/dist/node/cli/scaffold/format.js +243 -0
- package/dist/node/cli/scaffold/interactive.js +124 -0
- package/dist/node/cli/scaffold/moduleLayout.js +38 -0
- package/dist/node/cli/scaffold/reservedEntities.js +74 -0
- package/dist/node/cli/scaffold/spec.js +704 -0
- package/dist/node/cli/scaffold/steps.js +40 -0
- package/dist/node/cli/scaffold/userContractSource.js +133 -0
- package/dist/node/cli/scaffold/versions.js +57 -0
- package/dist/node/cli/scaffold/writer.js +172 -0
- package/dist/node/cli/startMenu.js +496 -0
- package/dist/node/cli/symbols.js +205 -0
- package/dist/node/cli/sysexits.js +34 -0
- package/dist/node/cli/tableReport.js +184 -0
- package/dist/node/cli/usageReport.js +144 -0
- package/dist/node/colors.js +63 -0
- package/dist/node/command/Builder.js +153 -0
- package/dist/node/command/Command.js +348 -0
- package/dist/node/config/configProvenance.js +101 -0
- package/dist/node/config/defaults.js +39 -0
- package/dist/node/config/defineConfig.js +129 -0
- package/dist/node/config/defineEnv.js +264 -0
- package/dist/node/config/envExample.js +41 -0
- package/dist/node/config/envOverride.js +423 -0
- package/dist/node/config/infra.js +163 -0
- package/dist/node/config/reactivity.js +23 -0
- package/dist/node/config/reservedEnv.js +118 -0
- package/dist/node/config/schema.js +134 -0
- package/dist/node/config/use.js +24 -0
- package/dist/node/finder/File.js +49 -0
- package/dist/node/finder/FileResult.js +86 -0
- package/dist/node/finder/Finder.js +201 -0
- package/dist/node/finder/Result.js +45 -0
- package/dist/node/index.js +121 -0
- package/dist/node/kernel/BootConfigurationError.js +54 -0
- package/dist/node/kernel/CliKernel.js +630 -0
- package/dist/node/kernel/Kernel.js +2198 -0
- package/dist/node/kernel/Module.js +453 -0
- package/dist/node/kernel/adminPlane/adminCaller.js +98 -0
- package/dist/node/kernel/adminPlane/adminRbac.js +51 -0
- package/dist/node/kernel/adminPlane/catalog.js +158 -0
- package/dist/node/kernel/adminPlane/executeAdmin.js +78 -0
- package/dist/node/kernel/checks/deep.js +335 -0
- package/dist/node/kernel/checks/freshness.js +139 -0
- package/dist/node/kernel/checks/gating.js +231 -0
- package/dist/node/kernel/checks/guards.js +232 -0
- package/dist/node/kernel/checks/lastBoot.js +136 -0
- package/dist/node/kernel/checks/live.js +330 -0
- package/dist/node/kernel/checks/packageDeps.js +292 -0
- package/dist/node/kernel/checks/readiness.js +153 -0
- package/dist/node/kernel/checks/renderReport.js +780 -0
- package/dist/node/kernel/checks/report.js +421 -0
- package/dist/node/kernel/checks/runDoctor.js +731 -0
- package/dist/node/kernel/checks/surface.js +364 -0
- package/dist/node/kernel/checks/walk.js +100 -0
- package/dist/node/kernel/checks/wiring.js +450 -0
- package/dist/node/kernel/commands/AiMcpCommand.js +65 -0
- package/dist/node/kernel/commands/AiSyncCommand.js +48 -0
- package/dist/node/kernel/commands/BuildCommand.js +49 -0
- package/dist/node/kernel/commands/CardCommand.js +50 -0
- package/dist/node/kernel/commands/ClusterCommand.js +58 -0
- package/dist/node/kernel/commands/CompletionCommand.js +31 -0
- package/dist/node/kernel/commands/CreateCommand.js +45 -0
- package/dist/node/kernel/commands/DevCommand.js +99 -0
- package/dist/node/kernel/commands/DoctorCommand.js +115 -0
- package/dist/node/kernel/commands/EnvCommand.js +52 -0
- package/dist/node/kernel/commands/GitHooksCommand.js +47 -0
- package/dist/node/kernel/commands/InspectCommand.js +153 -0
- package/dist/node/kernel/commands/InstallCommand.js +20 -0
- package/dist/node/kernel/commands/MenuCommand.js +261 -0
- package/dist/node/kernel/commands/OutdatedCommand.js +164 -0
- package/dist/node/kernel/commands/ProdCommand.js +60 -0
- package/dist/node/kernel/commands/StatusCommand.js +30 -0
- package/dist/node/kernel/commands/StopCommand.js +35 -0
- package/dist/node/kernel/commands/SymbolsCommand.js +45 -0
- package/dist/node/kernel/commands/runtimeLauncher.js +95 -0
- package/dist/node/kernel/decorators/kernelDecorator.js +110 -0
- package/dist/node/kernel/injector/injector.js +137 -0
- package/dist/node/kernel/injector/serviceOrder.js +106 -0
- package/dist/node/kernel/inspect/adminSubjects.js +194 -0
- package/dist/node/kernel/inspect/docOutline.js +86 -0
- package/dist/node/kernel/lifecycleTags.js +76 -0
- package/dist/node/kernel/moduleConfig.js +75 -0
- package/dist/node/kernel/moduleGating.js +68 -0
- package/dist/node/kernel/readinessRegistry.js +98 -0
- package/dist/node/kernel/resolveModuleEntry.js +61 -0
- package/dist/node/mcp/caller.js +40 -0
- package/dist/node/mcp/guard.js +51 -0
- package/dist/node/mcp/protocol.js +157 -0
- package/dist/node/mcp/server.js +216 -0
- package/dist/node/mcp/tools.js +861 -0
- package/dist/node/oauth/authorizationServer.js +221 -0
- package/dist/node/oauth/protectedResource.js +293 -0
- package/dist/node/package.js +4 -0
- package/dist/node/realtime/IRealtimeTransport.js +32 -0
- package/dist/node/realtime/JsonRpcPeer.js +251 -0
- package/dist/node/realtime/RealtimeEventMap.js +15 -0
- package/dist/node/realtime/channelRate.js +39 -0
- package/dist/node/realtime/platformChannels.js +155 -0
- package/dist/node/runtime/GcScheduler.js +92 -0
- package/dist/node/runtime/RequestContext.js +76 -0
- package/dist/node/runtime/bearer.js +84 -0
- package/dist/node/runtime/engineEnvironment.js +79 -0
- package/dist/node/runtime/loadEnv.js +105 -0
- package/dist/node/runtime/pageFacets.js +85 -0
- package/dist/node/runtime/pageFilters.js +103 -0
- package/dist/node/runtime/pageGuard.js +84 -0
- package/dist/node/runtime/pageQuery.js +160 -0
- package/dist/node/runtime/pageSort.js +114 -0
- package/dist/node/runtime/redact.js +43 -0
- package/dist/node/runtime/withTimeout.js +71 -0
- package/dist/node/service/cluster/ClusterManager.js +202 -0
- package/dist/node/service/cluster/ClusterProbeAggregator.js +120 -0
- package/dist/node/service/cluster/ClusterRelay.js +74 -0
- package/dist/node/service/cluster/clusterMaster.js +65 -0
- package/dist/node/service/cluster/clusterMessage.js +70 -0
- package/dist/node/service/cluster/cpuQuota.js +59 -0
- package/dist/node/service/cluster/instanceProbe.js +52 -0
- package/dist/node/service/cluster/podEnvironment.js +48 -0
- package/dist/node/service/cluster/processProbe.js +70 -0
- package/dist/node/service/cluster/richProcessProbe.js +115 -0
- package/dist/node/service/cluster/topology.js +71 -0
- package/dist/node/service/dev/BootReporter.js +490 -0
- package/dist/node/service/dev/DevSupervisor.js +1004 -0
- package/dist/node/service/dev/bootVerdict.js +158 -0
- package/dist/node/service/dev/detachedStart.js +372 -0
- package/dist/node/service/dev/devProcess.js +1137 -0
- package/dist/node/service/dev/devProjects.js +138 -0
- package/dist/node/service/dev/devStatusReport.js +395 -0
- package/dist/node/service/dev/devStop.js +263 -0
- package/dist/node/service/fetchService.js +35 -0
- package/dist/node/service/gitService.js +62 -0
- package/dist/node/syslog/Pdu.js +224 -0
- package/dist/node/syslog/Syslog.js +1159 -0
- package/dist/node/syslog/drivers/ClusterFileLogDriver.js +114 -0
- package/dist/node/syslog/drivers/FileLogDriver.js +127 -0
- package/dist/node/syslog/drivers/ILogDriver.js +26 -0
- package/dist/node/syslog/drivers/LokiLogDriver.js +144 -0
- package/dist/node/syslog/drivers/MemoryLogDriver.js +33 -0
- package/dist/node/syslog/drivers/OpenSearchLogDriver.js +159 -0
- package/dist/node/syslog/drivers/builtinLogDrivers.js +114 -0
- package/dist/node/syslog/drivers/filterPdus.js +71 -0
- package/dist/node/syslog/drivers/logDriverRegistry.js +83 -0
- package/dist/node/syslog/drivers/opensearchShared.js +17 -0
- package/dist/node/syslog/drivers/pduFlow.js +106 -0
- package/dist/node/syslog/drivers/pduProtocol.js +28 -0
- package/dist/node/syslog/httpFetch.js +43 -0
- package/dist/node/syslog/logColor.js +80 -0
- package/dist/node/syslog/sinks/FileSink.js +142 -0
- package/dist/node/syslog/transports/BatchingHttpTransport.js +98 -0
- package/dist/node/syslog/transports/ConsoleTransport.js +14 -0
- package/dist/node/syslog/transports/FileTransport.js +17 -0
- package/dist/node/syslog/transports/HttpTransport.js +44 -0
- package/dist/node/syslog/transports/LokiTransport.js +80 -0
- package/dist/node/syslog/transports/OpenSearchTransport.js +53 -0
- package/dist/node/syslog/transports/SyslogTransport.js +13 -0
- package/dist/node/syslog/transports/index.js +8 -0
- package/dist/node/testing/index.js +190 -0
- package/dist/types/Cli.d.ts +177 -0
- package/dist/types/Container.d.ts +221 -0
- package/dist/types/Error.d.ts +162 -0
- package/dist/types/Event.d.ts +171 -0
- package/dist/types/FileClass.d.ts +184 -0
- package/dist/types/Nodefony.d.ts +66 -0
- package/dist/types/Service.d.ts +165 -0
- package/dist/types/Tools.d.ts +116 -0
- package/dist/types/bin/resolveLocalCli.d.ts +49 -0
- package/dist/types/bundler/index.d.ts +66 -0
- package/dist/types/cli/agentTargets.d.ts +378 -0
- package/dist/types/cli/aiMcp.d.ts +239 -0
- package/dist/types/cli/aiMcpReport.d.ts +113 -0
- package/dist/types/cli/aiSync.d.ts +68 -0
- package/dist/types/cli/aiSyncReport.d.ts +149 -0
- package/dist/types/cli/card.d.ts +49 -0
- package/dist/types/cli/cardReport.d.ts +127 -0
- package/dist/types/cli/completion.d.ts +154 -0
- package/dist/types/cli/create.d.ts +177 -0
- package/dist/types/cli/env.d.ts +103 -0
- package/dist/types/cli/envReport.d.ts +153 -0
- package/dist/types/cli/execPortable.d.ts +54 -0
- package/dist/types/cli/gitHooks.d.ts +48 -0
- package/dist/types/cli/gitHooksReport.d.ts +101 -0
- package/dist/types/cli/globalFlags.d.ts +48 -0
- package/dist/types/cli/helpReport.d.ts +110 -0
- package/dist/types/cli/manPage.d.ts +21 -0
- package/dist/types/cli/nodefonyBin.d.ts +45 -0
- package/dist/types/cli/outdated.d.ts +119 -0
- package/dist/types/cli/progress.d.ts +349 -0
- package/dist/types/cli/projectRoot.d.ts +19 -0
- package/dist/types/cli/promptPassword.d.ts +22 -0
- package/dist/types/cli/prompts.d.ts +102 -0
- package/dist/types/cli/scaffold/destination.d.ts +70 -0
- package/dist/types/cli/scaffold/engine.d.ts +545 -0
- package/dist/types/cli/scaffold/entityFields.d.ts +238 -0
- package/dist/types/cli/scaffold/format.d.ts +78 -0
- package/dist/types/cli/scaffold/interactive.d.ts +11 -0
- package/dist/types/cli/scaffold/moduleLayout.d.ts +69 -0
- package/dist/types/cli/scaffold/reservedEntities.d.ts +49 -0
- package/dist/types/cli/scaffold/spec.d.ts +159 -0
- package/dist/types/cli/scaffold/steps.d.ts +24 -0
- package/dist/types/cli/scaffold/userContractSource.d.ts +86 -0
- package/dist/types/cli/scaffold/versions.d.ts +14 -0
- package/dist/types/cli/scaffold/writer.d.ts +100 -0
- package/dist/types/cli/startMenu.d.ts +195 -0
- package/dist/types/cli/symbols.d.ts +84 -0
- package/dist/types/cli/sysexits.d.ts +31 -0
- package/dist/types/cli/tableReport.d.ts +44 -0
- package/dist/types/cli/usageReport.d.ts +108 -0
- package/dist/types/client/ClientKernel.d.ts +90 -0
- package/dist/types/client/IClientKernel.d.ts +195 -0
- package/dist/types/client/angular/index.d.ts +273 -0
- package/dist/types/client/announce.d.ts +74 -0
- package/dist/types/client/debugbar/DebugBar.d.ts +279 -0
- package/dist/types/client/debugbar/format.d.ts +48 -0
- package/dist/types/client/debugbar/hmr.d.ts +16 -0
- package/dist/types/client/debugbar/index.d.ts +30 -0
- package/dist/types/client/debugbar/model.d.ts +158 -0
- package/dist/types/client/debugbar/network.d.ts +55 -0
- package/dist/types/client/debugbar/profile.d.ts +99 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/react/index.d.ts +207 -0
- package/dist/types/client/realtime/AdaptiveRate.d.ts +138 -0
- package/dist/types/client/realtime/BrowserWsTransport.d.ts +27 -0
- package/dist/types/client/realtime/RealtimeClient.d.ts +573 -0
- package/dist/types/client/realtime/localEvents.d.ts +53 -0
- package/dist/types/client/realtime/notice.d.ts +70 -0
- package/dist/types/client/realtime/observe.d.ts +284 -0
- package/dist/types/client/roles/index.d.ts +15 -0
- package/dist/types/client/roles/registry.d.ts +72 -0
- package/dist/types/client/roles/roles.d.ts +68 -0
- package/dist/types/client/shim/cli-color.d.ts +2 -0
- package/dist/types/client/shim/events.d.ts +39 -0
- package/dist/types/client/shim/util.d.ts +14 -0
- package/dist/types/client/svelte/index.d.ts +216 -0
- package/dist/types/client/syslog/context.d.ts +76 -0
- package/dist/types/client/syslog/errors.d.ts +46 -0
- package/dist/types/client/syslog/uplink.d.ts +89 -0
- package/dist/types/client/transport/websocket.d.ts +5 -0
- package/dist/types/client/vue/index.d.ts +234 -0
- package/dist/types/colors.d.ts +32 -0
- package/dist/types/command/Builder.d.ts +47 -0
- package/dist/types/command/Command.d.ts +268 -0
- package/dist/types/config/configMeta.d.ts +53 -0
- package/dist/types/config/configProvenance.d.ts +58 -0
- package/dist/types/config/defaults.d.ts +33 -0
- package/dist/types/config/defineConfig.d.ts +70 -0
- package/dist/types/config/defineEnv.d.ts +170 -0
- package/dist/types/config/envExample.d.ts +23 -0
- package/dist/types/config/envOverride.d.ts +236 -0
- package/dist/types/config/index.d.ts +23 -0
- package/dist/types/config/infra.d.ts +171 -0
- package/dist/types/config/reactivity.d.ts +36 -0
- package/dist/types/config/reservedEnv.d.ts +145 -0
- package/dist/types/config/schema.d.ts +113 -0
- package/dist/types/config/types.d.ts +374 -0
- package/dist/types/config/use.d.ts +80 -0
- package/dist/types/finder/File.d.ts +26 -0
- package/dist/types/finder/FileResult.d.ts +14 -0
- package/dist/types/finder/Finder.d.ts +51 -0
- package/dist/types/finder/Result.d.ts +10 -0
- package/dist/types/index.d.ts +205 -0
- package/dist/types/kernel/BootConfigurationError.d.ts +50 -0
- package/dist/types/kernel/CliKernel.d.ts +289 -0
- package/dist/types/kernel/Kernel.d.ts +1255 -0
- package/dist/types/kernel/Module.d.ts +326 -0
- package/dist/types/kernel/adminPlane/adminCaller.d.ts +105 -0
- package/dist/types/kernel/adminPlane/adminRbac.d.ts +44 -0
- package/dist/types/kernel/adminPlane/catalog.d.ts +122 -0
- package/dist/types/kernel/adminPlane/executeAdmin.d.ts +99 -0
- package/dist/types/kernel/bootReport.d.ts +112 -0
- package/dist/types/kernel/checks/deep.d.ts +122 -0
- package/dist/types/kernel/checks/freshness.d.ts +49 -0
- package/dist/types/kernel/checks/gating.d.ts +134 -0
- package/dist/types/kernel/checks/guards.d.ts +57 -0
- package/dist/types/kernel/checks/lastBoot.d.ts +189 -0
- package/dist/types/kernel/checks/live.d.ts +110 -0
- package/dist/types/kernel/checks/packageDeps.d.ts +44 -0
- package/dist/types/kernel/checks/readiness.d.ts +97 -0
- package/dist/types/kernel/checks/renderReport.d.ts +107 -0
- package/dist/types/kernel/checks/report.d.ts +414 -0
- package/dist/types/kernel/checks/runDoctor.d.ts +361 -0
- package/dist/types/kernel/checks/surface.d.ts +148 -0
- package/dist/types/kernel/checks/walk.d.ts +45 -0
- package/dist/types/kernel/checks/wiring.d.ts +54 -0
- package/dist/types/kernel/commands/AiMcpCommand.d.ts +39 -0
- package/dist/types/kernel/commands/AiSyncCommand.d.ts +28 -0
- package/dist/types/kernel/commands/BuildCommand.d.ts +13 -0
- package/dist/types/kernel/commands/CardCommand.d.ts +34 -0
- package/dist/types/kernel/commands/ClusterCommand.d.ts +29 -0
- package/dist/types/kernel/commands/CompletionCommand.d.ts +17 -0
- package/dist/types/kernel/commands/CreateCommand.d.ts +16 -0
- package/dist/types/kernel/commands/DevCommand.d.ts +40 -0
- package/dist/types/kernel/commands/DoctorCommand.d.ts +55 -0
- package/dist/types/kernel/commands/EnvCommand.d.ts +35 -0
- package/dist/types/kernel/commands/GitHooksCommand.d.ts +27 -0
- package/dist/types/kernel/commands/InspectCommand.d.ts +59 -0
- package/dist/types/kernel/commands/InstallCommand.d.ts +7 -0
- package/dist/types/kernel/commands/MenuCommand.d.ts +58 -0
- package/dist/types/kernel/commands/OutdatedCommand.d.ts +47 -0
- package/dist/types/kernel/commands/ProdCommand.d.ts +29 -0
- package/dist/types/kernel/commands/StatusCommand.d.ts +17 -0
- package/dist/types/kernel/commands/StopCommand.d.ts +17 -0
- package/dist/types/kernel/commands/SymbolsCommand.d.ts +29 -0
- package/dist/types/kernel/commands/runtimeLauncher.d.ts +76 -0
- package/dist/types/kernel/decorators/kernelDecorator.d.ts +76 -0
- package/dist/types/kernel/injector/injector.d.ts +47 -0
- package/dist/types/kernel/injector/serviceOrder.d.ts +29 -0
- package/dist/types/kernel/inspect/adminSubjects.d.ts +169 -0
- package/dist/types/kernel/inspect/docOutline.d.ts +53 -0
- package/dist/types/kernel/lifecycleTags.d.ts +79 -0
- package/dist/types/kernel/moduleConfig.d.ts +35 -0
- package/dist/types/kernel/moduleGating.d.ts +78 -0
- package/dist/types/kernel/readinessRegistry.d.ts +104 -0
- package/dist/types/kernel/resolveModuleEntry.d.ts +46 -0
- package/dist/types/mcp/caller.d.ts +55 -0
- package/dist/types/mcp/guard.d.ts +66 -0
- package/dist/types/mcp/protocol.d.ts +163 -0
- package/dist/types/mcp/server.d.ts +70 -0
- package/dist/types/mcp/tools.d.ts +209 -0
- package/dist/types/oauth/authorizationServer.d.ts +204 -0
- package/dist/types/oauth/protectedResource.d.ts +339 -0
- package/dist/types/realtime/IRealtimeSocket.d.ts +142 -0
- package/dist/types/realtime/IRealtimeTransport.d.ts +50 -0
- package/dist/types/realtime/JsonRpcPeer.d.ts +251 -0
- package/dist/types/realtime/RealtimeEventMap.d.ts +269 -0
- package/dist/types/realtime/channelRate.d.ts +55 -0
- package/dist/types/realtime/platformChannels.d.ts +147 -0
- package/dist/types/runtime/GcScheduler.d.ts +96 -0
- package/dist/types/runtime/RequestContext.d.ts +152 -0
- package/dist/types/runtime/bearer.d.ts +88 -0
- package/dist/types/runtime/engineEnvironment.d.ts +115 -0
- package/dist/types/runtime/loadEnv.d.ts +86 -0
- package/dist/types/runtime/pageFacets.d.ts +104 -0
- package/dist/types/runtime/pageFilters.d.ts +139 -0
- package/dist/types/runtime/pageGuard.d.ts +78 -0
- package/dist/types/runtime/pageQuery.d.ts +138 -0
- package/dist/types/runtime/pageSort.d.ts +86 -0
- package/dist/types/runtime/redact.d.ts +27 -0
- package/dist/types/runtime/withTimeout.d.ts +51 -0
- package/dist/types/service/cluster/ClusterManager.d.ts +112 -0
- package/dist/types/service/cluster/ClusterProbeAggregator.d.ts +68 -0
- package/dist/types/service/cluster/ClusterRelay.d.ts +57 -0
- package/dist/types/service/cluster/clusterMaster.d.ts +40 -0
- package/dist/types/service/cluster/clusterMessage.d.ts +94 -0
- package/dist/types/service/cluster/cpuQuota.d.ts +37 -0
- package/dist/types/service/cluster/instanceProbe.d.ts +82 -0
- package/dist/types/service/cluster/podEnvironment.d.ts +35 -0
- package/dist/types/service/cluster/processProbe.d.ts +53 -0
- package/dist/types/service/cluster/richProcessProbe.d.ts +77 -0
- package/dist/types/service/cluster/topology.d.ts +64 -0
- package/dist/types/service/dev/BootReporter.d.ts +67 -0
- package/dist/types/service/dev/DevSupervisor.d.ts +117 -0
- package/dist/types/service/dev/bootVerdict.d.ts +99 -0
- package/dist/types/service/dev/detachedStart.d.ts +164 -0
- package/dist/types/service/dev/devProcess.d.ts +638 -0
- package/dist/types/service/dev/devProjects.d.ts +121 -0
- package/dist/types/service/dev/devStatusReport.d.ts +141 -0
- package/dist/types/service/dev/devStop.d.ts +57 -0
- package/dist/types/service/fetchService.d.ts +29 -0
- package/dist/types/service/gitService.d.ts +24 -0
- package/dist/types/syslog/Pdu.d.ts +134 -0
- package/dist/types/syslog/Syslog.d.ts +482 -0
- package/dist/types/syslog/drivers/ClusterFileLogDriver.d.ts +40 -0
- package/dist/types/syslog/drivers/FileLogDriver.d.ts +55 -0
- package/dist/types/syslog/drivers/ILogDriver.d.ts +177 -0
- package/dist/types/syslog/drivers/LokiLogDriver.d.ts +44 -0
- package/dist/types/syslog/drivers/MemoryLogDriver.d.ts +20 -0
- package/dist/types/syslog/drivers/OpenSearchLogDriver.d.ts +40 -0
- package/dist/types/syslog/drivers/builtinLogDrivers.d.ts +43 -0
- package/dist/types/syslog/drivers/filterPdus.d.ts +21 -0
- package/dist/types/syslog/drivers/logDriverRegistry.d.ts +100 -0
- package/dist/types/syslog/drivers/opensearchShared.d.ts +11 -0
- package/dist/types/syslog/drivers/pduFlow.d.ts +53 -0
- package/dist/types/syslog/drivers/pduProtocol.d.ts +20 -0
- package/dist/types/syslog/httpFetch.d.ts +48 -0
- package/dist/types/syslog/logColor.d.ts +59 -0
- package/dist/types/syslog/sinks/FileSink.d.ts +78 -0
- package/dist/types/syslog/transports/BatchingHttpTransport.d.ts +59 -0
- package/dist/types/syslog/transports/ConsoleTransport.d.ts +8 -0
- package/dist/types/syslog/transports/FileTransport.d.ts +13 -0
- package/dist/types/syslog/transports/HttpTransport.d.ts +15 -0
- package/dist/types/syslog/transports/LokiTransport.d.ts +45 -0
- package/dist/types/syslog/transports/OpenSearchTransport.d.ts +40 -0
- package/dist/types/syslog/transports/SyslogTransport.d.ts +9 -0
- package/dist/types/syslog/transports/index.d.ts +12 -0
- package/dist/types/testing/index.d.ts +197 -0
- package/dist/types/types/IAdminApi.d.ts +246 -0
- package/dist/types/types/ICliKernel.d.ts +28 -0
- package/dist/types/types/ICommand.d.ts +14 -0
- package/dist/types/types/IContainer.d.ts +35 -0
- package/dist/types/types/IIdempotencyStore.d.ts +159 -0
- package/dist/types/types/IKernel.d.ts +138 -0
- package/dist/types/types/IMcpTool.d.ts +157 -0
- package/dist/types/types/IModule.d.ts +73 -0
- package/dist/types/types/IModuleManifest.d.ts +49 -0
- package/dist/types/types/IPage.d.ts +119 -0
- package/dist/types/types/IService.d.ts +62 -0
- package/dist/types/types/ISyslog.d.ts +58 -0
- package/dist/types/types/ITransport.d.ts +5 -0
- package/dist/types/types/globals.d.ts +47 -0
- package/docs/angular-services.md +318 -0
- package/docs/catalogue.md +184 -0
- package/docs/cli.md +369 -0
- package/docs/client.md +616 -0
- package/docs/debugbar.md +218 -0
- package/docs/environnement.md +308 -0
- package/docs/index.md +388 -0
- package/docs/kernel.md +670 -0
- package/docs/progression.md +228 -0
- package/docs/react-hooks.md +686 -0
- package/docs/request-context.md +507 -0
- package/docs/service.md +590 -0
- package/docs/svelte-reactivite.md +293 -0
- package/docs/syslog.md +848 -0
- package/docs/testing.md +203 -0
- package/docs/vue-composables.md +255 -0
- package/man/nodefony.1 +152 -0
- package/package.json +163 -83
- package/templates/app/agents/AGENTS.md.tpl +967 -0
- package/templates/app/agents/POINTEUR.md.tpl +12 -0
- package/templates/app/base/Dockerfile.tpl +104 -0
- package/templates/app/base/README.md.tpl +314 -0
- package/templates/app/base/dockerignore.tpl +28 -0
- package/templates/app/base/env.ts.tpl +130 -0
- package/templates/app/base/gitattributes.tpl +28 -0
- package/templates/app/base/github/workflows/ci.yml.tpl +91 -0
- package/templates/app/base/gitignore.tpl +46 -0
- package/templates/app/base/gitlab-ci.yml.tpl +42 -0
- package/templates/app/base/index.ts.tpl +69 -0
- package/templates/app/base/nodefony.config.ts.tpl +342 -0
- package/templates/app/base/oxlintrc.json.tpl +127 -0
- package/templates/app/base/package.json.tpl +75 -0
- package/templates/app/base/prettierignore.tpl +24 -0
- package/templates/app/base/prettierrc.json.tpl +12 -0
- package/templates/app/base/rolldown.config.ts.tpl +8 -0
- package/templates/app/base/tests/config.test.ts.tpl +29 -0
- package/templates/app/base/tests/e2e.setup.ts.tpl +181 -0
- package/templates/app/base/tests/e2e.test.ts.tpl +105 -0
- package/templates/app/base/tests/migrations.e2e.test.ts.tpl +579 -0
- package/templates/app/base/tsconfig.json.tpl +47 -0
- package/templates/app/base/vitest.config.ts.tpl +88 -0
- package/templates/app/base/vitest.e2e.config.ts.tpl +39 -0
- package/templates/app/complete/compose.yaml.tpl +328 -0
- package/templates/app/complete/deploy/migrate-job.yaml.tpl +100 -0
- package/templates/app/complete/docker/db/init-nodefony-e2e.sql.tpl +30 -0
- package/templates/app/complete/docker/grafana/provisioning/datasources/loki.yaml.tpl +15 -0
- package/templates/app/complete/env.local.tpl +11 -0
- package/templates/app/complete/env.tpl +53 -0
- package/templates/app/complete/nodefony/entity/User.ts.tpl +69 -0
- package/templates/app/complete/nodefony/security/provisionUsers.ts.tpl +124 -0
- package/templates/app/complete/nodefony/service/AppBannerService.ts.tpl +98 -0
- package/templates/app/complete/nodefony/service/AppInfoService.ts.tpl +85 -0
- package/templates/app/frontend/angular/frontend/src/accent.css.tpl +34 -0
- package/templates/app/frontend/angular/frontend/src/app/app.component.ts.tpl +400 -0
- package/templates/app/frontend/react/frontend/src/App.tsx.tpl +421 -0
- package/templates/app/frontend/react/frontend/src/accent.css.tpl +30 -0
- package/templates/app/frontend/shared/frontend/src/brand.ts.tpl +3 -0
- package/templates/app/frontend/shared/frontend/src/showcase.css.tpl +224 -0
- package/templates/app/frontend/shared/nodefony/controllers/AppController.ts.tpl +45 -0
- package/templates/app/frontend/svelte/frontend/src/App.svelte.tpl +371 -0
- package/templates/app/frontend/svelte/frontend/src/accent.css.tpl +34 -0
- package/templates/app/frontend/vue/frontend/src/App.vue.tpl +367 -0
- package/templates/app/frontend/vue/frontend/src/accent.css.tpl +34 -0
- package/templates/app/home/nodefony/controllers/HomeController.ts.tpl +38 -0
- package/templates/command/nodefony/command/__NAME__.ts.tpl +132 -0
- package/templates/controller/duplex/nodefony/controllers/__NAME__.ts.tpl +188 -0
- package/templates/controller/example/nodefony/controllers/__NAME__.ts.tpl +348 -0
- package/templates/controller/hello/nodefony/controllers/__NAME__.ts.tpl +143 -0
- package/templates/controller/realtime/nodefony/controllers/__NAME__.ts.tpl +153 -0
- package/templates/controller/realtime/tests/__KEBAB__-realtime.test.ts.tpl +79 -0
- package/templates/controller/rest/nodefony/controllers/__NAME__.ts.tpl +159 -0
- package/templates/entity/base/nodefony/entity/__PASCAL__.schema.ts.tpl +31 -0
- package/templates/entity/base/nodefony/entity/__PASCAL__.ts.tpl +68 -0
- package/templates/entity/controller/nodefony/controllers/__PASCAL__Controller.ts.tpl +335 -0
- package/templates/entity/service/nodefony/service/__PASCAL__Service.ts.tpl +126 -0
- package/templates/entity/tests/tests/__KEBAB__.e2e.test.ts.tpl +358 -0
- package/templates/entity/tests/tests/__KEBAB__.test.ts.tpl +87 -0
- package/templates/front/angular/frontend/src/app/app.component.ts.tpl +30 -0
- package/templates/front/base/nodefony/controllers/__NAME__.ts.tpl +60 -0
- package/templates/front/react/frontend/src/App.tsx.tpl +26 -0
- package/templates/front/svelte/frontend/src/App.svelte.tpl +19 -0
- package/templates/front/vue/frontend/src/App.vue.tpl +23 -0
- package/templates/module/ai/AGENTS.md.tpl +71 -0
- package/templates/module/base/README.md.tpl +62 -0
- package/templates/module/base/docs/index.md.tpl +55 -0
- package/templates/module/base/index.ts.tpl +50 -0
- package/templates/module/base/nodefony/config/config.ts.tpl +60 -0
- package/templates/module/base/nodefony/config/defineModuleConfig.ts.tpl +55 -0
- package/templates/module/base/nodefony/src/errors/__PASCAL__Error.ts.tpl +20 -0
- package/templates/module/base/package.json.tpl +53 -0
- package/templates/module/base/rolldown.config.ts.tpl +14 -0
- package/templates/module/base/tests/__KEBAB__.test.ts.tpl +39 -0
- package/templates/module/base/tsconfig.json.tpl +42 -0
- package/templates/module/base/vitest.config.ts.tpl +27 -0
- package/templates/module/packages/CLAUDE.md.tpl +51 -0
- package/templates/module/packages/MEMORY.md.tpl +39 -0
- package/templates/module/packages/tsconfig.declarations.json.tpl +19 -0
- package/templates/module/packages/tsconfig.tests.json.tpl +11 -0
- package/templates/module/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +14 -0
- package/templates/module/service/nodefony/interfaces/index.ts.tpl +1 -0
- package/templates/module/service/nodefony/service/__PASCAL__Service.ts.tpl +116 -0
- package/templates/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +19 -0
- package/templates/service/nodefony/service/__PASCAL__Service.ts.tpl +136 -0
- package/templates/service/tests/__PASCAL__Service.test.ts.tpl +53 -0
- package/templates/shared/front-entry/angular/frontend/src/main.ts.tpl +17 -0
- package/templates/shared/front-entry/react/frontend/src/main.tsx.tpl +11 -0
- package/templates/shared/front-entry/svelte/frontend/src/main.ts.tpl +13 -0
- package/templates/shared/front-entry/vue/frontend/src/main.ts.tpl +12 -0
- package/templates/shared/front-registrar/nodefony/frontend/register__PASCAL__Entry.ts.tpl +40 -0
- package/templates/shared/front-shell/frontend/index.html.tpl +39 -0
- package/templates/shared/ng-app-tsconfig/frontend/tsconfig.app.json.tpl +18 -0
- package/templates/shared/svelte-shim/frontend/src/env.d.ts.tpl +12 -0
- package/templates/shared/vue-shim/frontend/src/env.d.ts.tpl +15 -0
- package/.yarnclean +0 -45
- package/CHANGELOG.md +0 -406
- package/autoloader.es6 +0 -253
- package/builder.es6 +0 -366
- package/cli/builder/bundles/bundle.js +0 -328
- package/cli/builder/bundles/controller.js +0 -73
- package/cli/builder/bundles/users-bundle/.yarnclean +0 -45
- package/cli/builder/bundles/users-bundle/Command/fixtureTask.js +0 -47
- package/cli/builder/bundles/users-bundle/Command/usersCommand.js +0 -105
- package/cli/builder/bundles/users-bundle/Entity/mongoose/userEntity.js +0 -168
- package/cli/builder/bundles/users-bundle/Entity/sequelize/userEntity.js +0 -231
- package/cli/builder/bundles/users-bundle/Fixtures/users.js +0 -117
- package/cli/builder/bundles/users-bundle/Fixtures/usersFixtures.js +0 -114
- package/cli/builder/bundles/users-bundle/Resources/config/config.js +0 -90
- package/cli/builder/bundles/users-bundle/Resources/config/routing.js +0 -17
- package/cli/builder/bundles/users-bundle/Resources/config/security.js +0 -96
- package/cli/builder/bundles/users-bundle/Resources/config/services.js +0 -7
- package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.dev.config.js +0 -13
- package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.prod.config.js +0 -31
- package/cli/builder/bundles/users-bundle/Resources/config/webpack.config.js +0 -146
- package/cli/builder/bundles/users-bundle/Resources/js/users.js +0 -153
- package/cli/builder/bundles/users-bundle/Resources/public/favicon.ico +0 -0
- package/cli/builder/bundles/users-bundle/Resources/public/images/users-logo.png +0 -0
- package/cli/builder/bundles/users-bundle/Resources/scss/awesome/font-awesome.config.js +0 -11
- package/cli/builder/bundles/users-bundle/Resources/scss/custom.scss +0 -23
- package/cli/builder/bundles/users-bundle/Resources/scss/users.scss +0 -59
- package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/login.js +0 -235
- package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/users.js +0 -232
- package/cli/builder/bundles/users-bundle/Resources/translations/login.en_en.yml +0 -14
- package/cli/builder/bundles/users-bundle/Resources/translations/login.fr_fr.yml +0 -14
- package/cli/builder/bundles/users-bundle/Resources/translations/users.en_en.yml +0 -35
- package/cli/builder/bundles/users-bundle/Resources/translations/users.fr_fr.yml +0 -36
- package/cli/builder/bundles/users-bundle/Resources/views/base.html.twig +0 -32
- package/cli/builder/bundles/users-bundle/Resources/views/footer.html.twig +0 -7
- package/cli/builder/bundles/users-bundle/Resources/views/header.html.twig +0 -90
- package/cli/builder/bundles/users-bundle/Resources/views/login/login.html.twig +0 -43
- package/cli/builder/bundles/users-bundle/Resources/views/users/createUser.html.twig +0 -296
- package/cli/builder/bundles/users-bundle/Resources/views/users/readUsers.html.twig +0 -94
- package/cli/builder/bundles/users-bundle/controller/api/graphql/graphqlController.js +0 -56
- package/cli/builder/bundles/users-bundle/controller/api/graphql/userResolver.js +0 -121
- package/cli/builder/bundles/users-bundle/controller/api/graphql/usertype.js +0 -58
- package/cli/builder/bundles/users-bundle/controller/api/openapi/loginApiController.js +0 -210
- package/cli/builder/bundles/users-bundle/controller/api/openapi/restUserController.js +0 -244
- package/cli/builder/bundles/users-bundle/controller/loginController.js +0 -91
- package/cli/builder/bundles/users-bundle/controller/usersController.js +0 -332
- package/cli/builder/bundles/users-bundle/package.json +0 -58
- package/cli/builder/bundles/users-bundle/readme.md +0 -360
- package/cli/builder/bundles/users-bundle/services/usersService.js +0 -411
- package/cli/builder/bundles/users-bundle/src/providers/mongoose/userProvider.js +0 -47
- package/cli/builder/bundles/users-bundle/src/providers/sequelize/userProvider.js +0 -48
- package/cli/builder/bundles/users-bundle/tests/loginTest.js +0 -182
- package/cli/builder/bundles/users-bundle/usersBundle.js +0 -26
- package/cli/builder/microService/microService.js +0 -261
- package/cli/builder/microService/skeletons/.editorconfig +0 -18
- package/cli/builder/microService/skeletons/.env-cmdrc.js +0 -32
- package/cli/builder/microService/skeletons/.jshintrc +0 -37
- package/cli/builder/microService/skeletons/README.md +0 -117
- package/cli/builder/microService/skeletons/bin/bash/hello.sh +0 -5
- package/cli/builder/microService/skeletons/bin/cli +0 -13
- package/cli/builder/microService/skeletons/bin/python/hello.py +0 -20
- package/cli/builder/microService/skeletons/config/config.js +0 -29
- package/cli/builder/microService/skeletons/config/pm2.config.js +0 -72
- package/cli/builder/microService/skeletons/config/webpack/webpack.config.dev.js +0 -8
- package/cli/builder/microService/skeletons/config/webpack/webpack.config.prod.js +0 -25
- package/cli/builder/microService/skeletons/config/webpack.config.js +0 -114
- package/cli/builder/microService/skeletons/package.json +0 -76
- package/cli/builder/microService/skeletons/src/browser/index.css +0 -3
- package/cli/builder/microService/skeletons/src/browser/index.js +0 -16
- package/cli/builder/microService/skeletons/src/browser/socketio.js +0 -46
- package/cli/builder/microService/skeletons/src/cli/cli.js +0 -224
- package/cli/builder/microService/skeletons/src/cli/menu.js +0 -28
- package/cli/builder/microService/skeletons/src/n-api/README.md +0 -1
- package/cli/builder/microService/skeletons/src/n-api/binding.gyp +0 -14
- package/cli/builder/microService/skeletons/src/n-api/hello.cc +0 -14
- package/cli/builder/microService/skeletons/src/n-api/hello.js +0 -23
- package/cli/builder/microService/skeletons/src/n-api/package.json +0 -15
- package/cli/builder/microService/skeletons/src/node/examples/index.js +0 -120
- package/cli/builder/microService/skeletons/src/node/index.js +0 -102
- package/cli/builder/microService/skeletons/src/node/services/markdown/markdown.js +0 -27
- package/cli/builder/microService/skeletons/src/node/services/servers/http.js +0 -45
- package/cli/builder/microService/skeletons/src/node/services/servers/https.js +0 -44
- package/cli/builder/microService/skeletons/src/node/services/socketio/socketio.js +0 -46
- package/cli/builder/microService/skeletons/src/node/services/syscall/syscall.js +0 -111
- package/cli/builder/microService/skeletons/src/node/services/worker/thread.js +0 -59
- package/cli/builder/microService/skeletons/src/node/services/worker/worker.js +0 -60
- package/cli/builder/microService/skeletons/src/templates/base.html +0 -12
- package/cli/builder/microService/skeletons/src/templates/socket.html +0 -23
- package/cli/builder/microService/skeletons/tests/unit/serviceTest.js +0 -30
- package/cli/builder/project/project.js +0 -474
- package/cli/builder/project/skeletons/README.md +0 -778
- package/cli/builder/project/skeletons/bin/dev-deploy.sh +0 -5
- package/cli/builder/project/skeletons/bin/generateCertificates.sh.skeleton +0 -180
- package/cli/builder/project/skeletons/bin/prod-deploy.sh +0 -3
- package/cli/builder/project/skeletons/config/config.js.skeleton +0 -178
- package/cli/builder/project/skeletons/config/openssl/ca/openssl.cnf.skeleton +0 -150
- package/cli/builder/project/skeletons/config/openssl/ca_intermediate/openssl.cnf.skeleton +0 -152
- package/cli/builder/project/skeletons/config/pm2.config.js.skeleton +0 -62
- package/cli/builder/project/skeletons/documentation.html.twig +0 -24
- package/cli/builder/project/skeletons/editorconfig.skeleton +0 -18
- package/cli/builder/project/skeletons/eslintignore.skeleton +0 -15
- package/cli/builder/project/skeletons/eslintrc.js.skeleton +0 -97
- package/cli/builder/project/skeletons/gitignore.skeleton +0 -32
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.37.entity-user.js +0 -137
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.38.entity-session.js +0 -97
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.39.entity-requests.js +0 -115
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.40.entity-jwts.js +0 -97
- package/cli/builder/project/skeletons/migrations/migrations.skeleton.js +0 -101
- package/cli/builder/project/skeletons/package.json.twig +0 -71
- package/cli/builder/react/reactBuilder.js +0 -224
- package/cli/builder/react/skeletons/app/Resources/databases/nodefony.db +0 -0
- package/cli/builder/react/skeletons/app/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/react/skeletons/app/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/react/skeletons/app/Resources/views/base.html.twig +0 -33
- package/cli/builder/react/skeletons/app/config/config.js +0 -392
- package/cli/builder/react/skeletons/app/config/routing.js +0 -10
- package/cli/builder/react/skeletons/app/config/security.js +0 -74
- package/cli/builder/react/skeletons/app/controller/appController.js +0 -22
- package/cli/builder/sandbox/sandBoxBuilder.js +0 -610
- package/cli/builder/sandbox/skeletons/Resources/css/entry.css +0 -48
- package/cli/builder/sandbox/skeletons/Resources/js/entry.js +0 -60
- package/cli/builder/sandbox/skeletons/Resources/public/manifest.json +0 -32
- package/cli/builder/sandbox/skeletons/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/sandbox/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/sandbox/skeletons/Resources/views/base.html.twig +0 -46
- package/cli/builder/sandbox/skeletons/Resources/views/index.html.twig +0 -34
- package/cli/builder/sandbox/skeletons/bootstrap/custom.scss +0 -27
- package/cli/builder/sandbox/skeletons/bootstrap/entry.scss +0 -60
- package/cli/builder/sandbox/skeletons/workbox/templates/index.html.twig +0 -32
- package/cli/builder/sandbox/skeletons/workbox/workers/service-worker.js +0 -196
- package/cli/builder/skeletons/Resources/public/favicon.ico +0 -0
- package/cli/builder/skeletons/Resources/public/images/app-logo.png +0 -0
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.json.twig +0 -24
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.json.twig +0 -20
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.json.twig +0 -20
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/500.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/base.html.twig +0 -30
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.json.twig +0 -24
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/footer.html.twig +0 -33
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/header.html.twig +0 -6
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.html.twig +0 -6
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.json.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.html.twig +0 -125
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.json.twig +0 -5
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.json.twig +0 -24
- package/cli/builder/skeletons/app/appKernel.js +0 -133
- package/cli/builder/skeletons/binding/binding.cc +0 -26
- package/cli/builder/skeletons/binding/binding.skeleton +0 -8
- package/cli/builder/skeletons/bundle/bundleClass.js +0 -37
- package/cli/builder/skeletons/command/commandClass.js +0 -11
- package/cli/builder/skeletons/command/taskClass.js +0 -20
- package/cli/builder/skeletons/config/config.js +0 -138
- package/cli/builder/skeletons/config/nodefony/elastic-bundle.js +0 -37
- package/cli/builder/skeletons/config/nodefony/framework-bundle.js +0 -22
- package/cli/builder/skeletons/config/nodefony/http-bundle.js +0 -104
- package/cli/builder/skeletons/config/nodefony/mail-bundle.js +0 -44
- package/cli/builder/skeletons/config/nodefony/mongoose-bundle.js +0 -51
- package/cli/builder/skeletons/config/nodefony/monitoring-bundle.js +0 -34
- package/cli/builder/skeletons/config/nodefony/realtime-bundle.js +0 -15
- package/cli/builder/skeletons/config/nodefony/redis-bundle.js +0 -44
- package/cli/builder/skeletons/config/nodefony/security-bundle.js +0 -25
- package/cli/builder/skeletons/config/nodefony/sequelize-bundle.js +0 -156
- package/cli/builder/skeletons/config/routing.js +0 -55
- package/cli/builder/skeletons/config/security.js +0 -102
- package/cli/builder/skeletons/config/services.js +0 -1
- package/cli/builder/skeletons/config/webpack/webpack.dev.config.js +0 -13
- package/cli/builder/skeletons/config/webpack/webpack.prod.config.js +0 -20
- package/cli/builder/skeletons/config/webpack.config.js +0 -148
- package/cli/builder/skeletons/controller/controllerClass.js +0 -75
- package/cli/builder/skeletons/package.json.twig +0 -57
- package/cli/builder/skeletons/unittest/testFile.js +0 -46
- package/cli/builder/vue/skeletons/Resources/public/images/app-logo.png +0 -0
- package/cli/builder/vue/skeletons/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/vue/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/vue/skeletons/vue.config.js +0 -129
- package/cli/builder/vue/vueBuilder.js +0 -410
- package/cli/generate/generate.es6 +0 -103
- package/cli/install/install.js +0 -190
- package/cli/sequelize/sequelize.js +0 -94
- package/cli/start.js +0 -702
- package/cli/tools/pm2.es6 +0 -112
- package/cli/tools/tools.es6 +0 -97
- package/cli.es6 +0 -925
- package/container.es6 +0 -272
- package/error.es6 +0 -293
- package/fileClass.es6 +0 -242
- package/finder/file.es6 +0 -33
- package/finder/fileResult.es6 +0 -138
- package/finder/finder2.es6 +0 -275
- package/finder.es6 +0 -427
- package/kernel/annotations/annotations.es6 +0 -286
- package/kernel/api/api.es6 +0 -61
- package/kernel/api/graphqlApi.es6 +0 -107
- package/kernel/api/jsonApi.es6 +0 -177
- package/kernel/api/openApi.es6 +0 -139
- package/kernel/api/schemas/openApiSchema.js +0 -394
- package/kernel/babylon.es6 +0 -162
- package/kernel/bundle.es6 +0 -1200
- package/kernel/cliKernel.es6 +0 -869
- package/kernel/command.es6 +0 -78
- package/kernel/commands/nodefonyCommand.es6 +0 -247
- package/kernel/commands/tasks/bundlesTask.es6 +0 -59
- package/kernel/controllers/controller.es6 +0 -791
- package/kernel/fixture.es6 +0 -19
- package/kernel/injections/injections.es6 +0 -424
- package/kernel/kernel.es6 +0 -1960
- package/kernel/kernelWatcher.es6 +0 -387
- package/kernel/orm/entity.es6 +0 -59
- package/kernel/orm/orm.es6 +0 -139
- package/kernel/reader.es6 +0 -290
- package/kernel/security/encoder.es6 +0 -17
- package/kernel/security/factories/factory.es6 +0 -111
- package/kernel/security/factories/passeportFactory.es6 +0 -61
- package/kernel/security/provider.es6 +0 -58
- package/kernel/security/providers/chainProvider.es6 +0 -99
- package/kernel/security/providers/memoryProvider.es6 +0 -55
- package/kernel/security/providers/providerManager.es6 +0 -67
- package/kernel/security/providers/userEntityProvider.es6 +0 -50
- package/kernel/security/role.es6 +0 -12
- package/kernel/security/secureArea.es6 +0 -431
- package/kernel/security/tokens/token.es6 +0 -171
- package/kernel/security/user.es6 +0 -106
- package/kernel/serviceRealTime.es6 +0 -21
- package/kernel/services/connectionsService.es6 +0 -93
- package/kernel/services/cron/cronService.es6 +0 -123
- package/kernel/task.es6 +0 -80
- package/kernel/templates.es6 +0 -22
- package/kernel/templating/twig.es6 +0 -218
- package/kernel/tests/cliTest.js +0 -59
- package/kernel/tests/containerTest.js +0 -232
- package/kernel/tests/kernelTest.js +0 -39
- package/nodefony.es6 +0 -1084
- package/notificationsCenter.es6 +0 -118
- package/protocol.es6 +0 -86
- package/protocols/bayeux.es6 +0 -174
- package/protocols/jsonRpc/jsonrpc.es6 +0 -56
- package/result.es6 +0 -99
- package/service.es6 +0 -288
- package/syslog/pdu.es6 +0 -141
- package/syslog/syslog.es6 +0 -754
- package/watcher.es6 +0 -113
|
@@ -0,0 +1,4274 @@
|
|
|
1
|
+
//#region src/client/realtime/notice.ts
|
|
2
|
+
/**
|
|
3
|
+
* Traduit un refus de canal (`realtime:denied`, {@link IRealtimeDenied}) en
|
|
4
|
+
* {@link NodefonyNotice}. Pendant FRAME du {@link closeCodeToNotice} (qui traite
|
|
5
|
+
* la fermeture de la CONNEXION) : ici la connexion vit, seul l'abonnement à CE
|
|
6
|
+
* canal n'a pas abouti.
|
|
7
|
+
*
|
|
8
|
+
* Le message SUIT le motif, parce que les trois n'appellent pas le même geste :
|
|
9
|
+
* un accès refusé demande une élévation de droits, un plafond atteint demande de
|
|
10
|
+
* libérer un abonnement, un canal sans producteur demande de relire son nom. Un
|
|
11
|
+
* « accès refusé » affiché sur une faute de frappe envoie chercher un droit qui
|
|
12
|
+
* n'a jamais manqué. Ce qui reste générique, et le restera, c'est le DÉTAIL de la
|
|
13
|
+
* politique : le serveur ne dit jamais quel rôle ou quel scope manque.
|
|
14
|
+
*
|
|
15
|
+
* @param denied - `{ channel, reason }` reçu du serveur.
|
|
16
|
+
* @returns une notice prête à afficher (toujours, contrairement au close-code).
|
|
17
|
+
*/
|
|
18
|
+
function deniedToNotice(denied) {
|
|
19
|
+
const message = denied.reason === "unknown" ? `Canal « ${denied.channel} » inconnu du serveur : personne ne le produit (nom exact ? module chargé ?)` : denied.reason === "limit" ? `Canal « ${denied.channel} » non ouvert : trop d'abonnements sur cette connexion` : `Accès au canal « ${denied.channel} » refusé`;
|
|
20
|
+
return {
|
|
21
|
+
level: denied.reason === "unknown" ? "warning" : "error",
|
|
22
|
+
title: "Temps réel",
|
|
23
|
+
message,
|
|
24
|
+
source: "realtime",
|
|
25
|
+
ts: Date.now()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Traduit un **code de fermeture WebSocket** (RFC 6455 §7.4) en {@link NodefonyNotice}
|
|
30
|
+
* — pendant CLIENT du `toWsCloseCode` serveur (`@nodefony/http`). Pur, sans état,
|
|
31
|
+
* donc trivialement testable et réutilisable.
|
|
32
|
+
*
|
|
33
|
+
* Retourne `null` pour une fermeture **propre** (1000) ou **attendue** (1001 going
|
|
34
|
+
* away = restart serveur, le reconnect rétablit) : pas de bruit inutile.
|
|
35
|
+
*
|
|
36
|
+
* @param code - code de fermeture reçu (`CloseEvent.code`).
|
|
37
|
+
* @param reason - description optionnelle (`CloseEvent.reason`), ajoutée au message.
|
|
38
|
+
* @returns une notice à afficher, ou `null` si la fermeture ne mérite aucune alerte.
|
|
39
|
+
*/
|
|
40
|
+
function closeCodeToNotice(code, reason) {
|
|
41
|
+
if (code === 1e3 || code === 1001) return null;
|
|
42
|
+
let level = "warning";
|
|
43
|
+
let message;
|
|
44
|
+
switch (code) {
|
|
45
|
+
case 1002:
|
|
46
|
+
level = "error";
|
|
47
|
+
message = "Erreur de protocole temps réel";
|
|
48
|
+
break;
|
|
49
|
+
case 1003:
|
|
50
|
+
level = "error";
|
|
51
|
+
message = "Données temps réel non supportées";
|
|
52
|
+
break;
|
|
53
|
+
case 1006:
|
|
54
|
+
message = "Connexion temps réel perdue";
|
|
55
|
+
break;
|
|
56
|
+
case 1007:
|
|
57
|
+
level = "error";
|
|
58
|
+
message = "Trame temps réel invalide";
|
|
59
|
+
break;
|
|
60
|
+
case 1008:
|
|
61
|
+
level = "error";
|
|
62
|
+
message = "Accès temps réel refusé";
|
|
63
|
+
break;
|
|
64
|
+
case 1009:
|
|
65
|
+
message = "Message temps réel trop volumineux";
|
|
66
|
+
break;
|
|
67
|
+
case 1010:
|
|
68
|
+
level = "error";
|
|
69
|
+
message = "Extension temps réel requise manquante";
|
|
70
|
+
break;
|
|
71
|
+
case 1011:
|
|
72
|
+
level = "error";
|
|
73
|
+
message = "Erreur serveur temps réel";
|
|
74
|
+
break;
|
|
75
|
+
case 4004:
|
|
76
|
+
level = "error";
|
|
77
|
+
message = "Ressource temps réel introuvable";
|
|
78
|
+
break;
|
|
79
|
+
default: message = typeof code === "number" ? `Connexion temps réel fermée (code ${code})` : "Connexion temps réel fermée";
|
|
80
|
+
}
|
|
81
|
+
const trimmed = reason?.trim();
|
|
82
|
+
return {
|
|
83
|
+
level,
|
|
84
|
+
title: "Temps réel",
|
|
85
|
+
message: trimmed ? `${message} : ${trimmed}` : message,
|
|
86
|
+
source: "realtime",
|
|
87
|
+
code,
|
|
88
|
+
ts: Date.now()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Close codes (RFC 6455 §7.4 + privés Nodefony 4xxx) après lesquels une
|
|
93
|
+
* reconnexion automatique est INUTILE ou NUISIBLE — la cause ne disparaît pas en
|
|
94
|
+
* réessayant : fin propre (1000), erreurs de protocole/négociation
|
|
95
|
+
* (1002/1003/1007/1010), violation de politique = 401/403 mappés (1008), et
|
|
96
|
+
* ressource introuvable (4004, privé Nodefony). Une boucle de reco sur ces codes
|
|
97
|
+
* martèle le serveur pour rien (vécu : un anonyme sur un endpoint protégé).
|
|
98
|
+
*/
|
|
99
|
+
const FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([
|
|
100
|
+
1e3,
|
|
101
|
+
1002,
|
|
102
|
+
1003,
|
|
103
|
+
1007,
|
|
104
|
+
1008,
|
|
105
|
+
1010,
|
|
106
|
+
4004
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* Faut-il tenter une reconnexion après ce close code WebSocket ? Pendant CLIENT
|
|
110
|
+
* de la sémantique RFC 6455 §7.4 émise par le serveur (`toWsCloseCode`).
|
|
111
|
+
*
|
|
112
|
+
* `false` pour les fermetures DÉFINITIVES (cf {@link FATAL_CLOSE_CODES}) : l'app
|
|
113
|
+
* doit agir (ex. login après un 1008) puis rétablir explicitement (`connect()` /
|
|
114
|
+
* `retryNow()`). `true` pour les transitoires (1001 restart, 1006 perte réseau,
|
|
115
|
+
* 1011 erreur serveur, code absent/inconnu) où la reconnexion a une chance.
|
|
116
|
+
*
|
|
117
|
+
* @param code - close code reçu (`CloseEvent.code`), ou `undefined` (anormal).
|
|
118
|
+
* @returns `true` si une reconnexion auto est pertinente.
|
|
119
|
+
*/
|
|
120
|
+
function isReconnectableCloseCode(code) {
|
|
121
|
+
if (code === void 0) return true;
|
|
122
|
+
return !FATAL_CLOSE_CODES.has(code);
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/realtime/JsonRpcPeer.ts
|
|
126
|
+
var RpcError = class extends Error {
|
|
127
|
+
name = "RpcError";
|
|
128
|
+
data;
|
|
129
|
+
constructor(message, code = -32e3, data) {
|
|
130
|
+
super(message);
|
|
131
|
+
this.code = code;
|
|
132
|
+
if (data !== void 0) this.data = data;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Réponse d'un handler qui veut JOINDRE des métadonnées serveur à son résultat.
|
|
137
|
+
*
|
|
138
|
+
* Un handler retourne normalement sa valeur nue (elle devient `result`). Quand
|
|
139
|
+
* il retourne une enveloppe, le peer la déballe : `result` reste la valeur nue,
|
|
140
|
+
* et `meta` voyage dans un champ frère de la trame JSON-RPC. Coût pour ceux qui
|
|
141
|
+
* ne l'utilisent pas : un `instanceof` par frame.
|
|
142
|
+
*/
|
|
143
|
+
var RpcEnvelope = class {
|
|
144
|
+
result;
|
|
145
|
+
meta;
|
|
146
|
+
constructor(result, meta) {
|
|
147
|
+
this.result = result;
|
|
148
|
+
this.meta = meta;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var JsonRpcPeer = class JsonRpcPeer {
|
|
152
|
+
opts;
|
|
153
|
+
nextId = 1;
|
|
154
|
+
pending = null;
|
|
155
|
+
actions = null;
|
|
156
|
+
constructor(opts) {
|
|
157
|
+
this.opts = opts;
|
|
158
|
+
}
|
|
159
|
+
/** Expose une action (requête entrante `method`+`id` → `result`). Idempotent. */
|
|
160
|
+
register(method, handler) {
|
|
161
|
+
(this.actions ??= /* @__PURE__ */ new Map()).set(method, handler);
|
|
162
|
+
}
|
|
163
|
+
/** Retire une action. */
|
|
164
|
+
unregister(method) {
|
|
165
|
+
this.actions?.delete(method);
|
|
166
|
+
}
|
|
167
|
+
/** Liste des actions exposées (pour annoncer au handshake, découverte côté pair). */
|
|
168
|
+
get methods() {
|
|
169
|
+
return this.actions ? [...this.actions.keys()] : [];
|
|
170
|
+
}
|
|
171
|
+
/** Requête SORTANTE — `Promise` résolue avec le `result` (rejette sur `error`/timeout). */
|
|
172
|
+
request(method, params, timeoutMs = 3e4) {
|
|
173
|
+
return this.startCall(method, params, timeoutMs);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Requête SORTANTE **tracée** — rend `{ result, meta }` au lieu du `result` nu.
|
|
177
|
+
*
|
|
178
|
+
* Même appel que {@link request} : la seule différence est que l'appelant
|
|
179
|
+
* garde la méta serveur (en dev, le `requestId` du profil de la frame → sa
|
|
180
|
+
* radiographie). Un serveur qui n'en émet pas laisse simplement `meta`
|
|
181
|
+
* absent.
|
|
182
|
+
*/
|
|
183
|
+
requestTraced(method, params, timeoutMs = 3e4) {
|
|
184
|
+
return this.startCall(method, params, timeoutMs, true);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Frame d'une notification sortante — **source unique** de sa forme.
|
|
188
|
+
*
|
|
189
|
+
* Extraite pour que le fan-out d'un canal diffusé puisse sérialiser la frame
|
|
190
|
+
* **une seule fois** pour N abonnés, sans jamais risquer de diverger du format
|
|
191
|
+
* qu'émet {@link notify} : les deux voies passent par ici.
|
|
192
|
+
*
|
|
193
|
+
* @param method - nom de la méthode (côté canal : le nom du canal).
|
|
194
|
+
* @param params - charge applicative ; omise de la frame si `undefined`.
|
|
195
|
+
* @returns la frame JSON-RPC 2.0, prête à sérialiser.
|
|
196
|
+
*/
|
|
197
|
+
static buildNotification(method, params) {
|
|
198
|
+
return {
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
method,
|
|
201
|
+
params
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Notification SORTANTE (pas de réponse attendue).
|
|
206
|
+
*
|
|
207
|
+
* @returns `false` si le transport a signalé qu'il n'a pas émis. Une
|
|
208
|
+
* notification n'attend rien en retour : sans cette valeur, une frame perdue
|
|
209
|
+
* ne laisserait aucune trace chez l'appelant. Les appels qui se rattrapent
|
|
210
|
+
* seuls (ré-abonnement au reconnect) peuvent l'ignorer.
|
|
211
|
+
*/
|
|
212
|
+
notify(method, params) {
|
|
213
|
+
return this.opts.send(JsonRpcPeer.buildNotification(method, params)) !== false;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Ingestion d'une frame ENTRANTE déjà parsée → classe (request/notification/
|
|
217
|
+
* response) et route. Renvoie la nature (utile log/tests). Ne lève jamais.
|
|
218
|
+
* (Le pendant sortant = `request`/`notify` ; le `send` brut est injecté.)
|
|
219
|
+
*/
|
|
220
|
+
receive(msg) {
|
|
221
|
+
if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
|
|
222
|
+
this.opts.onFrameAudit?.("invalid", msg, this);
|
|
223
|
+
return "invalid";
|
|
224
|
+
}
|
|
225
|
+
const id = msg.id;
|
|
226
|
+
const hasId = typeof id === "number" || typeof id === "string";
|
|
227
|
+
const rawMethod = msg.method;
|
|
228
|
+
const method = typeof rawMethod === "string" ? rawMethod : void 0;
|
|
229
|
+
const params = msg.params;
|
|
230
|
+
if (method !== void 0) {
|
|
231
|
+
if (this.opts.beforeDispatch && !this.opts.beforeDispatch(msg, this)) {
|
|
232
|
+
this.opts.onFrameAudit?.("denied", msg, this);
|
|
233
|
+
if (hasId) {
|
|
234
|
+
this.opts.send({
|
|
235
|
+
jsonrpc: "2.0",
|
|
236
|
+
id,
|
|
237
|
+
error: {
|
|
238
|
+
code: -32001,
|
|
239
|
+
message: "unauthorized"
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return "request";
|
|
243
|
+
}
|
|
244
|
+
return "notification";
|
|
245
|
+
}
|
|
246
|
+
if (hasId) {
|
|
247
|
+
this.handleRequest(id, method, params, msg);
|
|
248
|
+
return "request";
|
|
249
|
+
}
|
|
250
|
+
this.opts.onNotification?.(method, params);
|
|
251
|
+
return "notification";
|
|
252
|
+
}
|
|
253
|
+
if (hasId) {
|
|
254
|
+
this.handleResponse(msg);
|
|
255
|
+
return "response";
|
|
256
|
+
}
|
|
257
|
+
this.opts.onFrameAudit?.("invalid", msg, this);
|
|
258
|
+
return "invalid";
|
|
259
|
+
}
|
|
260
|
+
/** Annule tous les pending (fermeture du transport). */
|
|
261
|
+
dispose(reason = "peer disposed") {
|
|
262
|
+
if (!this.pending) return;
|
|
263
|
+
for (const p of this.pending.values()) {
|
|
264
|
+
if (p.timer) clearTimeout(p.timer);
|
|
265
|
+
p.reject(new Error(reason));
|
|
266
|
+
}
|
|
267
|
+
this.pending.clear();
|
|
268
|
+
}
|
|
269
|
+
startCall(method, params, timeoutMs, withMeta) {
|
|
270
|
+
const id = this.nextId++;
|
|
271
|
+
const pending = this.pending ??= /* @__PURE__ */ new Map();
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
pending.delete(id);
|
|
275
|
+
reject(/* @__PURE__ */ new Error(`RPC timeout: ${method}`));
|
|
276
|
+
}, timeoutMs);
|
|
277
|
+
pending.set(id, {
|
|
278
|
+
resolve: (v) => {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
resolve(v);
|
|
281
|
+
},
|
|
282
|
+
reject: (e) => {
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
reject(e);
|
|
285
|
+
},
|
|
286
|
+
timer,
|
|
287
|
+
withMeta
|
|
288
|
+
});
|
|
289
|
+
if (this.opts.send({
|
|
290
|
+
jsonrpc: "2.0",
|
|
291
|
+
id,
|
|
292
|
+
method,
|
|
293
|
+
params
|
|
294
|
+
}) === false) {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
pending.delete(id);
|
|
297
|
+
reject(/* @__PURE__ */ new Error(`RPC non émis (transport indisponible) : ${method}`));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
handleRequest(id, method, params, rawFrame) {
|
|
302
|
+
const handler = this.actions?.get(method);
|
|
303
|
+
if (!handler) {
|
|
304
|
+
this.opts.onFrameAudit?.("method_not_found", rawFrame, this);
|
|
305
|
+
this.opts.send({
|
|
306
|
+
jsonrpc: "2.0",
|
|
307
|
+
id,
|
|
308
|
+
error: {
|
|
309
|
+
code: -32601,
|
|
310
|
+
message: `method not found: ${method}`
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
Promise.resolve().then(() => handler(params)).then((result) => {
|
|
316
|
+
if (result instanceof RpcEnvelope) {
|
|
317
|
+
this.opts.send({
|
|
318
|
+
jsonrpc: "2.0",
|
|
319
|
+
id,
|
|
320
|
+
result: result.result,
|
|
321
|
+
meta: result.meta
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.opts.send({
|
|
326
|
+
jsonrpc: "2.0",
|
|
327
|
+
id,
|
|
328
|
+
result
|
|
329
|
+
});
|
|
330
|
+
}, (err) => {
|
|
331
|
+
if (err instanceof RpcError) {
|
|
332
|
+
const error = {
|
|
333
|
+
code: err.code,
|
|
334
|
+
message: err.message
|
|
335
|
+
};
|
|
336
|
+
if (err.data !== void 0) error.data = err.data;
|
|
337
|
+
this.opts.send({
|
|
338
|
+
jsonrpc: "2.0",
|
|
339
|
+
id,
|
|
340
|
+
error
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.opts.onError?.(`rpc ${method}`, err);
|
|
345
|
+
this.opts.onFrameAudit?.("internal_error", rawFrame, this);
|
|
346
|
+
this.opts.send({
|
|
347
|
+
jsonrpc: "2.0",
|
|
348
|
+
id,
|
|
349
|
+
error: {
|
|
350
|
+
code: -32603,
|
|
351
|
+
message: "internal error"
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
handleResponse(msg) {
|
|
357
|
+
if (typeof msg.id !== "number" || !this.pending) return;
|
|
358
|
+
const pending = this.pending.get(msg.id);
|
|
359
|
+
if (!pending) return;
|
|
360
|
+
if (msg.error) {
|
|
361
|
+
this.pending.delete(msg.id);
|
|
362
|
+
pending.reject(new RpcError(msg.error.message, msg.error.code, msg.error.data));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if ("result" in msg) {
|
|
366
|
+
this.pending.delete(msg.id);
|
|
367
|
+
pending.resolve(pending.withMeta ? {
|
|
368
|
+
result: msg.result,
|
|
369
|
+
meta: msg.meta
|
|
370
|
+
} : msg.result);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/client/realtime/localEvents.ts
|
|
376
|
+
/**
|
|
377
|
+
* localEvents — **noms des événements LOCAUX du client temps réel**.
|
|
378
|
+
*
|
|
379
|
+
* Ces six noms ne voyagent jamais sur le fil : ils ne servent qu'à propager, dans
|
|
380
|
+
* la page, ce que le client vient d'apprendre (changement d'état de connexion,
|
|
381
|
+
* identité résolue au welcome, échantillon de statistiques, notice normalisée,
|
|
382
|
+
* refus de canal). Ils partagent la table de `on()`/`off()` avec les canaux
|
|
383
|
+
* réseau ; le double soulignement est ce qui les en distingue.
|
|
384
|
+
*
|
|
385
|
+
* **La table est la source unique**, exactement pour la même raison que
|
|
386
|
+
* {@link PLATFORM_CHANNELS} l'est pour les canaux réseau : une chaîne recopiée
|
|
387
|
+
* dans un écran est une chaîne qui survivra au renommage suivant. Celle-ci avait
|
|
388
|
+
* déjà divergé — `"__state__"` était écrit en clair dans les trois gabarits
|
|
389
|
+
* d'application, dans la barre de debug et dans la console d'administration,
|
|
390
|
+
* sans qu'aucune constante ne l'exporte.
|
|
391
|
+
*
|
|
392
|
+
* Un consommateur n'a d'ailleurs pas à les connaître : `onState`, `onIdentity`,
|
|
393
|
+
* `onStats`, `onNotice`, `onDenied` et `onReconnect` sont les portes publiques. La table existe
|
|
394
|
+
* pour les implémentations (le client lui-même, ses observateurs agnostiques) et
|
|
395
|
+
* pour un test qui veut se brancher au plus bas niveau.
|
|
396
|
+
*
|
|
397
|
+
* Pur et isomorphe (aucun import, aucune allocation).
|
|
398
|
+
*/
|
|
399
|
+
/**
|
|
400
|
+
* Événements locaux du {@link RealtimeClient} — jamais émis ni reçus par le réseau.
|
|
401
|
+
*
|
|
402
|
+
* @see {@link RealtimeClient.onState} · {@link RealtimeClient.onIdentity} ·
|
|
403
|
+
* {@link RealtimeClient.onStats} · {@link RealtimeClient.onNotice} ·
|
|
404
|
+
* {@link RealtimeClient.onDenied} — les portes publiques à préférer partout.
|
|
405
|
+
*/
|
|
406
|
+
const LOCAL_EVENTS = {
|
|
407
|
+
/** État de la connexion (`"connected" | "reconnecting" | …`), à chaque transition. */
|
|
408
|
+
state: "__state__",
|
|
409
|
+
/** Identité résolue par le serveur au welcome ; `null` après un `disconnect()`. */
|
|
410
|
+
identity: "__identity__",
|
|
411
|
+
/** Tick d'échantillonnage des statistiques par canal (1×/s), sans charge utile. */
|
|
412
|
+
stats: "__stats__",
|
|
413
|
+
/** Notice normalisée (criticité temps réel interprétée). */
|
|
414
|
+
notice: "__notice__",
|
|
415
|
+
/** Refus d'un canal poussé par le serveur (`realtime:denied`). */
|
|
416
|
+
denied: "__denied__",
|
|
417
|
+
/** Tentative de reconnexion programmée (numéro d'essai, délai, échéance). */
|
|
418
|
+
reconnect: "__reconnect__"
|
|
419
|
+
};
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/realtime/IRealtimeTransport.ts
|
|
422
|
+
/**
|
|
423
|
+
* IRealtimeTransport — le SEUL maillon qui diffère client/serveur.
|
|
424
|
+
*
|
|
425
|
+
* Le protocole ({@link JsonRpcPeer}) et les « smarts » (stats, pub/sub) sont
|
|
426
|
+
* isomorphes ; seule la façon dont les **octets** entrent et sortent change. On
|
|
427
|
+
* isole donc le transport derrière cette interface : un même endpoint compose
|
|
428
|
+
* N transports.
|
|
429
|
+
*
|
|
430
|
+
* Implémentations :
|
|
431
|
+
* - `BrowserWsTransport` (navigateur, `WebSocket` + reconnect géré au-dessus)
|
|
432
|
+
* - `WsConnectionTransport` (serveur, wrap d'une connexion `ws` — P… RealtimeController)
|
|
433
|
+
* - futurs P13 : `Tcp`/`Udp`/`Redis`/`SipTransport` (RealtimeService / IRealtimeHub)
|
|
434
|
+
*
|
|
435
|
+
* Les états suivent les constantes `WebSocket` (0..3) pour rester universels —
|
|
436
|
+
* voir {@link TransportState}.
|
|
437
|
+
*
|
|
438
|
+
* ── Sens des flux ──
|
|
439
|
+
* Sortant : `send` (frame brute déjà sérialisée), `close`.
|
|
440
|
+
* Entrant : le owner branche `onMessage` (chaque frame reçue) ; `onOpen`/`onClose`/
|
|
441
|
+
* `onError` pilotent le cycle de vie (l'orchestration reconnect/heartbeat vit
|
|
442
|
+
* AU-DESSUS, dans l'endpoint — le transport reste « bête »).
|
|
443
|
+
*/
|
|
444
|
+
/** États d'un transport — alignés sur les constantes `WebSocket`. */
|
|
445
|
+
const TransportState = {
|
|
446
|
+
CONNECTING: 0,
|
|
447
|
+
OPEN: 1,
|
|
448
|
+
CLOSING: 2,
|
|
449
|
+
CLOSED: 3
|
|
450
|
+
};
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/client/realtime/BrowserWsTransport.ts
|
|
453
|
+
/**
|
|
454
|
+
* BrowserWsTransport — transport {@link IRealtimeTransport} pour le navigateur :
|
|
455
|
+
* wrap d'un `WebSocket` natif. Volontairement « bête » : il ouvre/envoie/ferme et
|
|
456
|
+
* relaie les events. L'orchestration (reconnect, backoff, heartbeat, state machine)
|
|
457
|
+
* vit AU-DESSUS dans `RealtimeClient` — qui crée un transport NEUF à chaque tentative.
|
|
458
|
+
*
|
|
459
|
+
* L'URL reçue est déjà normalisée (ws/wss, token) par l'appelant.
|
|
460
|
+
*/
|
|
461
|
+
var BrowserWsTransport = class {
|
|
462
|
+
url;
|
|
463
|
+
ws = null;
|
|
464
|
+
_onOpen = null;
|
|
465
|
+
_onMessage = null;
|
|
466
|
+
_onClose = null;
|
|
467
|
+
_onError = null;
|
|
468
|
+
constructor(url) {
|
|
469
|
+
this.url = url;
|
|
470
|
+
}
|
|
471
|
+
connect() {
|
|
472
|
+
const ws = new WebSocket(this.url);
|
|
473
|
+
this.ws = ws;
|
|
474
|
+
ws.addEventListener("open", () => this._onOpen?.());
|
|
475
|
+
ws.addEventListener("message", (ev) => {
|
|
476
|
+
if (typeof ev.data === "string") this._onMessage?.(ev.data);
|
|
477
|
+
});
|
|
478
|
+
ws.addEventListener("close", (ev) => {
|
|
479
|
+
this.ws = null;
|
|
480
|
+
this._onClose?.(ev.code, ev.reason);
|
|
481
|
+
});
|
|
482
|
+
ws.addEventListener("error", () => this._onError?.(/* @__PURE__ */ new Error("websocket error")));
|
|
483
|
+
}
|
|
484
|
+
send(raw) {
|
|
485
|
+
if (this.ws?.readyState === TransportState.OPEN) this.ws.send(raw);
|
|
486
|
+
}
|
|
487
|
+
close(code, reason) {
|
|
488
|
+
this.ws?.close(code, reason);
|
|
489
|
+
}
|
|
490
|
+
get readyState() {
|
|
491
|
+
return this.ws?.readyState ?? TransportState.CLOSED;
|
|
492
|
+
}
|
|
493
|
+
onOpen(cb) {
|
|
494
|
+
this._onOpen = cb;
|
|
495
|
+
}
|
|
496
|
+
onMessage(cb) {
|
|
497
|
+
this._onMessage = cb;
|
|
498
|
+
}
|
|
499
|
+
onClose(cb) {
|
|
500
|
+
this._onClose = cb;
|
|
501
|
+
}
|
|
502
|
+
onError(cb) {
|
|
503
|
+
this._onError = cb;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
/** Le badge est-il déjà sorti sur cette page ? Posé même en production. */
|
|
507
|
+
function alreadyAnnounced() {
|
|
508
|
+
return globalThis.__nfAnnounced__ === true;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Mode annoncé par le SERVEUR au handshake temps réel — `null` tant qu'aucune
|
|
512
|
+
* socket n'a reçu d'accueil.
|
|
513
|
+
*
|
|
514
|
+
* Il existe parce que `import.meta.env.DEV` ne dit que le mode du BUNDLE : une
|
|
515
|
+
* application bâtie pour la production mais servie par un serveur de
|
|
516
|
+
* développement (banc, `--cluster`, image testée localement) se taisait alors
|
|
517
|
+
* qu'on avait tout intérêt à la faire parler. Le serveur, lui, sait.
|
|
518
|
+
*/
|
|
519
|
+
let serverEnv = null;
|
|
520
|
+
/**
|
|
521
|
+
* Le détail est-il sorti sur cette page ? Un seul par page, comme le badge — et
|
|
522
|
+
* c'est le noyau qui gagne quand il y en a un.
|
|
523
|
+
*
|
|
524
|
+
* Sur `globalThis` et non en variable de module, pour la même raison que le
|
|
525
|
+
* badge : un module vit plus longtemps qu'une page dans un banc, et un état qui
|
|
526
|
+
* ne se réarme pas fait passer les tests suivants pour des régressions.
|
|
527
|
+
*/
|
|
528
|
+
function alreadyDetailed() {
|
|
529
|
+
return globalThis.__nfDetailed__ === true;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Le mode annoncé par le serveur, quand il en annonce un.
|
|
533
|
+
*
|
|
534
|
+
* ⚠️ Le serveur **ne pose ce champ qu'en dehors de la production** : une absence
|
|
535
|
+
* vaut donc production, jamais l'inverse. C'est la même règle que pour le
|
|
536
|
+
* bundle, et pour la même raison — se taire à tort ne coûte qu'une ligne, parler
|
|
537
|
+
* à tort expose la console de tout le monde.
|
|
538
|
+
*
|
|
539
|
+
* @param env - la valeur reçue dans `realtime:welcome`, ou `undefined`.
|
|
540
|
+
*/
|
|
541
|
+
function noteServerEnv(env) {
|
|
542
|
+
if (env) serverEnv = env;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Peut-on se permettre le détail ? Bundle de développement, OU serveur qui
|
|
546
|
+
* annonce un mode hors production.
|
|
547
|
+
*/
|
|
548
|
+
function isVerbose() {
|
|
549
|
+
return isDevBuild() || serverEnv !== null && serverEnv !== "production";
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Le bundle a-t-il été bâti en développement ?
|
|
553
|
+
*
|
|
554
|
+
* Le mode se LIT, il ne se devine pas : `import.meta.env.DEV` est posé par le
|
|
555
|
+
* bundler de l'application (Vite le remplace par une constante au build). Hors
|
|
556
|
+
* bundler il n'existe pas — et **l'absence de preuve de développement se traite
|
|
557
|
+
* comme la production**, jamais l'inverse : se taire à tort ne coûte qu'une
|
|
558
|
+
* ligne manquante, parler à tort pollue la console de tout le monde.
|
|
559
|
+
*/
|
|
560
|
+
function isDevBuild() {
|
|
561
|
+
return import.meta.env?.DEV === true;
|
|
562
|
+
}
|
|
563
|
+
/** Les sockets partagées, lues dans le registre de `RealtimeClient.shared()`. */
|
|
564
|
+
function sharedSockets() {
|
|
565
|
+
const g = globalThis;
|
|
566
|
+
return g.__nfRealtime__ ? [...g.__nfRealtime__.values()] : [];
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Pose ou actualise `globalThis.nodefony`.
|
|
570
|
+
*
|
|
571
|
+
* Développement seulement : la console d'une application publiée appartient à
|
|
572
|
+
* ses développeurs, et un handle global y serait une surface offerte pour rien.
|
|
573
|
+
*/
|
|
574
|
+
function poseHandle() {
|
|
575
|
+
if (!isVerbose()) return;
|
|
576
|
+
globalThis.nodefony = {
|
|
577
|
+
get kernel() {},
|
|
578
|
+
get socket() {
|
|
579
|
+
return sharedSockets()[0];
|
|
580
|
+
},
|
|
581
|
+
sockets: () => sharedSockets().map((s) => ({
|
|
582
|
+
url: s.url ?? "(adresse non résolue)",
|
|
583
|
+
state: s.state
|
|
584
|
+
})),
|
|
585
|
+
identity: () => {
|
|
586
|
+
return sharedSockets()[0]?.identity ?? null;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Retire le handle et réarme le badge — appelé quand plus rien ne vit.
|
|
592
|
+
*
|
|
593
|
+
* « Plus rien » se CONSTATE : aucun noyau, et aucune socket dans le registre du
|
|
594
|
+
* partage. Une socket construite hors `shared()` échappe à ce constat ; le seul
|
|
595
|
+
* effet est qu'un badge pourrait ressortir, jamais une fuite.
|
|
596
|
+
*/
|
|
597
|
+
function release() {
|
|
598
|
+
const g = globalThis;
|
|
599
|
+
delete g.nodefony;
|
|
600
|
+
delete g.__nfAnnounced__;
|
|
601
|
+
delete globalThis.__nfDetailed__;
|
|
602
|
+
serverEnv = null;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Émet le badge, au plus une fois par page.
|
|
606
|
+
*
|
|
607
|
+
* La forme est celle qu'ont adoptée Vue, Vite et les outils qui vivent dans une
|
|
608
|
+
* console partagée : un badge en couleur sur UNE ligne. Pas de dessin en
|
|
609
|
+
* caractères — celui du serveur a du sens dans un terminal qu'on ouvre une fois
|
|
610
|
+
* au démarrage ; dans une console de navigateur il se répète à chaque
|
|
611
|
+
* rechargement et pousse hors de vue les messages de l'application.
|
|
612
|
+
*
|
|
613
|
+
* @param name - ce qui suit « nodefony » ; le nom du noyau quand il y en a un.
|
|
614
|
+
*/
|
|
615
|
+
function badge(name) {
|
|
616
|
+
if (alreadyAnnounced()) return;
|
|
617
|
+
const c = globalThis.console;
|
|
618
|
+
if (!c?.log) return;
|
|
619
|
+
globalThis.__nfAnnounced__ = true;
|
|
620
|
+
c.log(`%c◆ nodefony%c${name}%c`, "background:#0b1120;color:#5eead4;font-weight:700;padding:2px 6px;border-radius:3px 0 0 3px", "background:#1e293b;color:#e2e8f0;padding:2px 6px;border-radius:0 3px 3px 0", "");
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Annonce une socket qui vit NUE — aucune composition, aucun noyau. C'est le cas
|
|
624
|
+
* des vitrines du dépôt et de tout widget qui monte une socket et rien d'autre.
|
|
625
|
+
*
|
|
626
|
+
* @param annoncer - `false` fait taire l'annonce (option `banner` de la socket).
|
|
627
|
+
* @returns le débranchement ; une socket partagée vit avec la page et n'a pas à
|
|
628
|
+
* l'appeler — il existe pour les bancs, qui doivent repartir d'une page vierge.
|
|
629
|
+
*/
|
|
630
|
+
function announceRealtime(annoncer) {
|
|
631
|
+
if (annoncer === false) return () => void 0;
|
|
632
|
+
badge("client");
|
|
633
|
+
poseHandle();
|
|
634
|
+
return () => {
|
|
635
|
+
if (sharedSockets().length === 0) release();
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Détail du framework, dans un groupe REPLIÉ de la console : un tableau
|
|
640
|
+
* `clé → valeur` et les deux rappels qui font gagner du temps.
|
|
641
|
+
*
|
|
642
|
+
* **Un seul détail par page**, et le noyau gagne : quand il y en a un, la socket
|
|
643
|
+
* ne le double pas — c'est lui qui a le plus à dire (état, identité, services).
|
|
644
|
+
* Une vitrine sans noyau, elle, obtient enfin le sien : c'est la moitié
|
|
645
|
+
* « diagnostic » que l'ADR-0007 D7 laissait derrière le péage de la composition.
|
|
646
|
+
*
|
|
647
|
+
* Le groupe est replié et non ouvert : présent pour qui le cherche, invisible
|
|
648
|
+
* pour qui débogue autre chose. `groupEnd` est atteint même si une lecture
|
|
649
|
+
* jette — un groupe laissé ouvert avale tous les messages suivants.
|
|
650
|
+
*
|
|
651
|
+
* @param rows - le tableau à rendre, `clé → { valeur }`, composé par
|
|
652
|
+
* l'appelant : lui seul sait ce qu'il a d'important à dire.
|
|
653
|
+
* @param subject - l'objet vivant à imprimer en dernier (noyau, ou socket).
|
|
654
|
+
* @param title - l'intitulé du groupe.
|
|
655
|
+
* @returns `true` si le détail a été émis.
|
|
656
|
+
*/
|
|
657
|
+
function consoleDetails(rows, subject, title) {
|
|
658
|
+
if (alreadyDetailed() || !isVerbose()) return false;
|
|
659
|
+
const c = globalThis.console;
|
|
660
|
+
if (!c?.log || !c.groupCollapsed || !c.groupEnd) return false;
|
|
661
|
+
globalThis.__nfDetailed__ = true;
|
|
662
|
+
c.groupCollapsed(`%c${title}`, "color:#94a3b8");
|
|
663
|
+
try {
|
|
664
|
+
if (c.table) c.table(rows);
|
|
665
|
+
else for (const [k, v] of Object.entries(rows)) c.log(k, v.valeur);
|
|
666
|
+
c.log("%cconsole :%c nodefony.socket · nodefony.sockets() · nodefony.identity()", "color:#94a3b8", "color:#5eead4;font-family:monospace");
|
|
667
|
+
c.log("%ccorrélation :%c chaque journal porte un requestId — la même valeur côté serveur relie clic, route, base et réponse", "color:#94a3b8", "color:#e2e8f0");
|
|
668
|
+
c.log("%cobjet :%c", "color:#94a3b8", "", subject);
|
|
669
|
+
} finally {
|
|
670
|
+
c.groupEnd();
|
|
671
|
+
}
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
/** Un noyau est-il vivant sur cette page ? La socket s'efface devant lui. */
|
|
675
|
+
function hasKernel() {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/realtime/channelRate.ts
|
|
680
|
+
/**
|
|
681
|
+
* Construit le nom d'un canal cadencé à partir de sa base et de la cadence demandée.
|
|
682
|
+
*
|
|
683
|
+
* `intervalMs` omis OU égal à `defaultMs` → renvoie `base` nu : on **n'émet pas** de suffixe
|
|
684
|
+
* pour la cadence par défaut, ce qui évite de fragmenter le canal de base entre consommateurs
|
|
685
|
+
* qui veulent tous la valeur par défaut (ils partagent alors un seul ticker ref-compté).
|
|
686
|
+
*
|
|
687
|
+
* @param base - canal de base (ex. `"nodefony:orm:health"`).
|
|
688
|
+
* @param intervalMs - cadence demandée (ms). Omise → canal de base.
|
|
689
|
+
* @param defaultMs - cadence par défaut du canal ; `intervalMs === defaultMs` ⇒ canal de base.
|
|
690
|
+
* @returns `base` ou `base:<ms>`.
|
|
691
|
+
*/
|
|
692
|
+
function rateChannel(base, intervalMs, defaultMs) {
|
|
693
|
+
if (intervalMs == null || intervalMs === defaultMs) return base;
|
|
694
|
+
return `${base}:${intervalMs}`;
|
|
695
|
+
}
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/client/realtime/AdaptiveRate.ts
|
|
698
|
+
/**
|
|
699
|
+
* AdaptiveRate — cadence ADAPTATIVE automatique d'un canal d'ÉTAT, **client-driven**
|
|
700
|
+
* (niveau 1, zéro changement serveur). Pendant exact de l'ABR vidéo (HLS/DASH ajustent
|
|
701
|
+
* la qualité selon le buffer) et du contrôle de congestion réseau (TCP **AIMD**).
|
|
702
|
+
*
|
|
703
|
+
* Idée : la cadence vit dans le nom du canal (cf {@link rateChannel} — 1 canal = 1 cadence
|
|
704
|
+
* = 1 ticker serveur). Si le client observe que les frames arrivent **plus lentement** que
|
|
705
|
+
* demandé (famine ⇒ event-loop serveur ou réseau sous pression), il se ré-abonne à une
|
|
706
|
+
* cadence **plus grossière** (`1s→2s→5s`) → moins de pushes → la pression retombe → le flux
|
|
707
|
+
* récupère ; quand c'est sain durablement, il remonte **doucement** vers la cadence désirée.
|
|
708
|
+
*
|
|
709
|
+
* AIMD (asymétrie anti-oscillation, comme TCP) :
|
|
710
|
+
* - **Multiplicative Decrease** : famine ⇒ on monte d'un cran l'échelle (cadence grossie),
|
|
711
|
+
* immédiat. L'échelle géométrique (×2) rend la décrue multiplicative.
|
|
712
|
+
* - **Additive Increase** : on ne redescend d'un cran (cadence affinée) qu'après une
|
|
713
|
+
* **fenêtre** de N échantillons sains → reprise lente.
|
|
714
|
+
* - **bande morte** (hystérésis) entre « sain » et « famine » : aucun changement → pas de
|
|
715
|
+
* flip-flop.
|
|
716
|
+
*
|
|
717
|
+
* Réservé aux canaux d'**ÉTAT** (latest-wins : supervision, stats) où décimer est sans perte.
|
|
718
|
+
* PAS pour les canaux d'**ÉVÉNEMENTS** (syslog, frames protocole) où chaque item compte
|
|
719
|
+
* (eux se batchent, pas se décimer).
|
|
720
|
+
*
|
|
721
|
+
* Cette classe est la **machine à états pure** (aucun timer, aucune socket) → déterministe
|
|
722
|
+
* et testable. Le câblage à une socket réelle vit dans {@link bindAdaptiveChannel}.
|
|
723
|
+
*
|
|
724
|
+
* @see {@link bindAdaptiveChannel} — glue socket + watchdog.
|
|
725
|
+
* @see {@link rateChannel} — convention de nommage cadencé partagée client↔serveur.
|
|
726
|
+
*/
|
|
727
|
+
/** Construit une échelle géométrique ×2 de `intervalMs` à `maxMs` (bornes incluses). */
|
|
728
|
+
function deriveLadder(intervalMs, maxMs) {
|
|
729
|
+
const ladder = [];
|
|
730
|
+
let ms = intervalMs;
|
|
731
|
+
while (ms < maxMs) {
|
|
732
|
+
ladder.push(ms);
|
|
733
|
+
ms *= 2;
|
|
734
|
+
}
|
|
735
|
+
ladder.push(maxMs);
|
|
736
|
+
return ladder;
|
|
737
|
+
}
|
|
738
|
+
var AdaptiveRate = class {
|
|
739
|
+
ladder;
|
|
740
|
+
starvationFactor;
|
|
741
|
+
healthyFactor;
|
|
742
|
+
recoveryWindow;
|
|
743
|
+
/** Position dans l'échelle (0 = cadence la plus fine = désirée). */
|
|
744
|
+
idx = 0;
|
|
745
|
+
/** Échantillons sains consécutifs depuis le dernier changement. */
|
|
746
|
+
healthy = 0;
|
|
747
|
+
/** Timestamp (ms) de la dernière frame ; `null` = pas de référence (post-switch/init). */
|
|
748
|
+
lastFrameTs = null;
|
|
749
|
+
constructor(options) {
|
|
750
|
+
const maxMs = options.maxMs ?? 6e4;
|
|
751
|
+
const ladder = options.ladder && options.ladder.length > 0 ? [...options.ladder].sort((a, b) => a - b) : deriveLadder(options.intervalMs, Math.max(options.intervalMs, maxMs));
|
|
752
|
+
this.ladder = ladder;
|
|
753
|
+
this.starvationFactor = options.starvationFactor ?? 1.8;
|
|
754
|
+
this.healthyFactor = options.healthyFactor ?? 1.25;
|
|
755
|
+
this.recoveryWindow = options.recoveryWindow ?? 4;
|
|
756
|
+
}
|
|
757
|
+
/** Cadence courante (ms). */
|
|
758
|
+
current() {
|
|
759
|
+
return this.ladder[this.idx];
|
|
760
|
+
}
|
|
761
|
+
/** Décale d'un cran vers le grossier (MD) ; renvoie `true` si la cadence a changé. */
|
|
762
|
+
decrease() {
|
|
763
|
+
if (this.idx >= this.ladder.length - 1) return false;
|
|
764
|
+
this.idx += 1;
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
/** Décale d'un cran vers le fin (AI) ; renvoie `true` si la cadence a changé. */
|
|
768
|
+
increase() {
|
|
769
|
+
if (this.idx <= 0) return false;
|
|
770
|
+
this.idx -= 1;
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
/** Réamorce la mesure (après un changement de cadence). */
|
|
774
|
+
resetSample() {
|
|
775
|
+
this.lastFrameTs = null;
|
|
776
|
+
this.healthy = 0;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* À appeler à CHAQUE frame reçue. Pilote l'**AI** (reprise) et détecte aussi la famine
|
|
780
|
+
* sur frames lentes (**MD**).
|
|
781
|
+
*
|
|
782
|
+
* @param now - horloge (ms).
|
|
783
|
+
* @returns une {@link RateDecision} si la cadence doit changer, sinon `null`.
|
|
784
|
+
*/
|
|
785
|
+
noteFrame(now) {
|
|
786
|
+
const prev = this.lastFrameTs;
|
|
787
|
+
this.lastFrameTs = now;
|
|
788
|
+
if (prev === null) return null;
|
|
789
|
+
const gap = now - prev;
|
|
790
|
+
const cur = this.current();
|
|
791
|
+
if (gap > this.starvationFactor * cur) {
|
|
792
|
+
if (this.decrease()) {
|
|
793
|
+
this.resetSample();
|
|
794
|
+
return {
|
|
795
|
+
intervalMs: this.current(),
|
|
796
|
+
reason: "decrease"
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
this.healthy = 0;
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
if (gap <= this.healthyFactor * cur) {
|
|
803
|
+
this.healthy += 1;
|
|
804
|
+
if (this.healthy >= this.recoveryWindow && this.increase()) {
|
|
805
|
+
this.resetSample();
|
|
806
|
+
return {
|
|
807
|
+
intervalMs: this.current(),
|
|
808
|
+
reason: "increase"
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
this.healthy = 0;
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Watchdog : à appeler périodiquement. Détecte la famine **totale** (plus aucune frame)
|
|
818
|
+
* que {@link noteFrame} ne verrait pas (faute de frame).
|
|
819
|
+
*
|
|
820
|
+
* @param now - horloge (ms).
|
|
821
|
+
* @returns une {@link RateDecision} (toujours `"decrease"`) si la cadence doit grossir.
|
|
822
|
+
*/
|
|
823
|
+
checkStarvation(now) {
|
|
824
|
+
if (this.lastFrameTs === null) return null;
|
|
825
|
+
if (now - this.lastFrameTs > this.starvationFactor * this.current()) {
|
|
826
|
+
if (this.decrease()) {
|
|
827
|
+
this.resetSample();
|
|
828
|
+
return {
|
|
829
|
+
intervalMs: this.current(),
|
|
830
|
+
reason: "decrease"
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
const defaultScheduler = {
|
|
838
|
+
set: (cb, ms) => setInterval(cb, ms),
|
|
839
|
+
clear: (h) => clearInterval(h)
|
|
840
|
+
};
|
|
841
|
+
const defaultClock = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
842
|
+
/**
|
|
843
|
+
* Câble un {@link AdaptiveRate} à une socket réelle : abonne `base` à sa cadence désirée,
|
|
844
|
+
* mesure la gigue d'arrivée, et **ré-abonne** automatiquement à une cadence plus grossière
|
|
845
|
+
* en cas de famine (MD) puis plus fine quand c'est sain (AI). Les frames continuent d'être
|
|
846
|
+
* livrées à `handler` à travers les changements de cadence.
|
|
847
|
+
*
|
|
848
|
+
* Réservé aux canaux d'ÉTAT (latest-wins). Pour les canaux d'événements, ne pas décimer.
|
|
849
|
+
*
|
|
850
|
+
* @param socket - la socket Nodefony ({@link IRealtimeSocket}).
|
|
851
|
+
* @param base - canal de base (sans suffixe de cadence).
|
|
852
|
+
* @param handler - reçoit le payload de chaque frame.
|
|
853
|
+
* @param options - cadence désirée + réglages AIMD + injections.
|
|
854
|
+
* @returns une {@link AdaptiveChannelBinding} (cadence courante + `dispose`).
|
|
855
|
+
*/
|
|
856
|
+
function bindAdaptiveChannel(socket, base, handler, options) {
|
|
857
|
+
if (options.enabled === false) {
|
|
858
|
+
const channel = rateChannel(base, options.intervalMs, options.defaultMs);
|
|
859
|
+
socket.subscribe(channel);
|
|
860
|
+
const offFixed = socket.on(channel, handler);
|
|
861
|
+
options.onRate?.(options.intervalMs, "init");
|
|
862
|
+
let downFixed = false;
|
|
863
|
+
return {
|
|
864
|
+
get intervalMs() {
|
|
865
|
+
return options.intervalMs;
|
|
866
|
+
},
|
|
867
|
+
get channel() {
|
|
868
|
+
return channel;
|
|
869
|
+
},
|
|
870
|
+
dispose() {
|
|
871
|
+
if (downFixed) return;
|
|
872
|
+
downFixed = true;
|
|
873
|
+
offFixed();
|
|
874
|
+
socket.unsubscribe(channel);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const clock = options.clock ?? defaultClock;
|
|
879
|
+
const scheduler = options.scheduler ?? defaultScheduler;
|
|
880
|
+
const ar = new AdaptiveRate(options);
|
|
881
|
+
let currentMs = ar.current();
|
|
882
|
+
let currentChannel = rateChannel(base, currentMs, options.defaultMs);
|
|
883
|
+
let off = null;
|
|
884
|
+
let watchdog;
|
|
885
|
+
let disposed = false;
|
|
886
|
+
const wrapped = (...args) => {
|
|
887
|
+
const decision = ar.noteFrame(clock());
|
|
888
|
+
handler(...args);
|
|
889
|
+
if (decision) applyDecision(decision);
|
|
890
|
+
};
|
|
891
|
+
const armWatchdog = () => {
|
|
892
|
+
if (watchdog !== void 0) scheduler.clear(watchdog);
|
|
893
|
+
watchdog = scheduler.set(() => {
|
|
894
|
+
const decision = ar.checkStarvation(clock());
|
|
895
|
+
if (decision) applyDecision(decision);
|
|
896
|
+
}, currentMs);
|
|
897
|
+
};
|
|
898
|
+
function applyDecision(decision) {
|
|
899
|
+
if (disposed) return;
|
|
900
|
+
const nextChannel = rateChannel(base, decision.intervalMs, options.defaultMs);
|
|
901
|
+
currentMs = decision.intervalMs;
|
|
902
|
+
if (nextChannel !== currentChannel) {
|
|
903
|
+
const oldChannel = currentChannel;
|
|
904
|
+
const oldOff = off;
|
|
905
|
+
socket.subscribe(nextChannel);
|
|
906
|
+
off = socket.on(nextChannel, wrapped);
|
|
907
|
+
oldOff?.();
|
|
908
|
+
socket.unsubscribe(oldChannel);
|
|
909
|
+
currentChannel = nextChannel;
|
|
910
|
+
}
|
|
911
|
+
armWatchdog();
|
|
912
|
+
options.onRate?.(currentMs, decision.reason);
|
|
913
|
+
}
|
|
914
|
+
socket.subscribe(currentChannel);
|
|
915
|
+
off = socket.on(currentChannel, wrapped);
|
|
916
|
+
armWatchdog();
|
|
917
|
+
options.onRate?.(currentMs, "init");
|
|
918
|
+
return {
|
|
919
|
+
get intervalMs() {
|
|
920
|
+
return currentMs;
|
|
921
|
+
},
|
|
922
|
+
get channel() {
|
|
923
|
+
return currentChannel;
|
|
924
|
+
},
|
|
925
|
+
dispose() {
|
|
926
|
+
if (disposed) return;
|
|
927
|
+
disposed = true;
|
|
928
|
+
if (watchdog !== void 0) scheduler.clear(watchdog);
|
|
929
|
+
off?.();
|
|
930
|
+
socket.unsubscribe(currentChannel);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/realtime/platformChannels.ts
|
|
936
|
+
/**
|
|
937
|
+
* Canaux de **diffusion** de la plateforme (le serveur pousse, les écrans regardent).
|
|
938
|
+
*
|
|
939
|
+
* Les valeurs sont des **bases** : trois d'entre elles se déclinent par cible
|
|
940
|
+
* (`…@<pid>` pour un forage sur un process, `…@<id>` pour un job) et toutes acceptent
|
|
941
|
+
* le suffixe de cadence `:<ms>` (cf {@link rateChannel}). Composer, jamais concaténer
|
|
942
|
+
* un préfixe à la main.
|
|
943
|
+
*/
|
|
944
|
+
const PLATFORM_CHANNELS = {
|
|
945
|
+
/** Journaux du pod (flux de `Pdu`, coalescé par le pont syslog). */
|
|
946
|
+
syslog: "nodefony:syslog",
|
|
947
|
+
/** Journal d'audit sécurité — plancher super-admin, un cran au-dessus des autres. */
|
|
948
|
+
audit: "nodefony:audit",
|
|
949
|
+
/** Métriques agrégées du tableau de bord (vue d'ensemble). */
|
|
950
|
+
dashboard: "nodefony:dashboard",
|
|
951
|
+
/** Supervision des process du cluster ; forage par process : `…@<pid>`. */
|
|
952
|
+
supervision: "nodefony:supervision",
|
|
953
|
+
/** Sondes de la barre de debug (mêmes mesures que la supervision, ticker séparé). */
|
|
954
|
+
debugbar: "nodefony:debugbar",
|
|
955
|
+
/** Auto-observabilité de la socket elle-même (canaux, abonnés, fan-out, backpressure). */
|
|
956
|
+
socket: "nodefony:socket",
|
|
957
|
+
/** Santé des connecteurs de base (état, ping). */
|
|
958
|
+
ormHealth: "nodefony:orm:health",
|
|
959
|
+
/** Flux ORM (débit de requêtes, latence, requêtes lentes) — plus dynamique que la santé. */
|
|
960
|
+
ormFlow: "nodefony:orm:flow",
|
|
961
|
+
/** Forage détaillé d'un connecteur sur un process donné : `…@<pid>`. */
|
|
962
|
+
ormRich: "nodefony:orm:rich",
|
|
963
|
+
/** Sortie d'un job de génération de code : `…@<jobId>` (développement uniquement). */
|
|
964
|
+
scaffoldJob: "nodefony:scaffold:job"
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* Méthodes **RPC** de la plateforme (le client demande, le serveur répond).
|
|
968
|
+
*
|
|
969
|
+
* Elles portent la même marque que les canaux pour la même raison : ce sont des
|
|
970
|
+
* surfaces du serveur, pas de l'application. `nodefony:kernel:gc` déclenche un cycle
|
|
971
|
+
* de ramasse-miettes bloquant — un nom aussi lourd de conséquences n'a rien à faire
|
|
972
|
+
* dans l'espace de nommage où une application déclare ses propres actions.
|
|
973
|
+
*
|
|
974
|
+
* ⚠️ À ne pas confondre avec les **notifications du protocole** (`realtime:welcome`,
|
|
975
|
+
* `realtime:denied`) : celles-là décrivent la mécanique de la socket elle-même, pas
|
|
976
|
+
* une surface de la plateforme, et ne sont ni des canaux ni des actions.
|
|
977
|
+
*/
|
|
978
|
+
const PLATFORM_METHODS = {
|
|
979
|
+
/** Liveness + mesure du temps d'aller-retour. */
|
|
980
|
+
ping: "nodefony:kernel:ping",
|
|
981
|
+
/** Force un cycle GC (seulement si le pod tourne avec `--expose-gc`). */
|
|
982
|
+
gc: "nodefony:kernel:gc",
|
|
983
|
+
/** Ce qu'un scaffold écrirait — fichiers créés, diff des réécritures — sans rien écrire. */
|
|
984
|
+
scaffoldPreview: "nodefony:scaffold:preview",
|
|
985
|
+
/** Démarre un job de génération de code et rend son identifiant. */
|
|
986
|
+
scaffoldRun: "nodefony:scaffold:run",
|
|
987
|
+
/** Demande l'arrêt d'un job de génération en cours. */
|
|
988
|
+
scaffoldCancel: "nodefony:scaffold:cancel"
|
|
989
|
+
};
|
|
990
|
+
/**
|
|
991
|
+
* Événements **DOM** de la plateforme (`window`), dans le même espace de nommage.
|
|
992
|
+
*
|
|
993
|
+
* Ils ne passent pas par la socket — ce sont des `CustomEvent` que le navigateur
|
|
994
|
+
* fait circuler entre la barre de debug, le pont HMR et l'écran qui les écoute.
|
|
995
|
+
* Ils portent la marque pour la même raison que les canaux : `window` est un
|
|
996
|
+
* espace partagé avec l'application, et un nom d'événement générique s'y
|
|
997
|
+
* télescoperait.
|
|
998
|
+
*/
|
|
999
|
+
const PLATFORM_EVENTS = {
|
|
1000
|
+
/** Un hot-update Vite vient d'être appliqué (émis par le pont du template). */
|
|
1001
|
+
hmr: "nodefony:hmr",
|
|
1002
|
+
/** L'utilisateur a choisi une entrée dans la barre de debug. */
|
|
1003
|
+
debugbarSelect: "nodefony:debugbar:select"
|
|
1004
|
+
};
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/client/realtime/RealtimeClient.ts
|
|
1007
|
+
/**
|
|
1008
|
+
* RealtimeClient — Core isomorphe Nodefony côté navigateur (P14.11).
|
|
1009
|
+
*
|
|
1010
|
+
* Protocole : JSON-RPC 2.0 maison sur WebSocket (+ HTTP long-polling fallback TODO P13).
|
|
1011
|
+
* State machine : disconnected → connecting → connected → reconnecting → error.
|
|
1012
|
+
*
|
|
1013
|
+
* Features :
|
|
1014
|
+
* - reconnect exponentiel avec back-off
|
|
1015
|
+
* - pub/sub (on/off/emit) — notifications JSON-RPC sans id
|
|
1016
|
+
* - request/response — RPC bidirectionnel typé
|
|
1017
|
+
* - streaming — chunks `{ id, stream: { chunk, done } }` pour LLM token-by-token
|
|
1018
|
+
*
|
|
1019
|
+
* Le backend WS de référence (RealtimeService) sera implémenté en P13.4.
|
|
1020
|
+
* En attendant, ce client peut parler à n'importe quel serveur JSON-RPC 2.0.
|
|
1021
|
+
*/
|
|
1022
|
+
/** Taille max du ring du log protocole (inspecteur realtime). */
|
|
1023
|
+
const FRAME_LOG_MAX = 300;
|
|
1024
|
+
/** Clés sensibles masquées dans le log protocole (sécurité — jamais de secret en clair). */
|
|
1025
|
+
const FRAME_REDACT_RE = /(token|password|secret|api[_-]?key|apikey|authorization|bearer)/i;
|
|
1026
|
+
/** Copie d'un payload JSON-RPC avec masquage des champs sensibles (bornée en profondeur). */
|
|
1027
|
+
function redactFrame(value, depth = 0) {
|
|
1028
|
+
if (depth > 4 || value === null || typeof value !== "object") return value;
|
|
1029
|
+
if (Array.isArray(value)) return value.map((v) => redactFrame(v, depth + 1));
|
|
1030
|
+
const out = {};
|
|
1031
|
+
for (const [k, v] of Object.entries(value)) out[k] = FRAME_REDACT_RE.test(k) ? "[redacted]" : redactFrame(v, depth + 1);
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
var RealtimeClient = class RealtimeClient {
|
|
1035
|
+
opts;
|
|
1036
|
+
transport = null;
|
|
1037
|
+
transportFactory;
|
|
1038
|
+
_state = "disconnected";
|
|
1039
|
+
peer = new JsonRpcPeer({
|
|
1040
|
+
send: (frame) => this.send(frame),
|
|
1041
|
+
onNotification: (method, params) => this.dispatchNotification(method, params)
|
|
1042
|
+
});
|
|
1043
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1044
|
+
_subscriptions = /* @__PURE__ */ new Map();
|
|
1045
|
+
_welcomed = false;
|
|
1046
|
+
reconnectAttempt = 0;
|
|
1047
|
+
_nextRetryAt = null;
|
|
1048
|
+
reconnectTimer = null;
|
|
1049
|
+
heartbeatTimer = null;
|
|
1050
|
+
intentionalClose = false;
|
|
1051
|
+
_stats = /* @__PURE__ */ new Map();
|
|
1052
|
+
_framesReceived = 0;
|
|
1053
|
+
_framesUnsent = 0;
|
|
1054
|
+
_unsentNotified = true;
|
|
1055
|
+
_lastFrameAt = null;
|
|
1056
|
+
_lastFrameMethod = null;
|
|
1057
|
+
statsTimer = null;
|
|
1058
|
+
_prevSampled = /* @__PURE__ */ new Map();
|
|
1059
|
+
_rawFrames = null;
|
|
1060
|
+
_identity = null;
|
|
1061
|
+
_serverChannels = null;
|
|
1062
|
+
_serverMethods = null;
|
|
1063
|
+
constructor(opts = {}, transportFactory) {
|
|
1064
|
+
this.opts = opts;
|
|
1065
|
+
this.transportFactory = transportFactory ?? ((url) => new BrowserWsTransport(url));
|
|
1066
|
+
this.startStatsSampler();
|
|
1067
|
+
announceRealtime(this.opts.banner);
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Instance de connexion **partagée par URL** (résolue en absolu) sur
|
|
1071
|
+
* `globalThis`. Plusieurs consommateurs d'une même page (ex. Studio + barre de
|
|
1072
|
+
* debug) obtiennent la MÊME instance → **une seule socket** WebSocket. Les
|
|
1073
|
+
* options ne s'appliquent qu'à la 1ʳᵉ création (les suivantes réutilisent).
|
|
1074
|
+
*
|
|
1075
|
+
* @param opts - options (au moins `url`), appliquées seulement à la création.
|
|
1076
|
+
* @returns l'instance partagée pour cette URL.
|
|
1077
|
+
*/
|
|
1078
|
+
static shared(opts = {}) {
|
|
1079
|
+
const key = RealtimeClient.resolveUrl(opts.url);
|
|
1080
|
+
const g = globalThis;
|
|
1081
|
+
const map = g.__nfRealtime__ ??= /* @__PURE__ */ new Map();
|
|
1082
|
+
let client = map.get(key);
|
|
1083
|
+
if (!client) {
|
|
1084
|
+
client = new RealtimeClient({
|
|
1085
|
+
...opts,
|
|
1086
|
+
url: key
|
|
1087
|
+
});
|
|
1088
|
+
map.set(key, client);
|
|
1089
|
+
}
|
|
1090
|
+
return client;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Message d'une adresse manquante — LA seule formulation, employée par les
|
|
1094
|
+
* deux chemins qui peuvent la constater.
|
|
1095
|
+
*
|
|
1096
|
+
* Il n'y a **pas** de valeur par défaut, et c'est délibéré : la route dépend
|
|
1097
|
+
* de l'application, pas du framework. Deviner `/nodefony/api/realtime` — ce
|
|
1098
|
+
* que faisait ce client — donnait une socket qui ne se connecte jamais et se
|
|
1099
|
+
* contente de retenter, sans un mot. Un échec franc coûte trente secondes ;
|
|
1100
|
+
* une socket silencieusement morte coûte une soirée.
|
|
1101
|
+
*/
|
|
1102
|
+
static missingUrl() {
|
|
1103
|
+
return /* @__PURE__ */ new Error("[nodefony] adresse du serveur temps réel manquante.\n Donne-la explicitement : RealtimeClient.shared({ url: \"/api/live/realtime\" })\n ou, en React : <NodefonyProvider url=\"/api/live/realtime\">\n Il n'y a pas de valeur par défaut : la route dépend de ton application (une application générée monte /api/live/realtime, la console d'administration /nodefony/studio/api/realtime).");
|
|
1104
|
+
}
|
|
1105
|
+
/** Résout une URL (relative ou absolue) en chaîne absolue stable = clé du singleton. */
|
|
1106
|
+
static resolveUrl(url) {
|
|
1107
|
+
if (!url) throw RealtimeClient.missingUrl();
|
|
1108
|
+
if (typeof window === "undefined") return url;
|
|
1109
|
+
try {
|
|
1110
|
+
const u = new URL(url, window.location.href);
|
|
1111
|
+
if (u.protocol === "http:") u.protocol = "ws:";
|
|
1112
|
+
else if (u.protocol === "https:") u.protocol = "wss:";
|
|
1113
|
+
return u.toString();
|
|
1114
|
+
} catch {
|
|
1115
|
+
throw new Error(`[nodefony] adresse du serveur temps réel illisible : ${JSON.stringify(url)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
get state() {
|
|
1119
|
+
return this._state;
|
|
1120
|
+
}
|
|
1121
|
+
/** Nombre de tentatives de reconnexion depuis la dernière connexion réussie. */
|
|
1122
|
+
get reconnectAttempts() {
|
|
1123
|
+
return this.reconnectAttempt;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Timestamp (ms epoch) de la prochaine tentative de reconnexion planifiée,
|
|
1127
|
+
* `null` hors backoff. Permet un compte à rebours UI synchronisé au vrai délai.
|
|
1128
|
+
*/
|
|
1129
|
+
get nextRetryAt() {
|
|
1130
|
+
return this._nextRetryAt;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Force une reconnexion **immédiate** (annule le backoff en cours). No-op si
|
|
1134
|
+
* déjà connecté/connexion en cours.
|
|
1135
|
+
*/
|
|
1136
|
+
retryNow() {
|
|
1137
|
+
if (this._state === "connected" || this._state === "connecting") return;
|
|
1138
|
+
if (this.reconnectTimer) {
|
|
1139
|
+
clearTimeout(this.reconnectTimer);
|
|
1140
|
+
this.reconnectTimer = null;
|
|
1141
|
+
}
|
|
1142
|
+
this._nextRetryAt = null;
|
|
1143
|
+
this.intentionalClose = false;
|
|
1144
|
+
this.openSocket().catch(() => {});
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Ouvre la connexion WS. Idempotent — si déjà ouverte, no-op.
|
|
1148
|
+
* Resolve une fois `connected` ou reject sur erreur fatale.
|
|
1149
|
+
*/
|
|
1150
|
+
async connect(url) {
|
|
1151
|
+
if (this._state === "connected" || this._state === "connecting") return;
|
|
1152
|
+
this.intentionalClose = false;
|
|
1153
|
+
this.opts.url = url ?? this.requireUrl();
|
|
1154
|
+
return this.openSocket();
|
|
1155
|
+
}
|
|
1156
|
+
/** L'URL configurée, ou l'échec franc — jamais une valeur devinée. */
|
|
1157
|
+
requireUrl() {
|
|
1158
|
+
if (!this.opts.url) throw RealtimeClient.missingUrl();
|
|
1159
|
+
return this.opts.url;
|
|
1160
|
+
}
|
|
1161
|
+
disconnect() {
|
|
1162
|
+
this.intentionalClose = true;
|
|
1163
|
+
this.clearTimers();
|
|
1164
|
+
this.transport?.close(1e3, "client disconnect");
|
|
1165
|
+
this.peer.dispose("client disconnect");
|
|
1166
|
+
this._welcomed = false;
|
|
1167
|
+
this._identity = null;
|
|
1168
|
+
this._serverChannels = null;
|
|
1169
|
+
this._serverMethods = null;
|
|
1170
|
+
this.fireLocal(LOCAL_EVENTS.identity, null);
|
|
1171
|
+
this.setState("disconnected");
|
|
1172
|
+
}
|
|
1173
|
+
/** Pub/sub local — handler sur un event server-pushed (notification JSON-RPC). */
|
|
1174
|
+
on(event, handler) {
|
|
1175
|
+
if (!this.handlers.has(event)) this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
1176
|
+
this.handlers.get(event).add(handler);
|
|
1177
|
+
return () => this.off(event, handler);
|
|
1178
|
+
}
|
|
1179
|
+
off(event, handler) {
|
|
1180
|
+
this.handlers.get(event)?.delete(handler);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* S'abonne aux **notices normalisées** émises par le client : criticités qui
|
|
1184
|
+
* cassent le temps réel (close codes RFC 6455 interprétés via
|
|
1185
|
+
* {@link closeCodeToNotice}), erreurs serveur poussées, rétablissement de
|
|
1186
|
+
* connexion. Réutilisable par toute app — le centre de notifications (snackbar
|
|
1187
|
+
* Studio) s'y branche directement.
|
|
1188
|
+
*
|
|
1189
|
+
* @param handler - reçoit chaque {@link NodefonyNotice}.
|
|
1190
|
+
* @returns dispose (désabonnement).
|
|
1191
|
+
*/
|
|
1192
|
+
onNotice(handler) {
|
|
1193
|
+
return this.on(LOCAL_EVENTS.notice, handler);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* S'abonne aux **refus de canal** poussés par le serveur (`realtime:denied`) :
|
|
1197
|
+
* un `subscribe`/push vers un canal protégé sans droit suffisant. Permet une
|
|
1198
|
+
* réaction CIBLÉE par canal (griser un contrôle, demander une élévation),
|
|
1199
|
+
* complémentaire de {@link onNotice} (UX générique). Event LOCAL `__denied__`.
|
|
1200
|
+
*
|
|
1201
|
+
* Polymorphisme CLIENT : la réaction est pluggable ici ; l'AUTORITÉ reste
|
|
1202
|
+
* serveur (le firewall a déjà décidé, le motif est générique — pas d'oracle).
|
|
1203
|
+
*
|
|
1204
|
+
* @param handler - reçoit `{ channel, reason }`.
|
|
1205
|
+
* @returns dispose (désabonnement).
|
|
1206
|
+
*/
|
|
1207
|
+
onDenied(handler) {
|
|
1208
|
+
return this.on(LOCAL_EVENTS.denied, handler);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Notification one-way client → server (pas de réponse attendue).
|
|
1212
|
+
*
|
|
1213
|
+
* C'est la SEULE émission dont personne n'apprend l'échec autrement : une
|
|
1214
|
+
* requête corrélée est rejetée, un (dés)abonnement est rejoué au reconnect —
|
|
1215
|
+
* une notification applicative perdue, elle, ne laisse aucune trace. Si le
|
|
1216
|
+
* transport est fermé, l'utilisateur en est donc averti (une fois par épisode
|
|
1217
|
+
* de coupure, cf {@link send}).
|
|
1218
|
+
*/
|
|
1219
|
+
emit(method, params) {
|
|
1220
|
+
if (!this._emitRaw(method, params)) this.noticeUnsent();
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Émission interne sans typage strict — utilisée pour les notifications système
|
|
1224
|
+
* (`subscribe`, `unsubscribe`, `ping`) qui ne figurent pas dans la map `Emit`
|
|
1225
|
+
* utilisateur. Bypasse les types conditionnels de {@link emit}.
|
|
1226
|
+
*
|
|
1227
|
+
* Ces frames-là **ne préviennent pas** l'utilisateur quand elles se perdent :
|
|
1228
|
+
* les abonnements sont rejoués à la reconnexion et le heartbeat est arrêté
|
|
1229
|
+
* avec les timers. Avertir ferait paraître comme une perte ce que le client
|
|
1230
|
+
* rattrape tout seul — du bruit dans le centre de notifications d'une app qui
|
|
1231
|
+
* monte et démonte des vues (le cas de Studio).
|
|
1232
|
+
*
|
|
1233
|
+
* @returns `false` si le transport n'a pas émis la frame.
|
|
1234
|
+
*/
|
|
1235
|
+
_emitRaw(method, params) {
|
|
1236
|
+
return this.peer.notify(method, params);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Émet sur un canal — verbe socket de {@link IRealtimeSocket.publish}. Côté client =
|
|
1240
|
+
* notification au serveur (alias clair de {@link emit}).
|
|
1241
|
+
*/
|
|
1242
|
+
publish(channel, payload) {
|
|
1243
|
+
this.emit(channel, payload);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* S'abonne à un canal pub/sub serveur (**ref-compté**). Émet la notification
|
|
1247
|
+
* `subscribe` au serveur UNIQUEMENT au 1er consommateur du canal ; les suivants
|
|
1248
|
+
* ne font qu'incrémenter le compteur. Ré-émis automatiquement à chaque
|
|
1249
|
+
* `realtime:welcome` — jamais à la simple ouverture de la socket, où le serveur
|
|
1250
|
+
* le jetterait sans un mot. NE remplace PAS {@link on} : `on(channel, h)` REÇOIT les
|
|
1251
|
+
* messages, `subscribe(channel)` DEMANDE au serveur de les pousser.
|
|
1252
|
+
*
|
|
1253
|
+
* Autorité unique partagée par le binding `nodefony/react` ET le store Studio →
|
|
1254
|
+
* deux consommateurs du même canal ne se coupent plus l'un l'autre.
|
|
1255
|
+
*/
|
|
1256
|
+
subscribe(channel) {
|
|
1257
|
+
const c = channel;
|
|
1258
|
+
const n = (this._subscriptions.get(c) ?? 0) + 1;
|
|
1259
|
+
this._subscriptions.set(c, n);
|
|
1260
|
+
if (n === 1 && this._welcomed) this._emitRaw("subscribe", { channel: c });
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Désabonne un consommateur d'un canal (ref-compté) : émet `unsubscribe` au
|
|
1264
|
+
* serveur seulement au **dernier** consommateur. No-op si le canal n'est pas suivi.
|
|
1265
|
+
*/
|
|
1266
|
+
unsubscribe(channel) {
|
|
1267
|
+
const c = channel;
|
|
1268
|
+
const cur = this._subscriptions.get(c);
|
|
1269
|
+
if (!cur) return;
|
|
1270
|
+
if (cur <= 1) {
|
|
1271
|
+
this._subscriptions.delete(c);
|
|
1272
|
+
if (this._welcomed) this._emitRaw("unsubscribe", { channel: c });
|
|
1273
|
+
} else this._subscriptions.set(c, cur - 1);
|
|
1274
|
+
}
|
|
1275
|
+
/** Canaux actuellement abonnés (≥ 1 consommateur). Lecture seule. */
|
|
1276
|
+
get subscribedChannels() {
|
|
1277
|
+
return Array.from(this._subscriptions.keys());
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* L'adresse du serveur temps réel, telle qu'elle a été donnée (ou résolue en
|
|
1281
|
+
* absolu par {@link shared}). `null` tant qu'aucune n'a été fournie — le
|
|
1282
|
+
* framework n'en devine aucune.
|
|
1283
|
+
*
|
|
1284
|
+
* Publiée pour l'auto-observation : un instantané de socket qui ne dit pas DE
|
|
1285
|
+
* QUELLE socket il parle ne sert à rien dès qu'une page en tient plus d'une.
|
|
1286
|
+
*/
|
|
1287
|
+
get url() {
|
|
1288
|
+
return this.opts.url ?? null;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Identité de la connexion, **annoncée par le serveur** au `realtime:welcome`
|
|
1292
|
+
* ({@link RealtimeIdentity}). `null` tant qu'aucun welcome n'a été reçu ; une
|
|
1293
|
+
* fois reçu, un visiteur anonyme a `authenticated: false` (jamais `null`). Un
|
|
1294
|
+
* consommateur (Studio) sait ainsi s'il doit afficher le login **sans taper de
|
|
1295
|
+
* route**. Rafraîchie à chaque (re)connexion ; remise à `null` au
|
|
1296
|
+
* {@link disconnect} volontaire (logout).
|
|
1297
|
+
*/
|
|
1298
|
+
get identity() {
|
|
1299
|
+
return this._identity;
|
|
1300
|
+
}
|
|
1301
|
+
/** Canaux pub/sub annoncés par le serveur au welcome (découverte). `null` si pas (encore) reçu. */
|
|
1302
|
+
get serverChannels() {
|
|
1303
|
+
return this._serverChannels;
|
|
1304
|
+
}
|
|
1305
|
+
/** Actions RPC annoncées par le serveur au welcome (découverte). `null` si pas (encore) reçu. */
|
|
1306
|
+
get serverMethods() {
|
|
1307
|
+
return this._serverMethods;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* S'abonne à l'identité résolue (event LOCAL `__identity__`, jamais réseau) :
|
|
1311
|
+
* le handler est rappelé à chaque (re)welcome et au `disconnect()`. Permet à
|
|
1312
|
+
* l'UI de basculer anonyme↔authentifié sans polling ni route `/auth/me`.
|
|
1313
|
+
*
|
|
1314
|
+
* @param handler - reçoit la {@link RealtimeIdentity} courante (ou `null`).
|
|
1315
|
+
* @returns dispose (désabonnement).
|
|
1316
|
+
*/
|
|
1317
|
+
onIdentity(handler) {
|
|
1318
|
+
return this.on(LOCAL_EVENTS.identity, handler);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* S'abonne à l'**état de la connexion** (event LOCAL `__state__`, jamais réseau) :
|
|
1322
|
+
* le handler est rappelé à chaque transition (`connecting`, `connected`,
|
|
1323
|
+
* `reconnecting`, `error`, `disconnected`), jamais pour un état inchangé.
|
|
1324
|
+
*
|
|
1325
|
+
* C'est la porte à employer partout — un badge de connexion, une liaison de vue,
|
|
1326
|
+
* une barre de debug. Écrire `on("__state__", …)` en clair marche aussi, mais
|
|
1327
|
+
* recopie un nom d'événement interne dans du code applicatif : c'est ce qui a
|
|
1328
|
+
* fait diverger les trois gabarits d'application.
|
|
1329
|
+
*
|
|
1330
|
+
* @param handler - reçoit le nouvel {@link RealtimeState}.
|
|
1331
|
+
* @returns dispose (désabonnement).
|
|
1332
|
+
*/
|
|
1333
|
+
onState(handler) {
|
|
1334
|
+
return this.on(LOCAL_EVENTS.state, handler);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* S'abonne au **tick d'échantillonnage des statistiques** (event LOCAL
|
|
1338
|
+
* `__stats__`, jamais réseau), émis une fois par seconde après recalcul des
|
|
1339
|
+
* débits par canal. Sans charge utile : lire ensuite {@link getChannelStats}
|
|
1340
|
+
* ou {@link getStats}, dont les valeurs viennent d'être rafraîchies.
|
|
1341
|
+
*
|
|
1342
|
+
* @param handler - appelé à chaque échantillon.
|
|
1343
|
+
* @returns dispose (désabonnement).
|
|
1344
|
+
*/
|
|
1345
|
+
onStats(handler) {
|
|
1346
|
+
return this.on(LOCAL_EVENTS.stats, handler);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* S'abonne aux **tentatives de reconnexion programmées** (event LOCAL
|
|
1350
|
+
* `__reconnect__`, jamais réseau) : numéro d'essai, délai retenu par le
|
|
1351
|
+
* back-off, et échéance absolue. De quoi afficher un compte à rebours plutôt
|
|
1352
|
+
* qu'un « déconnecté » muet.
|
|
1353
|
+
*
|
|
1354
|
+
* @param handler - reçoit `{ attempt, delay, nextRetryAt }`.
|
|
1355
|
+
* @returns dispose (désabonnement).
|
|
1356
|
+
*/
|
|
1357
|
+
onReconnect(handler) {
|
|
1358
|
+
return this.on(LOCAL_EVENTS.reconnect, handler);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Handle « socket-like » d'un canal ({@link IRealtimeChannel}) — fine liaison sur
|
|
1362
|
+
* les primitives (`subscribe`/`on`/`publish`/`unsubscribe`), forme naturelle des
|
|
1363
|
+
* canaux à état (SIP, bridge) et point d'accroche des couches à venir (codec,
|
|
1364
|
+
* cadence, politique). N'ouvre rien : appeler `.open()` pour s'abonner. `kind` reste
|
|
1365
|
+
* indéfini côté client tant que le serveur ne l'annonce pas.
|
|
1366
|
+
*/
|
|
1367
|
+
channel(name) {
|
|
1368
|
+
const hub = this;
|
|
1369
|
+
const disposers = /* @__PURE__ */ new Set();
|
|
1370
|
+
return {
|
|
1371
|
+
name,
|
|
1372
|
+
on(handler) {
|
|
1373
|
+
const dispose = hub.on(name, handler);
|
|
1374
|
+
disposers.add(dispose);
|
|
1375
|
+
return () => {
|
|
1376
|
+
dispose();
|
|
1377
|
+
disposers.delete(dispose);
|
|
1378
|
+
};
|
|
1379
|
+
},
|
|
1380
|
+
send(payload) {
|
|
1381
|
+
hub.publish(name, payload);
|
|
1382
|
+
},
|
|
1383
|
+
open() {
|
|
1384
|
+
hub.subscribe(name);
|
|
1385
|
+
},
|
|
1386
|
+
close() {
|
|
1387
|
+
for (const d of disposers) d();
|
|
1388
|
+
disposers.clear();
|
|
1389
|
+
hub.unsubscribe(name);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* S'abonne à un canal d'ÉTAT en **cadence adaptative** (AIMD client-driven) : la lib
|
|
1395
|
+
* mesure la gigue d'arrivée et ré-abonne automatiquement à une cadence plus grossière en
|
|
1396
|
+
* cas de famine, plus fine quand c'est sain — sans changement serveur. `handler` reçoit
|
|
1397
|
+
* les frames à travers les changements de cadence. Réservé aux canaux latest-wins.
|
|
1398
|
+
*
|
|
1399
|
+
* @param base - canal de base (sans suffixe de cadence).
|
|
1400
|
+
* @param handler - reçoit le payload de chaque frame.
|
|
1401
|
+
* @param options - cadence désirée + réglages AIMD (cf {@link BindAdaptiveOptions}).
|
|
1402
|
+
* @returns une poignée {@link AdaptiveChannelBinding} (cadence courante + `dispose`).
|
|
1403
|
+
*/
|
|
1404
|
+
adaptiveChannel(base, handler, options) {
|
|
1405
|
+
return bindAdaptiveChannel(this, base, handler, options);
|
|
1406
|
+
}
|
|
1407
|
+
async request(method, params, timeoutMs = 3e4) {
|
|
1408
|
+
if (method.charCodeAt(0) === 47) {
|
|
1409
|
+
if (typeof params === "number") timeoutMs = params;
|
|
1410
|
+
params = { path: method };
|
|
1411
|
+
method = "api.request";
|
|
1412
|
+
}
|
|
1413
|
+
return this.peer.request(method, params, timeoutMs);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* **Mutation** API par le pont `api.request` (POST/PUT/PATCH/DELETE) — pendant
|
|
1417
|
+
* d'écriture de {@link request} (qui ne fait que des lectures GET). Transporte
|
|
1418
|
+
* la méthode HTTP **logique**, le corps, et une **clé d'idempotence**
|
|
1419
|
+
* (OBLIGATOIRE côté serveur : une socket reconnecte et peut rejouer une frame
|
|
1420
|
+
* en vol → la clé dédoublonne le rejeu, anti double-effet) :
|
|
1421
|
+
* ```ts
|
|
1422
|
+
* await socket.mutate("/nodefony/security/api/apikeys/42/revoke", {
|
|
1423
|
+
* method: "POST", idempotencyKey: crypto.randomUUID(),
|
|
1424
|
+
* });
|
|
1425
|
+
* ```
|
|
1426
|
+
* Échec → rejet {@link RpcError} (`data.status` = statut HTTP équivalent :
|
|
1427
|
+
* 400 clé absente, 409 rejeu concurrent, 403 refus, 404 path inconnu…).
|
|
1428
|
+
*/
|
|
1429
|
+
async mutate(path, init) {
|
|
1430
|
+
return this.peer.request("api.request", {
|
|
1431
|
+
path,
|
|
1432
|
+
method: init.method,
|
|
1433
|
+
body: init.body,
|
|
1434
|
+
idempotencyKey: init.idempotencyKey
|
|
1435
|
+
}, init.timeoutMs ?? 3e4);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Appel API par la socket rendant l'**enveloppe complète** — le résultat ET
|
|
1439
|
+
* la méta serveur — là où {@link request}/{@link mutate} ne rendent que la
|
|
1440
|
+
* valeur (forme courante, contrat « snapshot ≡ GET REST »).
|
|
1441
|
+
*
|
|
1442
|
+
* Utile à l'outillage plus qu'aux applications : en développement, le serveur
|
|
1443
|
+
* joint le `requestId` du profil de CETTE frame, ce qui permet d'aller lire sa
|
|
1444
|
+
* radiographie (`GET /nodefony/profiler/api/{requestId}` — phases, SQL,
|
|
1445
|
+
* décision du firewall). En production le serveur n'émet aucune méta :
|
|
1446
|
+
* `requestId` vaut alors `null`, et l'appel coûte exactement le même trafic.
|
|
1447
|
+
*
|
|
1448
|
+
* ```ts
|
|
1449
|
+
* const { result, requestId } = await socket.call("/nodefony/kernel/api/modules");
|
|
1450
|
+
* ```
|
|
1451
|
+
*
|
|
1452
|
+
* Échec → rejet {@link RpcError}, comme {@link request} ; l'id du profil, lui,
|
|
1453
|
+
* voyage dans `error.data.requestId` (un refus se radiographie aussi).
|
|
1454
|
+
*
|
|
1455
|
+
* @param path - chemin de la route (transport WEBSOCKET requis côté serveur).
|
|
1456
|
+
* @param init - méthode logique (défaut `GET`), corps, clé d'idempotence.
|
|
1457
|
+
*/
|
|
1458
|
+
async call(path, init) {
|
|
1459
|
+
const { result, meta } = await this.peer.requestTraced("api.request", {
|
|
1460
|
+
path,
|
|
1461
|
+
method: init?.method ?? "GET",
|
|
1462
|
+
body: init?.body,
|
|
1463
|
+
idempotencyKey: init?.idempotencyKey
|
|
1464
|
+
}, init?.timeoutMs ?? 3e4);
|
|
1465
|
+
return {
|
|
1466
|
+
result,
|
|
1467
|
+
requestId: typeof meta?.requestId === "string" ? meta.requestId : null
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Mesure le round-trip WS via la méthode RPC standard `nodefony:kernel:ping` — helper
|
|
1472
|
+
* RÉUTILISABLE par tout consommateur (topbar Studio, debug bar, app user) :
|
|
1473
|
+
* la mesure du RTT et la convention `nodefony:kernel:ping` vivent dans la lib cliente,
|
|
1474
|
+
* pas dupliquées dans chaque front. Renvoie le payload serveur enrichi de `rtt`
|
|
1475
|
+
* (ms, aller-retour mesuré côté client). Lève si le serveur ne répond pas
|
|
1476
|
+
* (`request` timeout) ou ne connaît pas la méthode (`-32601`).
|
|
1477
|
+
*/
|
|
1478
|
+
async ping(timeoutMs = 5e3) {
|
|
1479
|
+
const now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
1480
|
+
const t0 = now();
|
|
1481
|
+
return {
|
|
1482
|
+
...await this.request(PLATFORM_METHODS.ping, void 0, timeoutMs),
|
|
1483
|
+
rtt: Math.round(now() - t0)
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/** Notification SORTANTE typée (pas de réponse). Pendant de {@link emit}/{@link publish}. */
|
|
1487
|
+
notify(method, params) {
|
|
1488
|
+
this.peer.notify(method, params);
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Expose une action appelable PAR LE PAIR (requête entrante serveur→client).
|
|
1492
|
+
* Cœur du duplex débloqué par L0 : sans handler, une requête entrante reçoit
|
|
1493
|
+
* `-32601` ; avec, le `result` repart au serveur (confirmation d'action,
|
|
1494
|
+
* invalidation de cache poussée, health applicatif serveur→client).
|
|
1495
|
+
*/
|
|
1496
|
+
register(method, handler) {
|
|
1497
|
+
this.peer.register(method, handler);
|
|
1498
|
+
}
|
|
1499
|
+
/** Retire une action exposée. */
|
|
1500
|
+
unregister(method) {
|
|
1501
|
+
this.peer.unregister(method);
|
|
1502
|
+
}
|
|
1503
|
+
/** Actions exposées par CE client (découverte). ≠ {@link serverMethods} (côté serveur). */
|
|
1504
|
+
get methods() {
|
|
1505
|
+
return this.peer.methods;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Ingestion d'une frame ENTRANTE déjà parsée → log + classification/route par le
|
|
1509
|
+
* moteur. Renvoie sa nature. Une frame `invalid` peut porter une erreur GLOBALE
|
|
1510
|
+
* serveur (`{jsonrpc, error}` hors spec JSON-RPC) → notice (cf {@link handleServerError}).
|
|
1511
|
+
*/
|
|
1512
|
+
receive(frame) {
|
|
1513
|
+
this.recordFrame("in", frame);
|
|
1514
|
+
const kind = this.peer.receive(frame);
|
|
1515
|
+
if (kind === "invalid") this.handleServerError(frame);
|
|
1516
|
+
return kind;
|
|
1517
|
+
}
|
|
1518
|
+
/** Annule les requêtes sortantes en attente (fermeture transport / logout). */
|
|
1519
|
+
dispose(reason) {
|
|
1520
|
+
this.peer.dispose(reason);
|
|
1521
|
+
}
|
|
1522
|
+
/** Total de notifications reçues, tous canaux confondus (welcome inclus). */
|
|
1523
|
+
get framesReceived() {
|
|
1524
|
+
return this._framesReceived;
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Total de frames qui n'ont **pas** pu partir depuis la création du client
|
|
1528
|
+
* (transport fermé au moment de l'émission). Reste à `0` en fonctionnement
|
|
1529
|
+
* normal ; toute valeur non nulle date d'une coupure ou d'une émission
|
|
1530
|
+
* antérieure à la première connexion.
|
|
1531
|
+
*/
|
|
1532
|
+
get framesUnsent() {
|
|
1533
|
+
return this._framesUnsent;
|
|
1534
|
+
}
|
|
1535
|
+
/** Timestamp (ms) de la dernière notification reçue. */
|
|
1536
|
+
get lastFrameAt() {
|
|
1537
|
+
return this._lastFrameAt;
|
|
1538
|
+
}
|
|
1539
|
+
/** `method`/canal de la dernière notification reçue. */
|
|
1540
|
+
get lastFrameMethod() {
|
|
1541
|
+
return this._lastFrameMethod;
|
|
1542
|
+
}
|
|
1543
|
+
/** Snapshot des stats par canal (msgCount/rate/series). Les objets sont les
|
|
1544
|
+
* refs internes — à LIRE, pas à muter (copier les valeurs si besoin). */
|
|
1545
|
+
getStats() {
|
|
1546
|
+
return Array.from(this._stats.values());
|
|
1547
|
+
}
|
|
1548
|
+
/** Stats d'un canal précis (== method JSON-RPC) ou `undefined`. */
|
|
1549
|
+
getChannelStats(method) {
|
|
1550
|
+
return this._stats.get(method);
|
|
1551
|
+
}
|
|
1552
|
+
/** Comptabilise une notification entrante (avant dispatch aux handlers). */
|
|
1553
|
+
trackFrame(method) {
|
|
1554
|
+
const now = Date.now();
|
|
1555
|
+
this._framesReceived++;
|
|
1556
|
+
this._lastFrameAt = now;
|
|
1557
|
+
this._lastFrameMethod = method;
|
|
1558
|
+
let st = this._stats.get(method);
|
|
1559
|
+
if (!st) {
|
|
1560
|
+
st = {
|
|
1561
|
+
method,
|
|
1562
|
+
msgCount: 0,
|
|
1563
|
+
lastMessage: null,
|
|
1564
|
+
rate: 0,
|
|
1565
|
+
series: []
|
|
1566
|
+
};
|
|
1567
|
+
this._stats.set(method, st);
|
|
1568
|
+
}
|
|
1569
|
+
st.msgCount++;
|
|
1570
|
+
st.lastMessage = now;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* (Ré)émet un `subscribe` pour chaque canal ref-compté, à la réception du
|
|
1574
|
+
* `realtime:welcome` — le seul instant où le serveur est prêt à l'enregistrer.
|
|
1575
|
+
*
|
|
1576
|
+
* Ouvrir la socket ne suffit pas : pendant que le serveur authentifie, son
|
|
1577
|
+
* transport JSON-RPC n'est pas encore branché et il **jette** les frames
|
|
1578
|
+
* entrantes, sans pouvoir répondre — c'est le contrat écrit par
|
|
1579
|
+
* `RealtimeController.handleRealtime`. Rejouer sur `onOpen` revenait à parler
|
|
1580
|
+
* dans le vide, et perdait en silence deux cas entiers : un `subscribe` posé
|
|
1581
|
+
* avant `start()`, et TOUS les abonnements après une reconnexion.
|
|
1582
|
+
*
|
|
1583
|
+
* Cold path (1×/connexion) : ne parcourt que les canaux réellement suivis, et
|
|
1584
|
+
* n'alloue rien.
|
|
1585
|
+
*/
|
|
1586
|
+
replaySubscriptions() {
|
|
1587
|
+
this._welcomed = true;
|
|
1588
|
+
for (const channel of this._subscriptions.keys()) this._emitRaw("subscribe", { channel });
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Ingère le `realtime:welcome` : mémorise l'identité résolue + les capabilities
|
|
1592
|
+
* annoncées (canaux/actions découvrables) et émet `__identity__`. Tolérant à un
|
|
1593
|
+
* welcome partiel/legacy (champs absents → `null`). Cold path (1×/connexion).
|
|
1594
|
+
*/
|
|
1595
|
+
ingestWelcome(params) {
|
|
1596
|
+
const w = params;
|
|
1597
|
+
if (!w || typeof w !== "object") return;
|
|
1598
|
+
this._identity = w.identity ?? null;
|
|
1599
|
+
this._serverChannels = Array.isArray(w.channels) ? w.channels : null;
|
|
1600
|
+
this._serverMethods = Array.isArray(w.methods) ? w.methods : null;
|
|
1601
|
+
noteServerEnv(w.env);
|
|
1602
|
+
if (this.opts.banner !== false && !hasKernel()) this.detailsSocket();
|
|
1603
|
+
this.fireLocal(LOCAL_EVENTS.identity, this._identity);
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Détail de la socket dans la console — le pendant, pour une page SANS noyau,
|
|
1607
|
+
* du groupe que le noyau émet quand il y en a un (#136).
|
|
1608
|
+
*
|
|
1609
|
+
* Une vitrine ne compose rien : sans ceci, elle n'a aucun endroit où lire son
|
|
1610
|
+
* adresse, ses canaux ou son identité, alors que c'est exactement ce qu'on
|
|
1611
|
+
* cherche quand une socket ne pousse rien. Émis au premier accueil, une seule
|
|
1612
|
+
* fois par page, et jamais en production.
|
|
1613
|
+
*/
|
|
1614
|
+
detailsSocket() {
|
|
1615
|
+
consoleDetails({
|
|
1616
|
+
adresse: { valeur: this.url ?? "—" },
|
|
1617
|
+
état: { valeur: this.state },
|
|
1618
|
+
identité: { valeur: this._identity?.authenticated ? this._identity.userIdentifier ?? "authentifié" : "anonyme" },
|
|
1619
|
+
canaux: { valeur: this._serverChannels?.join(", ") || "aucun" },
|
|
1620
|
+
actions: { valeur: this._serverMethods?.join(", ") || "aucune" }
|
|
1621
|
+
}, this, "socket nodefony — détail et raccourcis");
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Ingère un `realtime:denied` (refus d'abonnement/push poussé par le serveur) :
|
|
1625
|
+
* émet une notice normalisée ({@link onNotice}) ET un event ciblé `__denied__`
|
|
1626
|
+
* ({@link onDenied}) pour une réaction polymorphe de l'app. Tolérant à un
|
|
1627
|
+
* payload partiel (motif par défaut `forbidden`). Cold path (refus rare).
|
|
1628
|
+
*/
|
|
1629
|
+
ingestDenied(params) {
|
|
1630
|
+
const p = params;
|
|
1631
|
+
const channel = typeof p?.channel === "string" ? p.channel : "";
|
|
1632
|
+
const brut = p?.reason;
|
|
1633
|
+
const reason = brut === "unknown" || brut === "limit" ? brut : "forbidden";
|
|
1634
|
+
const detail = typeof p?.detail === "string" ? p.detail : void 0;
|
|
1635
|
+
const denied = {
|
|
1636
|
+
channel,
|
|
1637
|
+
reason,
|
|
1638
|
+
...detail === void 0 ? {} : { detail }
|
|
1639
|
+
};
|
|
1640
|
+
this.fireNotice(deniedToNotice(denied));
|
|
1641
|
+
this.fireLocal(LOCAL_EVENTS.denied, denied);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Dispatch d'une NOTIFICATION entrante — appelé par le moteur via `onNotification`.
|
|
1645
|
+
* Ordre figé : ingestion welcome (1ʳᵉ frame) → stats → handlers locaux + wildcard
|
|
1646
|
+
* (un handler `on("realtime:welcome")` voit donc l'identité déjà ingérée).
|
|
1647
|
+
*/
|
|
1648
|
+
dispatchNotification(method, params) {
|
|
1649
|
+
if (method === "realtime:welcome") {
|
|
1650
|
+
this.ingestWelcome(params);
|
|
1651
|
+
this.replaySubscriptions();
|
|
1652
|
+
} else if (method === "realtime:denied") this.ingestDenied(params);
|
|
1653
|
+
this.trackFrame(method);
|
|
1654
|
+
this.handlers.get(method)?.forEach((h) => {
|
|
1655
|
+
try {
|
|
1656
|
+
h(params);
|
|
1657
|
+
} catch {}
|
|
1658
|
+
});
|
|
1659
|
+
this.handlers.get("*")?.forEach((h) => {
|
|
1660
|
+
try {
|
|
1661
|
+
h(method, params);
|
|
1662
|
+
} catch {}
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Frame `invalid` (JSON-RPC strict) portant un `error` sans `id`/`method` = erreur
|
|
1667
|
+
* GLOBALE serveur (extension Nodefony hors spec, ex. refus tardif) → notice pour le
|
|
1668
|
+
* centre de notifications. Frame réellement malformée (sans `.error`) → no-op.
|
|
1669
|
+
*/
|
|
1670
|
+
handleServerError(frame) {
|
|
1671
|
+
const err = frame.error;
|
|
1672
|
+
if (!err || typeof err !== "object") return;
|
|
1673
|
+
this.fireNotice({
|
|
1674
|
+
level: "error",
|
|
1675
|
+
title: "Temps réel",
|
|
1676
|
+
message: err.message || "Erreur serveur temps réel",
|
|
1677
|
+
source: "server",
|
|
1678
|
+
code: err.code,
|
|
1679
|
+
ts: Date.now()
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/** Échantillonne le débit (msg/s) + série par canal, 1×/s, puis émet
|
|
1683
|
+
* `__stats__` (event local) pour notifier les consommateurs réactifs. */
|
|
1684
|
+
startStatsSampler() {
|
|
1685
|
+
if (this.statsTimer) return;
|
|
1686
|
+
this.statsTimer = setInterval(() => {
|
|
1687
|
+
for (const st of this._stats.values()) {
|
|
1688
|
+
const prev = this._prevSampled.get(st.method) ?? st.msgCount;
|
|
1689
|
+
st.rate = Math.max(0, st.msgCount - prev);
|
|
1690
|
+
this._prevSampled.set(st.method, st.msgCount);
|
|
1691
|
+
st.series = [...st.series, st.rate].slice(-32);
|
|
1692
|
+
}
|
|
1693
|
+
this.fireLocal(LOCAL_EVENTS.stats);
|
|
1694
|
+
}, 1e3);
|
|
1695
|
+
this.statsTimer.unref?.();
|
|
1696
|
+
}
|
|
1697
|
+
/** Émet une notice normalisée aux abonnés `onNotice` (event local, pas réseau). */
|
|
1698
|
+
fireNotice(notice) {
|
|
1699
|
+
this.fireLocal(LOCAL_EVENTS.notice, notice);
|
|
1700
|
+
}
|
|
1701
|
+
/** Déclenche les handlers locaux d'un event interne (pas d'envoi réseau). */
|
|
1702
|
+
fireLocal(event, ...args) {
|
|
1703
|
+
this.handlers.get(event)?.forEach((h) => {
|
|
1704
|
+
try {
|
|
1705
|
+
h(...args);
|
|
1706
|
+
} catch {}
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
openSocket() {
|
|
1710
|
+
return new Promise((resolve, reject) => {
|
|
1711
|
+
this._welcomed = false;
|
|
1712
|
+
this.setState(this.reconnectAttempt > 0 ? "reconnecting" : "connecting");
|
|
1713
|
+
let transport;
|
|
1714
|
+
try {
|
|
1715
|
+
const base = typeof window !== "undefined" ? window.location.href : "http://localhost";
|
|
1716
|
+
const url = new URL(this.requireUrl(), base);
|
|
1717
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
1718
|
+
else if (url.protocol === "https:") url.protocol = "wss:";
|
|
1719
|
+
if (this.opts.token) url.searchParams.set("token", this.opts.token);
|
|
1720
|
+
transport = this.transportFactory(url.toString());
|
|
1721
|
+
this.transport = transport;
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
this.setState("error");
|
|
1724
|
+
reject(e);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
transport.onOpen(() => {
|
|
1728
|
+
const wasReconnecting = this.reconnectAttempt > 0;
|
|
1729
|
+
this.reconnectAttempt = 0;
|
|
1730
|
+
this._nextRetryAt = null;
|
|
1731
|
+
this.setState("connected");
|
|
1732
|
+
this.startHeartbeat();
|
|
1733
|
+
if (wasReconnecting) this.fireNotice({
|
|
1734
|
+
level: "success",
|
|
1735
|
+
title: "Temps réel",
|
|
1736
|
+
message: "Connexion temps réel rétablie",
|
|
1737
|
+
source: "realtime",
|
|
1738
|
+
ts: Date.now()
|
|
1739
|
+
});
|
|
1740
|
+
resolve();
|
|
1741
|
+
});
|
|
1742
|
+
transport.onMessage((raw) => this.handleMessage(raw));
|
|
1743
|
+
transport.onError(() => {});
|
|
1744
|
+
transport.onClose((code, reason) => {
|
|
1745
|
+
this.clearTimers();
|
|
1746
|
+
this.transport = null;
|
|
1747
|
+
if (this.intentionalClose) {
|
|
1748
|
+
this.setState("disconnected");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const notice = closeCodeToNotice(code, reason);
|
|
1752
|
+
if (notice) this.fireNotice(notice);
|
|
1753
|
+
const reconnectable = isReconnectableCloseCode(code);
|
|
1754
|
+
if (reconnectable && this.opts.autoReconnect !== false) this.scheduleReconnect();
|
|
1755
|
+
else this.setState(reconnectable ? "disconnected" : "error");
|
|
1756
|
+
});
|
|
1757
|
+
transport.connect();
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
scheduleReconnect() {
|
|
1761
|
+
this.reconnectAttempt++;
|
|
1762
|
+
const base = this.opts.reconnectDelay ?? 1e3;
|
|
1763
|
+
const max = this.opts.reconnectDelayMax ?? 3e4;
|
|
1764
|
+
const delay = Math.min(base * 2 ** (this.reconnectAttempt - 1), max);
|
|
1765
|
+
this._nextRetryAt = Date.now() + delay;
|
|
1766
|
+
this.setState("reconnecting");
|
|
1767
|
+
this.fireLocal(LOCAL_EVENTS.reconnect, {
|
|
1768
|
+
attempt: this.reconnectAttempt,
|
|
1769
|
+
delay,
|
|
1770
|
+
nextRetryAt: this._nextRetryAt
|
|
1771
|
+
});
|
|
1772
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1773
|
+
this.openSocket().catch(() => {});
|
|
1774
|
+
}, delay);
|
|
1775
|
+
}
|
|
1776
|
+
startHeartbeat() {
|
|
1777
|
+
const interval = this.opts.heartbeatInterval ?? 3e4;
|
|
1778
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1779
|
+
if (this.transport?.readyState === TransportState.OPEN) this._emitRaw("ping", { ts: Date.now() });
|
|
1780
|
+
}, interval);
|
|
1781
|
+
}
|
|
1782
|
+
clearTimers() {
|
|
1783
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
1784
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
1785
|
+
this.reconnectTimer = null;
|
|
1786
|
+
this.heartbeatTimer = null;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Log protocole : les `FRAME_LOG_MAX` dernières frames JSON-RPC (redactées),
|
|
1790
|
+
* enregistrées EN CONTINU → la console « retrace l'instant » dès l'ouverture
|
|
1791
|
+
* (jamais de démarrage à vide). Construction + redaction faites ici, à la lecture.
|
|
1792
|
+
*/
|
|
1793
|
+
get frameLog() {
|
|
1794
|
+
return (this._rawFrames ?? []).map((f) => RealtimeClient.buildFrame(f.dir, f.msg, f.ts));
|
|
1795
|
+
}
|
|
1796
|
+
/** Purge le log protocole. */
|
|
1797
|
+
clearFrameLog() {
|
|
1798
|
+
if (this._rawFrames) this._rawFrames.length = 0;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Enregistre une frame dans le ring — coût = 1 push de réf (construction +
|
|
1802
|
+
* redaction DIFFÉRÉES). Émet `__frame__` (frame construite) seulement si la
|
|
1803
|
+
* console écoute.
|
|
1804
|
+
*/
|
|
1805
|
+
recordFrame(dir, msg) {
|
|
1806
|
+
const ts = Date.now();
|
|
1807
|
+
(this._rawFrames ??= []).push({
|
|
1808
|
+
ts,
|
|
1809
|
+
dir,
|
|
1810
|
+
msg
|
|
1811
|
+
});
|
|
1812
|
+
if (this._rawFrames.length > FRAME_LOG_MAX) this._rawFrames.shift();
|
|
1813
|
+
const listeners = this.handlers.get("__frame__");
|
|
1814
|
+
if (listeners && listeners.size > 0) this.fireLocal("__frame__", RealtimeClient.buildFrame(dir, msg, ts));
|
|
1815
|
+
}
|
|
1816
|
+
/** Construit une frame affichable (kind/canal/id + payload redacté). */
|
|
1817
|
+
static buildFrame(dir, msg, ts) {
|
|
1818
|
+
const m = msg ?? {};
|
|
1819
|
+
let kind = "?";
|
|
1820
|
+
let channel;
|
|
1821
|
+
if (typeof m.method === "string") {
|
|
1822
|
+
kind = m.method;
|
|
1823
|
+
const p = m.params;
|
|
1824
|
+
if (p && typeof p.channel === "string") channel = p.channel;
|
|
1825
|
+
} else if ("error" in m) kind = "error";
|
|
1826
|
+
else if ("result" in m) kind = "response";
|
|
1827
|
+
return {
|
|
1828
|
+
ts,
|
|
1829
|
+
dir,
|
|
1830
|
+
kind,
|
|
1831
|
+
id: typeof m.id === "number" ? m.id : void 0,
|
|
1832
|
+
channel,
|
|
1833
|
+
payload: redactFrame(msg)
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Émet une frame si — et seulement si — le transport est ouvert.
|
|
1838
|
+
*
|
|
1839
|
+
* La frame n'est **pas** mise en file d'attente : rejouer une intention après
|
|
1840
|
+
* coup peut être pire que la perdre (une commande obsolète appliquée en
|
|
1841
|
+
* retard, un abonnement rétabli sur un écran déjà quitté). Ce qui n'est plus
|
|
1842
|
+
* accepté, c'est de la perdre **en silence** — l'appelant reçoit `false`, une
|
|
1843
|
+
* requête corrélée est rejetée aussitôt par {@link JsonRpcPeer} plutôt qu'au
|
|
1844
|
+
* bout de son timeout, et l'utilisateur est prévenu une fois par épisode.
|
|
1845
|
+
*
|
|
1846
|
+
* @param msg - la frame JSON-RPC à sérialiser.
|
|
1847
|
+
* @returns `true` si la frame est partie, `false` si le transport était fermé.
|
|
1848
|
+
*/
|
|
1849
|
+
send(msg) {
|
|
1850
|
+
if (this.transport?.readyState !== TransportState.OPEN) {
|
|
1851
|
+
this._framesUnsent++;
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
this.transport.send(JSON.stringify(msg));
|
|
1855
|
+
this.recordFrame("out", msg);
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Avertit que des notifications applicatives se perdent — **une fois par
|
|
1860
|
+
* épisode** de coupure, jamais par frame : une vue qui pousse en boucle
|
|
1861
|
+
* remplirait sinon le centre de notifications à elle seule.
|
|
1862
|
+
*
|
|
1863
|
+
* Appelé depuis {@link emit} uniquement. Les requêtes corrélées et les
|
|
1864
|
+
* (dés)abonnements ont déjà leur propre voie (rejet, rejeu au reconnect).
|
|
1865
|
+
*/
|
|
1866
|
+
noticeUnsent() {
|
|
1867
|
+
if (this._unsentNotified) return;
|
|
1868
|
+
this._unsentNotified = true;
|
|
1869
|
+
this.fireNotice({
|
|
1870
|
+
level: "warning",
|
|
1871
|
+
title: "Temps réel",
|
|
1872
|
+
message: "Connexion interrompue — les messages émis pendant la coupure sont perdus",
|
|
1873
|
+
source: "realtime",
|
|
1874
|
+
ts: Date.now()
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
handleMessage(raw) {
|
|
1878
|
+
if (typeof raw !== "string") return;
|
|
1879
|
+
let msg;
|
|
1880
|
+
try {
|
|
1881
|
+
msg = JSON.parse(raw);
|
|
1882
|
+
} catch {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
this.receive(msg);
|
|
1886
|
+
}
|
|
1887
|
+
setState(s) {
|
|
1888
|
+
if (this._state === s) return;
|
|
1889
|
+
if (s === "connected") this._unsentNotified = false;
|
|
1890
|
+
this._state = s;
|
|
1891
|
+
this.fireLocal(LOCAL_EVENTS.state, s);
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
//#endregion
|
|
1895
|
+
//#region src/syslog/Pdu.ts
|
|
1896
|
+
var SysLogSeverity = /* @__PURE__ */ function(SysLogSeverity) {
|
|
1897
|
+
SysLogSeverity[SysLogSeverity["EMERGENCY"] = 0] = "EMERGENCY";
|
|
1898
|
+
SysLogSeverity[SysLogSeverity["ALERT"] = 1] = "ALERT";
|
|
1899
|
+
SysLogSeverity[SysLogSeverity["CRITIC"] = 2] = "CRITIC";
|
|
1900
|
+
SysLogSeverity[SysLogSeverity["ERROR"] = 3] = "ERROR";
|
|
1901
|
+
SysLogSeverity[SysLogSeverity["WARNING"] = 4] = "WARNING";
|
|
1902
|
+
SysLogSeverity[SysLogSeverity["NOTICE"] = 5] = "NOTICE";
|
|
1903
|
+
SysLogSeverity[SysLogSeverity["INFO"] = 6] = "INFO";
|
|
1904
|
+
SysLogSeverity[SysLogSeverity["DEBUG"] = 7] = "DEBUG";
|
|
1905
|
+
return SysLogSeverity;
|
|
1906
|
+
}(SysLogSeverity || {});
|
|
1907
|
+
const sysLogSeverity = [
|
|
1908
|
+
0,
|
|
1909
|
+
1,
|
|
1910
|
+
2,
|
|
1911
|
+
3,
|
|
1912
|
+
4,
|
|
1913
|
+
5,
|
|
1914
|
+
6,
|
|
1915
|
+
7
|
|
1916
|
+
];
|
|
1917
|
+
const translateSeverity = function(severity = "INFO") {
|
|
1918
|
+
if (typeof severity === "number") {
|
|
1919
|
+
if (sysLogSeverity[severity] !== void 0) return sysLogSeverity[severity];
|
|
1920
|
+
else throw new Error(`Not a valid nodefony syslog severity: ${severity}`);
|
|
1921
|
+
} else if (SysLogSeverity[severity] !== void 0) return SysLogSeverity[severity];
|
|
1922
|
+
else throw new Error(`Not a valid nodefony syslog severity: ${severity}`);
|
|
1923
|
+
};
|
|
1924
|
+
const sysLogSeverityObj = Object.entries(SysLogSeverity).reduce((acc, [key, value]) => {
|
|
1925
|
+
acc[key] = value;
|
|
1926
|
+
return acc;
|
|
1927
|
+
}, {});
|
|
1928
|
+
const severityNameMap = new Map(Object.entries(SysLogSeverity).filter(([, v]) => typeof v === "number").map(([k, v]) => [v, k]));
|
|
1929
|
+
const _gBuffer = globalThis.Buffer;
|
|
1930
|
+
const PID = globalThis.process?.pid ?? 0;
|
|
1931
|
+
const fastTypeOf = (value) => {
|
|
1932
|
+
if (value === null) return null;
|
|
1933
|
+
const t = typeof value;
|
|
1934
|
+
if (t !== "object") return t;
|
|
1935
|
+
if (Array.isArray(value)) return "array";
|
|
1936
|
+
if (value instanceof Date) return "date";
|
|
1937
|
+
if (value instanceof RegExp) return "RegExp";
|
|
1938
|
+
if (value instanceof Error) return "Error";
|
|
1939
|
+
if (_gBuffer?.isBuffer(value)) return "buffer";
|
|
1940
|
+
return "object";
|
|
1941
|
+
};
|
|
1942
|
+
/**
|
|
1943
|
+
* Process Data Unit — entrée de log unitaire conforme RFC 5424.
|
|
1944
|
+
*
|
|
1945
|
+
* Une instance représente un événement de log dans le pipeline Syslog.
|
|
1946
|
+
* Stockée dans le `CircularBuffer` (O(1)) et diffusée aux transports
|
|
1947
|
+
* via l'event `onLog` du {@link Syslog}.
|
|
1948
|
+
*
|
|
1949
|
+
* @example
|
|
1950
|
+
* ```ts
|
|
1951
|
+
* const pdu = new Pdu("user logged in", "INFO", "AUTH", "USER_LOGIN", "user@example.com");
|
|
1952
|
+
* pdu.severity; // 6
|
|
1953
|
+
* pdu.severityName; // "INFO"
|
|
1954
|
+
* pdu.payload; // "user logged in"
|
|
1955
|
+
* ```
|
|
1956
|
+
*
|
|
1957
|
+
* @remarks `pdu.severity` est numérique (RFC 5424) pour comparaisons rapides.
|
|
1958
|
+
* `pdu.severityName` est la string (`"INFO"`, `"CRITIC"` — pas `"CRITICAL"`).
|
|
1959
|
+
*/
|
|
1960
|
+
let guid = 0;
|
|
1961
|
+
var Pdu = class Pdu {
|
|
1962
|
+
payload;
|
|
1963
|
+
uid;
|
|
1964
|
+
severity;
|
|
1965
|
+
timeStamp;
|
|
1966
|
+
severityName;
|
|
1967
|
+
typePayload;
|
|
1968
|
+
moduleName;
|
|
1969
|
+
msgid;
|
|
1970
|
+
msg;
|
|
1971
|
+
status;
|
|
1972
|
+
/** PID du process émetteur (= procid RFC 5424). Constant, capturé au chargement du module. */
|
|
1973
|
+
pid;
|
|
1974
|
+
/**
|
|
1975
|
+
* Identifiant de la requête en cours (corrélation log↔requête).
|
|
1976
|
+
*
|
|
1977
|
+
* Présent UNIQUEMENT si le Pdu est créé dans une bulle `RequestContext` active
|
|
1978
|
+
* (ALS Node) ET que {@link Pdu.requestIdProvider} a été branché côté Node — voir
|
|
1979
|
+
* `src/index.ts` (barrel Node uniquement, isomorphisme préservé : le bundle
|
|
1980
|
+
* client/browser ne branche pas, le provider reste `null` → 0 alloc, 0 lecture).
|
|
1981
|
+
*
|
|
1982
|
+
* Combiné à `pid` (procid RFC 5424), permet la corrélation **complète** :
|
|
1983
|
+
* `pid` = quel worker, `requestId` = quelle requête. Voyage dans ring buffer +
|
|
1984
|
+
* `nodefony:syslog` + transports JSON (champ public sérialisé naturellement).
|
|
1985
|
+
*/
|
|
1986
|
+
requestId;
|
|
1987
|
+
/**
|
|
1988
|
+
* Provider injectable du `requestId` courant (ALS). Branché par le barrel
|
|
1989
|
+
* Node (`src/index.ts`) sur `RequestContext.getRequestId`. Reste `null` côté
|
|
1990
|
+
* browser/debugbar (le bundle client n'importe pas `node:async_hooks`) et
|
|
1991
|
+
* pendant les phases boot pré-Kernel (cohérent : pas de requête en cours).
|
|
1992
|
+
*
|
|
1993
|
+
* Coût : 1 test de référence par Pdu (~5 ns) quand `null` ; ~50-100 ns quand
|
|
1994
|
+
* branché (lecture ALS + accès propriété). Pas d'alloc supplémentaire si
|
|
1995
|
+
* `requestId` est `undefined` (le champ reste non-défini sur l'instance).
|
|
1996
|
+
*/
|
|
1997
|
+
static requestIdProvider = null;
|
|
1998
|
+
/**
|
|
1999
|
+
* Construit un Pdu prêt à être publié dans le pipeline Syslog.
|
|
2000
|
+
*
|
|
2001
|
+
* @param pci - payload (string, Error, objet — typé `unknown`, narrower côté lecteur).
|
|
2002
|
+
* @param severity - niveau RFC 5424 (string `"INFO"` ou numérique `6`). Défaut `"INFO"`.
|
|
2003
|
+
* @param moduleName - nom du module/service qui a produit le log. Défaut `"nodefony"`.
|
|
2004
|
+
* @param msgid - catégorie de message (`"AUTH"`, `"ROUTER"`, etc.). Défaut `""`.
|
|
2005
|
+
* @param msg - détail libre optionnel. Défaut `""`.
|
|
2006
|
+
* @param date - timestamp (number, Date, ou string parsable). Défaut `Date.now()`.
|
|
2007
|
+
* @throws Si `severity` n'est pas une valeur valide de {@link SysLogSeverity}.
|
|
2008
|
+
*/
|
|
2009
|
+
constructor(pci, severity, moduleName = "nodefony", msgid = "", msg = "", date) {
|
|
2010
|
+
if (date === void 0) this.timeStamp = Date.now();
|
|
2011
|
+
else if (typeof date === "number") this.timeStamp = date;
|
|
2012
|
+
else if (date instanceof Date) this.timeStamp = date.getTime();
|
|
2013
|
+
else this.timeStamp = new Date(date).getTime();
|
|
2014
|
+
this.uid = ++guid;
|
|
2015
|
+
this.severity = translateSeverity(severity);
|
|
2016
|
+
const sName = severityNameMap.get(this.severity);
|
|
2017
|
+
if (sName === void 0) throw new Error(`Invalid severity value: ${this.severity}`);
|
|
2018
|
+
this.severityName = sName;
|
|
2019
|
+
this.typePayload = fastTypeOf(pci);
|
|
2020
|
+
this.payload = pci;
|
|
2021
|
+
this.moduleName = moduleName;
|
|
2022
|
+
this.msgid = msgid;
|
|
2023
|
+
this.msg = msg;
|
|
2024
|
+
this.status = "NOTDEFINED";
|
|
2025
|
+
this.pid = PID;
|
|
2026
|
+
this.requestId = Pdu.requestIdProvider !== null ? Pdu.requestIdProvider() : void 0;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Map bidirectionnelle des sévérités RFC 5424 (`{ INFO: 6, 6: "INFO", ... }`).
|
|
2030
|
+
*
|
|
2031
|
+
* @returns dictionnaire `{ severityName: severityValue }` plus enum reverse.
|
|
2032
|
+
*/
|
|
2033
|
+
static sysLogSeverity() {
|
|
2034
|
+
return sysLogSeverityObj;
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Convertit une sévérité numérique ou string en son nom canonique.
|
|
2038
|
+
*
|
|
2039
|
+
* @param severity - valeur numérique (`6`) ou string (`"INFO"` ou `"6"`).
|
|
2040
|
+
* @returns nom canonique (`"INFO"`) ou `undefined` si invalide.
|
|
2041
|
+
*/
|
|
2042
|
+
static severityToString(severity) {
|
|
2043
|
+
const numericSeverity = typeof severity === "string" ? parseInt(severity, 10) : severity;
|
|
2044
|
+
if (!isNaN(numericSeverity) && SysLogSeverity[numericSeverity] !== void 0) return SysLogSeverity[numericSeverity];
|
|
2045
|
+
const severityKey = Pdu.severityToString(SysLogSeverity[severity]);
|
|
2046
|
+
return severityKey !== void 0 ? severityKey : void 0;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Retourne le timestamp formaté `HH:MM:SS GMT±HHMM (TZ Name)`.
|
|
2050
|
+
*
|
|
2051
|
+
* @returns time string lisible par humain.
|
|
2052
|
+
*/
|
|
2053
|
+
getDate() {
|
|
2054
|
+
return new Date(this.timeStamp).toTimeString();
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Sérialise le Pdu en string mono-ligne pour transport console/file.
|
|
2058
|
+
*
|
|
2059
|
+
* @returns ligne formatée `TimeStamp:... Log:... ModuleName:... SeverityName:... MessageID:... UID:... Message:...`.
|
|
2060
|
+
*/
|
|
2061
|
+
toString() {
|
|
2062
|
+
return `TimeStamp:${this.getDate()} Log:${this.payload} ModuleName:${this.moduleName} SeverityName:${this.severityName} MessageID:${this.msgid} UID:${this.uid} Message:${this.msg}`;
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Réhydrate un Pdu existant à partir d'une string JSON — mute uniquement les
|
|
2066
|
+
* clés déjà présentes sur l'instance (filtre `key in this`).
|
|
2067
|
+
*
|
|
2068
|
+
* @param str - JSON sérialisé d'un Pdu.
|
|
2069
|
+
* @returns objet parsé (ou `null` si JSON vide). Mute l'instance courante au passage.
|
|
2070
|
+
*/
|
|
2071
|
+
parseJson(str) {
|
|
2072
|
+
const json = JSON.parse(str);
|
|
2073
|
+
Object.entries(json).forEach(([key, value]) => {
|
|
2074
|
+
if (key in this) this[key] = value;
|
|
2075
|
+
});
|
|
2076
|
+
return json;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
//#endregion
|
|
2080
|
+
//#region src/client/debugbar/format.ts
|
|
2081
|
+
/**
|
|
2082
|
+
* Helpers de formatage PURS de la debug bar — aucune dépendance DOM/réseau,
|
|
2083
|
+
* donc unit-testables côté Node (suite mocha du core).
|
|
2084
|
+
*/
|
|
2085
|
+
const UNITS = [
|
|
2086
|
+
"B",
|
|
2087
|
+
"KB",
|
|
2088
|
+
"MB",
|
|
2089
|
+
"GB",
|
|
2090
|
+
"TB"
|
|
2091
|
+
];
|
|
2092
|
+
/**
|
|
2093
|
+
* Formatte un nombre d'octets en unité lisible (1 décimale, base 1024).
|
|
2094
|
+
*
|
|
2095
|
+
* @param bytes - taille en octets (négatif/NaN → "0 B")
|
|
2096
|
+
* @returns ex. `"1.5 MB"`, `"0 B"`
|
|
2097
|
+
*/
|
|
2098
|
+
function formatBytes(bytes) {
|
|
2099
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
2100
|
+
let i = 0;
|
|
2101
|
+
let n = bytes;
|
|
2102
|
+
while (n >= 1024 && i < UNITS.length - 1) {
|
|
2103
|
+
n /= 1024;
|
|
2104
|
+
i++;
|
|
2105
|
+
}
|
|
2106
|
+
return `${i === 0 ? n : n.toFixed(1)} ${UNITS[i]}`;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Formatte une durée (secondes) en chaîne compacte (`1h 02m`, `3m 12s`, `45s`).
|
|
2110
|
+
*
|
|
2111
|
+
* @param seconds - uptime en secondes
|
|
2112
|
+
*/
|
|
2113
|
+
function formatUptime(seconds) {
|
|
2114
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "0s";
|
|
2115
|
+
const s = Math.floor(seconds);
|
|
2116
|
+
const h = Math.floor(s / 3600);
|
|
2117
|
+
const m = Math.floor(s % 3600 / 60);
|
|
2118
|
+
const sec = s % 60;
|
|
2119
|
+
if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
|
|
2120
|
+
if (m > 0) return `${m}m ${String(sec).padStart(2, "0")}s`;
|
|
2121
|
+
return `${sec}s`;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Classe une valeur 0-100 en palier vert/orange/rouge.
|
|
2125
|
+
*
|
|
2126
|
+
* @param percent - valeur de la jauge
|
|
2127
|
+
* @param warn - seuil orange (défaut 70)
|
|
2128
|
+
* @param crit - seuil rouge (défaut 90)
|
|
2129
|
+
*/
|
|
2130
|
+
function gauge(percent, warn = 70, crit = 90) {
|
|
2131
|
+
if (!Number.isFinite(percent)) return "ok";
|
|
2132
|
+
if (percent >= crit) return "crit";
|
|
2133
|
+
if (percent >= warn) return "warn";
|
|
2134
|
+
return "ok";
|
|
2135
|
+
}
|
|
2136
|
+
const ANSI_RE = /\[[0-9;]*m/g;
|
|
2137
|
+
/** Retire les séquences d'échappement ANSI (couleurs terminal) d'une chaîne. */
|
|
2138
|
+
function stripAnsi(input) {
|
|
2139
|
+
return input.replace(ANSI_RE, "");
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Calcule l'attribut `points` d'un `<polyline>` SVG (sparkline) à partir d'une
|
|
2143
|
+
* série de valeurs. Pur → testable. Mise à l'échelle sur `[0, max]`.
|
|
2144
|
+
*
|
|
2145
|
+
* @param values - série temporelle (gauche = ancien, droite = récent)
|
|
2146
|
+
* @param width - largeur du viewBox
|
|
2147
|
+
* @param height - hauteur du viewBox
|
|
2148
|
+
* @param max - plafond de l'axe Y (défaut = max de la série, plancher 1)
|
|
2149
|
+
* @returns ex. `"0,14 10,7 20,2"` (vide si < 2 points)
|
|
2150
|
+
*/
|
|
2151
|
+
function sparklinePoints(values, width, height, max) {
|
|
2152
|
+
const n = values.length;
|
|
2153
|
+
if (n < 2) return "";
|
|
2154
|
+
const top = max ?? Math.max(...values, 1);
|
|
2155
|
+
const ceil = top > 0 ? top : 1;
|
|
2156
|
+
const step = width / (n - 1);
|
|
2157
|
+
let out = "";
|
|
2158
|
+
for (let i = 0; i < n; i++) {
|
|
2159
|
+
const v = Number.isFinite(values[i]) ? values[i] : 0;
|
|
2160
|
+
const ratio = Math.min(1, Math.max(0, v / ceil));
|
|
2161
|
+
const x = Math.round(i * step * 100) / 100;
|
|
2162
|
+
const y = Math.round((height - ratio * height) * 100) / 100;
|
|
2163
|
+
out += i === 0 ? `${x},${y}` : ` ${x},${y}`;
|
|
2164
|
+
}
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Heure locale d'un horodatage, à la milliseconde — `14:03:27.482`.
|
|
2169
|
+
*
|
|
2170
|
+
* La date n'y figure pas : une barre de débogage montre ce qui vient de se
|
|
2171
|
+
* passer, et une date répétée sur chaque ligne coûte de la place sans rien
|
|
2172
|
+
* apprendre. Le détail d'une entrée, lui, rend l'horodatage complet.
|
|
2173
|
+
*/
|
|
2174
|
+
function fmtClock(ts) {
|
|
2175
|
+
const d = new Date(ts);
|
|
2176
|
+
const p2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
2177
|
+
const ms = d.getMilliseconds();
|
|
2178
|
+
const p3 = ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms);
|
|
2179
|
+
return `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}.${p3}`;
|
|
2180
|
+
}
|
|
2181
|
+
//#endregion
|
|
2182
|
+
//#region src/client/debugbar/model.ts
|
|
2183
|
+
/** Sévérité RFC 5424 : ≤ 3 = erreur, 4 = warning. */
|
|
2184
|
+
const SEVERITY_ERROR_MAX = 3;
|
|
2185
|
+
const SEVERITY_WARNING = 4;
|
|
2186
|
+
/** Points conservés dans les séries de sparkline (~1 min à 1 tick/s). */
|
|
2187
|
+
const SERIES_POINTS = 60;
|
|
2188
|
+
function pushCapped(arr, v, cap) {
|
|
2189
|
+
arr.push(v);
|
|
2190
|
+
if (arr.length > cap) arr.shift();
|
|
2191
|
+
}
|
|
2192
|
+
function logText(pci) {
|
|
2193
|
+
if (pci == null) return "";
|
|
2194
|
+
if (typeof pci === "string") return stripAnsi(pci);
|
|
2195
|
+
if (typeof pci === "number" || typeof pci === "boolean") return String(pci);
|
|
2196
|
+
try {
|
|
2197
|
+
return stripAnsi(JSON.stringify(pci));
|
|
2198
|
+
} catch {
|
|
2199
|
+
return String(pci);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
var DebugBarModel = class {
|
|
2203
|
+
_state = "disconnected";
|
|
2204
|
+
_stats = null;
|
|
2205
|
+
_cpuPeak = 0;
|
|
2206
|
+
_heapPeak = 0;
|
|
2207
|
+
_loopPeak = 0;
|
|
2208
|
+
_cpuSeries = [];
|
|
2209
|
+
_heapSeries = [];
|
|
2210
|
+
_loopSeries = [];
|
|
2211
|
+
_logTotal = 0;
|
|
2212
|
+
_errorCount = 0;
|
|
2213
|
+
_warnCount = 0;
|
|
2214
|
+
_dropped = 0;
|
|
2215
|
+
_feed = [];
|
|
2216
|
+
setState(state) {
|
|
2217
|
+
this._state = state;
|
|
2218
|
+
}
|
|
2219
|
+
/** Mémorise le dernier tick stats + alimente les séries/peaks. */
|
|
2220
|
+
ingestStats(payload) {
|
|
2221
|
+
this._stats = payload;
|
|
2222
|
+
const cpu = payload.cpuPercent ?? 0;
|
|
2223
|
+
const loop = payload.eventLoopMs ?? 0;
|
|
2224
|
+
const heapUsed = payload.memory?.heapUsed ?? 0;
|
|
2225
|
+
const heapLimit = payload.memory?.heapLimit ?? 0;
|
|
2226
|
+
const heapPct = heapLimit > 0 ? Math.round(heapUsed / heapLimit * 100) : 0;
|
|
2227
|
+
pushCapped(this._cpuSeries, cpu, SERIES_POINTS);
|
|
2228
|
+
pushCapped(this._heapSeries, heapPct, SERIES_POINTS);
|
|
2229
|
+
pushCapped(this._loopSeries, loop, SERIES_POINTS);
|
|
2230
|
+
if (cpu > this._cpuPeak) this._cpuPeak = cpu;
|
|
2231
|
+
if (heapPct > this._heapPeak) this._heapPeak = heapPct;
|
|
2232
|
+
if (loop > this._loopPeak) this._loopPeak = loop;
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Comptabilise un batch de logs coalescés (compteurs cumulés + feed). Chaque
|
|
2236
|
+
* entrée est **réhydratée en {@link Pdu}** — la MÊME classe Core que côté
|
|
2237
|
+
* serveur (severityName/payload canoniques, pas de devinette de champ).
|
|
2238
|
+
*/
|
|
2239
|
+
ingestSyslog(payload) {
|
|
2240
|
+
const logs = payload.logs;
|
|
2241
|
+
if (logs) {
|
|
2242
|
+
for (let i = 0; i < logs.length; i++) {
|
|
2243
|
+
const entry = logs[i];
|
|
2244
|
+
if (typeof entry?.severity !== "number") continue;
|
|
2245
|
+
let pdu;
|
|
2246
|
+
try {
|
|
2247
|
+
pdu = new Pdu(entry.payload, entry.severity, entry.moduleName ?? "nodefony", entry.msgid ?? "", entry.msg ?? "");
|
|
2248
|
+
} catch {
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
const sev = pdu.severity;
|
|
2252
|
+
this._logTotal++;
|
|
2253
|
+
if (sev <= SEVERITY_ERROR_MAX) this._errorCount++;
|
|
2254
|
+
else if (sev === SEVERITY_WARNING) this._warnCount++;
|
|
2255
|
+
this._feed.push({
|
|
2256
|
+
severity: sev,
|
|
2257
|
+
name: pdu.severityName,
|
|
2258
|
+
text: logText(pdu.payload),
|
|
2259
|
+
module: pdu.moduleName,
|
|
2260
|
+
ts: typeof entry.timeStamp === "number" ? entry.timeStamp : Date.now(),
|
|
2261
|
+
uid: typeof entry.uid === "number" ? entry.uid : 0,
|
|
2262
|
+
msgid: typeof entry.msgid === "string" ? entry.msgid : "",
|
|
2263
|
+
pid: typeof entry.pid === "number" ? entry.pid : 0,
|
|
2264
|
+
requestId: typeof entry.requestId === "string" ? entry.requestId : void 0
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
if (this._feed.length > 40) this._feed.splice(0, this._feed.length - 40);
|
|
2268
|
+
}
|
|
2269
|
+
if (typeof payload.dropped === "number") this._dropped += payload.dropped;
|
|
2270
|
+
}
|
|
2271
|
+
get view() {
|
|
2272
|
+
const s = this._stats;
|
|
2273
|
+
const mem = s?.memory;
|
|
2274
|
+
const app = s?.app;
|
|
2275
|
+
const heapUsed = mem?.heapUsed ?? 0;
|
|
2276
|
+
const heapLimit = mem?.heapLimit ?? 0;
|
|
2277
|
+
return {
|
|
2278
|
+
state: this._state,
|
|
2279
|
+
appName: app?.name ?? "",
|
|
2280
|
+
appVersion: app?.version ?? "",
|
|
2281
|
+
env: app?.env ?? "",
|
|
2282
|
+
debug: app?.debug ?? false,
|
|
2283
|
+
branch: app?.branch ?? "",
|
|
2284
|
+
cpuPercent: s?.cpuPercent ?? 0,
|
|
2285
|
+
cpuPeak: this._cpuPeak,
|
|
2286
|
+
cpuCount: s?.cpuCount ?? 0,
|
|
2287
|
+
eventLoopMs: s?.eventLoopMs ?? 0,
|
|
2288
|
+
eventLoopPeak: this._loopPeak,
|
|
2289
|
+
uptime: s?.uptime ?? 0,
|
|
2290
|
+
pid: s?.pid ?? 0,
|
|
2291
|
+
instanceId: s?.instanceId ?? "—",
|
|
2292
|
+
loadavg: s?.loadavg ?? [],
|
|
2293
|
+
rss: mem?.rss ?? 0,
|
|
2294
|
+
heapUsed,
|
|
2295
|
+
heapTotal: mem?.heapTotal ?? 0,
|
|
2296
|
+
heapLimit,
|
|
2297
|
+
external: mem?.external ?? 0,
|
|
2298
|
+
heapPercent: heapLimit > 0 ? Math.round(heapUsed / heapLimit * 100) : 0,
|
|
2299
|
+
heapPeak: this._heapPeak,
|
|
2300
|
+
cpuSeries: this._cpuSeries,
|
|
2301
|
+
heapSeries: this._heapSeries,
|
|
2302
|
+
loopSeries: this._loopSeries,
|
|
2303
|
+
logTotal: this._logTotal,
|
|
2304
|
+
errorCount: this._errorCount,
|
|
2305
|
+
warnCount: this._warnCount,
|
|
2306
|
+
dropped: this._dropped,
|
|
2307
|
+
feed: this._feed
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
//#endregion
|
|
2312
|
+
//#region src/client/debugbar/hmr.ts
|
|
2313
|
+
/**
|
|
2314
|
+
* Observation des hot-updates Vite **sans ouvrir de WebSocket**.
|
|
2315
|
+
*
|
|
2316
|
+
* Historique : cette sonde ouvrait jadis un **2ᵉ client** WebSocket `vite-hmr`
|
|
2317
|
+
* sur le serveur HMR de Vite. Ce client passif (il ne renvoyait jamais le
|
|
2318
|
+
* heartbeat attendu) se faisait fermer par Vite, puis se reconnectait en
|
|
2319
|
+
* boucle → une cascade de connexions `wss://…:5173` qui s'ouvrent et se ferment
|
|
2320
|
+
* sans fin dans la console. C'était aussi un WebSocket de trop (philosophie
|
|
2321
|
+
* Nodefony : une seule connexion temps réel côté navigateur).
|
|
2322
|
+
*
|
|
2323
|
+
* Désormais : aucun socket. On écoute un `CustomEvent` `nodefony:hmr` émis sur
|
|
2324
|
+
* `window` par un pont injecté côté page (TemplateHelper, `@nodefony/frontend`),
|
|
2325
|
+
* lequel se branche sur le client HMR **déjà chargé** par Vite via
|
|
2326
|
+
* `createHotContext` — zéro connexion ajoutée.
|
|
2327
|
+
*/
|
|
2328
|
+
/** Nom de l'événement DOM émis par le pont HMR côté page. */
|
|
2329
|
+
const HMR_EVENT = PLATFORM_EVENTS.hmr;
|
|
2330
|
+
/**
|
|
2331
|
+
* Observe les hot-updates Vite via l'événement `window` `nodefony:hmr`.
|
|
2332
|
+
* Aucune connexion réseau ouverte. No-op hors navigateur.
|
|
2333
|
+
*
|
|
2334
|
+
* @param onEvent - appelé à chaque hot-update relayé par le pont.
|
|
2335
|
+
* @returns dispose() — retire le listener (OBLIGATOIRE au démontage).
|
|
2336
|
+
*/
|
|
2337
|
+
function observeViteHmr(onEvent) {
|
|
2338
|
+
if (typeof window === "undefined") return () => {};
|
|
2339
|
+
const handler = (ev) => {
|
|
2340
|
+
const detail = ev.detail;
|
|
2341
|
+
if (detail && typeof detail.kind === "string") onEvent(detail);
|
|
2342
|
+
};
|
|
2343
|
+
window.addEventListener(HMR_EVENT, handler);
|
|
2344
|
+
return () => window.removeEventListener(HMR_EVENT, handler);
|
|
2345
|
+
}
|
|
2346
|
+
//#endregion
|
|
2347
|
+
//#region src/client/debugbar/network.ts
|
|
2348
|
+
/**
|
|
2349
|
+
* Intercepteur réseau **dev-only** de la debug bar — capture les appels AJAX
|
|
2350
|
+
* (`fetch` + `XMLHttpRequest`) de la page pour le panneau Network, et lit le
|
|
2351
|
+
* header `X-Request-Id` de chaque réponse → corrélation avec le profil serveur
|
|
2352
|
+
* (`/nodefony/profiler/api/{requestId}`).
|
|
2353
|
+
*
|
|
2354
|
+
* ⚠️ Monkey-patch des globals = intrusif. Donc 6 garde-fous (cf décision design) :
|
|
2355
|
+
* 1. **dev-only** : n'est installé que par la debug bar, jamais en prod.
|
|
2356
|
+
* 2. **défensif** : toute la collecte est en try/catch — un bug ici ne casse
|
|
2357
|
+
* JAMAIS la requête de l'app (on relaie toujours l'appel original).
|
|
2358
|
+
* 3. **header-only** : on ne touche jamais au body (pas de `.clone()`, pas de
|
|
2359
|
+
* lecture du stream) → 0 risque de consommer/altérer la réponse.
|
|
2360
|
+
* 4. **réversible** : `uninstall()` restaure les implémentations d'origine.
|
|
2361
|
+
* 5. **chain-safe** : on wrappe le `fetch`/XHR COURANT (déjà potentiellement
|
|
2362
|
+
* patché par une autre lib), on ne clobber pas — l'uninstall ne restaure que
|
|
2363
|
+
* si personne n'a re-patché par-dessus.
|
|
2364
|
+
* 6. **opt-out** : `mountDebugBar({ network:false })` n'installe rien.
|
|
2365
|
+
*
|
|
2366
|
+
* Coût : 2 `performance.now()` + lecture d'1 header + 1 push capé par appel.
|
|
2367
|
+
* Négligeable vs la latence réseau.
|
|
2368
|
+
*/
|
|
2369
|
+
/**
|
|
2370
|
+
* En-têtes de corrélation posés par le framework sur chaque réponse :
|
|
2371
|
+
* - `x-request-id` = **clé de lookup** du profiler serveur (concept requestId) ;
|
|
2372
|
+
* - `traceparent` = W3C Trace Context (RFC-propre), affiché pour la corrélation
|
|
2373
|
+
* distribuée — pas la clé du profiler (qui est keyé par requestId).
|
|
2374
|
+
*/
|
|
2375
|
+
const REQUEST_ID_HEADER = "x-request-id";
|
|
2376
|
+
const TRACEPARENT_HEADER = "traceparent";
|
|
2377
|
+
/** Extrait le path d'une URL (absolue ou relative), tolérant. */
|
|
2378
|
+
function toPath(url) {
|
|
2379
|
+
try {
|
|
2380
|
+
return new URL(url, location.href).pathname;
|
|
2381
|
+
} catch {
|
|
2382
|
+
return url;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
/** Normalise l'`input` polymorphe de `fetch` en URL string. */
|
|
2386
|
+
function fetchUrl(input) {
|
|
2387
|
+
if (typeof input === "string") return input;
|
|
2388
|
+
if (input && typeof input === "object") {
|
|
2389
|
+
const u = input.url;
|
|
2390
|
+
if (typeof u === "string") return u;
|
|
2391
|
+
}
|
|
2392
|
+
return String(input);
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Installe l'intercepteur fetch + XHR. No-op hors navigateur.
|
|
2396
|
+
*
|
|
2397
|
+
* @returns `uninstall()` — restaure les globals (OBLIGATOIRE à l'unmount).
|
|
2398
|
+
*/
|
|
2399
|
+
function installNetworkInterceptor(opts) {
|
|
2400
|
+
if (typeof window === "undefined") return () => {};
|
|
2401
|
+
const ignore = opts.ignore ?? (() => false);
|
|
2402
|
+
let seq = 0;
|
|
2403
|
+
const origFetch = window.fetch;
|
|
2404
|
+
const patchedFetch = typeof origFetch === "function" ? function(...args) {
|
|
2405
|
+
let url = "";
|
|
2406
|
+
let method = "GET";
|
|
2407
|
+
try {
|
|
2408
|
+
url = fetchUrl(args[0]);
|
|
2409
|
+
method = (args[1]?.method ?? args[0]?.method ?? "GET").toString().toUpperCase();
|
|
2410
|
+
} catch {}
|
|
2411
|
+
if (!url || ignore(url)) return origFetch.apply(this, args);
|
|
2412
|
+
const entry = {
|
|
2413
|
+
id: ++seq,
|
|
2414
|
+
method,
|
|
2415
|
+
url,
|
|
2416
|
+
path: toPath(url),
|
|
2417
|
+
status: null,
|
|
2418
|
+
ok: false,
|
|
2419
|
+
durationMs: null,
|
|
2420
|
+
startedAt: performance.now(),
|
|
2421
|
+
requestId: null,
|
|
2422
|
+
traceparent: null,
|
|
2423
|
+
pending: true,
|
|
2424
|
+
error: null
|
|
2425
|
+
};
|
|
2426
|
+
safeEmit(opts, entry);
|
|
2427
|
+
return origFetch.apply(this, args).then((res) => {
|
|
2428
|
+
try {
|
|
2429
|
+
entry.status = res.status;
|
|
2430
|
+
entry.ok = res.ok;
|
|
2431
|
+
entry.requestId = res.headers.get(REQUEST_ID_HEADER);
|
|
2432
|
+
entry.traceparent = res.headers.get(TRACEPARENT_HEADER);
|
|
2433
|
+
entry.durationMs = round(performance.now() - entry.startedAt);
|
|
2434
|
+
entry.pending = false;
|
|
2435
|
+
safeEmit(opts, entry);
|
|
2436
|
+
} catch {}
|
|
2437
|
+
return res;
|
|
2438
|
+
}, (err) => {
|
|
2439
|
+
try {
|
|
2440
|
+
entry.error = err instanceof Error ? err.message : "network error";
|
|
2441
|
+
entry.durationMs = round(performance.now() - entry.startedAt);
|
|
2442
|
+
entry.pending = false;
|
|
2443
|
+
safeEmit(opts, entry);
|
|
2444
|
+
} catch {}
|
|
2445
|
+
throw err;
|
|
2446
|
+
});
|
|
2447
|
+
} : null;
|
|
2448
|
+
if (patchedFetch) window.fetch = patchedFetch;
|
|
2449
|
+
const XHR = window.XMLHttpRequest;
|
|
2450
|
+
const origOpen = XHR?.prototype.open;
|
|
2451
|
+
const origSend = XHR?.prototype.send;
|
|
2452
|
+
const state = /* @__PURE__ */ new WeakMap();
|
|
2453
|
+
if (XHR && origOpen && origSend) {
|
|
2454
|
+
XHR.prototype.open = function(method, url, ...rest) {
|
|
2455
|
+
try {
|
|
2456
|
+
const u = url.toString();
|
|
2457
|
+
if (u && !ignore(u)) state.set(this, {
|
|
2458
|
+
id: ++seq,
|
|
2459
|
+
method: (method ?? "GET").toUpperCase(),
|
|
2460
|
+
url: u,
|
|
2461
|
+
path: toPath(u),
|
|
2462
|
+
status: null,
|
|
2463
|
+
ok: false,
|
|
2464
|
+
durationMs: null,
|
|
2465
|
+
startedAt: 0,
|
|
2466
|
+
requestId: null,
|
|
2467
|
+
traceparent: null,
|
|
2468
|
+
pending: true,
|
|
2469
|
+
error: null
|
|
2470
|
+
});
|
|
2471
|
+
} catch {}
|
|
2472
|
+
return origOpen.apply(this, [
|
|
2473
|
+
method,
|
|
2474
|
+
url,
|
|
2475
|
+
...rest
|
|
2476
|
+
]);
|
|
2477
|
+
};
|
|
2478
|
+
XHR.prototype.send = function(...args) {
|
|
2479
|
+
const entry = state.get(this);
|
|
2480
|
+
if (entry) {
|
|
2481
|
+
entry.startedAt = performance.now();
|
|
2482
|
+
safeEmit(opts, entry);
|
|
2483
|
+
const finalize = (error) => {
|
|
2484
|
+
try {
|
|
2485
|
+
entry.status = this.status || null;
|
|
2486
|
+
entry.ok = this.status >= 200 && this.status < 400;
|
|
2487
|
+
entry.requestId = this.getResponseHeader(REQUEST_ID_HEADER) || null;
|
|
2488
|
+
entry.traceparent = this.getResponseHeader(TRACEPARENT_HEADER) || null;
|
|
2489
|
+
entry.durationMs = round(performance.now() - entry.startedAt);
|
|
2490
|
+
entry.error = error;
|
|
2491
|
+
entry.pending = false;
|
|
2492
|
+
safeEmit(opts, entry);
|
|
2493
|
+
} catch {}
|
|
2494
|
+
};
|
|
2495
|
+
this.addEventListener("load", () => finalize(null));
|
|
2496
|
+
this.addEventListener("error", () => finalize("network error"));
|
|
2497
|
+
this.addEventListener("abort", () => finalize("aborted"));
|
|
2498
|
+
this.addEventListener("timeout", () => finalize("timeout"));
|
|
2499
|
+
}
|
|
2500
|
+
return origSend.apply(this, args);
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
return () => {
|
|
2504
|
+
if (patchedFetch && window.fetch === patchedFetch) window.fetch = origFetch;
|
|
2505
|
+
if (XHR && origOpen && XHR.prototype.open !== origOpen) XHR.prototype.open = origOpen;
|
|
2506
|
+
if (XHR && origSend && XHR.prototype.send !== origSend) XHR.prototype.send = origSend;
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
function round(ms) {
|
|
2510
|
+
return Math.round(ms * 10) / 10;
|
|
2511
|
+
}
|
|
2512
|
+
/** Émission protégée — un onChange qui throw ne doit pas casser la requête. */
|
|
2513
|
+
function safeEmit(opts, entry) {
|
|
2514
|
+
try {
|
|
2515
|
+
opts.onChange(entry);
|
|
2516
|
+
} catch {}
|
|
2517
|
+
}
|
|
2518
|
+
//#endregion
|
|
2519
|
+
//#region src/client/debugbar/profile.ts
|
|
2520
|
+
/** Couleur stable par nom de phase canonique. */
|
|
2521
|
+
const PHASE_TIER = {
|
|
2522
|
+
parse: "parse",
|
|
2523
|
+
resolve: "resolve",
|
|
2524
|
+
firewall: "firewall",
|
|
2525
|
+
identity: "identity",
|
|
2526
|
+
initialize: "init",
|
|
2527
|
+
action: "action",
|
|
2528
|
+
render: "render",
|
|
2529
|
+
send: "send"
|
|
2530
|
+
};
|
|
2531
|
+
/** Mappe un nom de phase vers sa classe de couleur (défaut neutre). */
|
|
2532
|
+
function phaseTier(name) {
|
|
2533
|
+
return PHASE_TIER[name] ?? "other";
|
|
2534
|
+
}
|
|
2535
|
+
/** Logs/appels conservés dans le panneau Network. */
|
|
2536
|
+
const NET_MAX = 80;
|
|
2537
|
+
var NetworkModel = class {
|
|
2538
|
+
_byId = /* @__PURE__ */ new Map();
|
|
2539
|
+
_profiles = /* @__PURE__ */ new Map();
|
|
2540
|
+
_total = 0;
|
|
2541
|
+
_errors = 0;
|
|
2542
|
+
/** Upsert d'une entrée (pending puis résolue partagent le même `id`). */
|
|
2543
|
+
ingest(entry) {
|
|
2544
|
+
if (!this._byId.has(entry.id)) this._total++;
|
|
2545
|
+
const prev = this._byId.get(entry.id);
|
|
2546
|
+
const wasErr = prev ? isError(prev) : false;
|
|
2547
|
+
const isErr = isError(entry);
|
|
2548
|
+
if (isErr && !wasErr) this._errors++;
|
|
2549
|
+
else if (!isErr && wasErr) this._errors--;
|
|
2550
|
+
this._byId.set(entry.id, entry);
|
|
2551
|
+
if (this._byId.size > NET_MAX) {
|
|
2552
|
+
const oldest = this._byId.keys().next().value;
|
|
2553
|
+
if (oldest !== void 0) {
|
|
2554
|
+
const ev = this._byId.get(oldest);
|
|
2555
|
+
if (ev && isError(ev)) this._errors--;
|
|
2556
|
+
this._byId.delete(oldest);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
/** Entrées récentes (récent → ancien) pour le rendu. */
|
|
2561
|
+
entries() {
|
|
2562
|
+
return [...this._byId.values()].reverse();
|
|
2563
|
+
}
|
|
2564
|
+
get total() {
|
|
2565
|
+
return this._total;
|
|
2566
|
+
}
|
|
2567
|
+
get errors() {
|
|
2568
|
+
return this._errors;
|
|
2569
|
+
}
|
|
2570
|
+
get pending() {
|
|
2571
|
+
let n = 0;
|
|
2572
|
+
for (const e of this._byId.values()) if (e.pending) n++;
|
|
2573
|
+
return n;
|
|
2574
|
+
}
|
|
2575
|
+
profileState(requestId) {
|
|
2576
|
+
return this._profiles.get(requestId);
|
|
2577
|
+
}
|
|
2578
|
+
setProfileState(requestId, state) {
|
|
2579
|
+
this._profiles.set(requestId, state);
|
|
2580
|
+
}
|
|
2581
|
+
clear() {
|
|
2582
|
+
this._byId.clear();
|
|
2583
|
+
this._profiles.clear();
|
|
2584
|
+
this._total = 0;
|
|
2585
|
+
this._errors = 0;
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
/** Vrai si une entrée est en erreur réseau ou status ≥ 400. */
|
|
2589
|
+
function isError(e) {
|
|
2590
|
+
if (e.error) return true;
|
|
2591
|
+
return e.status !== null && e.status >= 400;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Calcule le layout proportionnel des phases serveur (waterfall). Pur.
|
|
2595
|
+
*
|
|
2596
|
+
* Chaque barre est positionnée sur le span `[min start, max end]` en %. Une
|
|
2597
|
+
* phase de durée nulle reçoit une largeur plancher (visibilité). Span nul →
|
|
2598
|
+
* barres vides (évite la division par zéro).
|
|
2599
|
+
*
|
|
2600
|
+
* @param phases - phases du profil serveur (startMs relatif + durationMs)
|
|
2601
|
+
* @returns barres prêtes à rendre (gauche/largeur en %)
|
|
2602
|
+
*/
|
|
2603
|
+
function computeWaterfall(phases) {
|
|
2604
|
+
if (phases.length === 0) return [];
|
|
2605
|
+
let min = Infinity;
|
|
2606
|
+
let max = -Infinity;
|
|
2607
|
+
for (const p of phases) {
|
|
2608
|
+
if (p.startMs < min) min = p.startMs;
|
|
2609
|
+
const end = p.startMs + (p.durationMs ?? 0);
|
|
2610
|
+
if (end > max) max = end;
|
|
2611
|
+
}
|
|
2612
|
+
const span = max - min;
|
|
2613
|
+
return phases.map((p) => {
|
|
2614
|
+
const dur = p.durationMs ?? 0;
|
|
2615
|
+
const leftPct = span > 0 ? (p.startMs - min) / span * 100 : 0;
|
|
2616
|
+
const widthRaw = span > 0 ? dur / span * 100 : 0;
|
|
2617
|
+
return {
|
|
2618
|
+
name: p.name,
|
|
2619
|
+
leftPct: clampPct(leftPct),
|
|
2620
|
+
widthPct: clampPct(Math.max(widthRaw, dur > 0 || span === 0 ? 1.5 : .6)),
|
|
2621
|
+
durationMs: Math.round(dur * 100) / 100,
|
|
2622
|
+
tier: phaseTier(p.name)
|
|
2623
|
+
};
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
function clampPct(v) {
|
|
2627
|
+
if (!Number.isFinite(v) || v < 0) return 0;
|
|
2628
|
+
if (v > 100) return 100;
|
|
2629
|
+
return Math.round(v * 100) / 100;
|
|
2630
|
+
}
|
|
2631
|
+
//#endregion
|
|
2632
|
+
//#region src/client/debugbar/DebugBar.ts
|
|
2633
|
+
/**
|
|
2634
|
+
* DebugBar — toolbar de debug par-page type Symfony WDT, **dev-only**, pensée
|
|
2635
|
+
* comme une **vitrine du realtime Nodefony** (on doit *voir* le framework
|
|
2636
|
+
* respirer en direct).
|
|
2637
|
+
*
|
|
2638
|
+
* 2ᵉ consommateur navigateur du Core isomorphe après Studio : MÊME backbone
|
|
2639
|
+
* realtime (WS JSON-RPC 2.0, canaux `nodefony:supervision` / `nodefony:syslog`) via
|
|
2640
|
+
* {@link RealtimeClient}. Aucun rendu serveur splicé dans le body : le serveur
|
|
2641
|
+
* *collecte*, le client *rend*.
|
|
2642
|
+
*
|
|
2643
|
+
* UI : **onglets** (Realtime / Network / Perf / Logs / Runtime) — un seul pane
|
|
2644
|
+
* rendu/visible à la fois (≠ grid fourre-tout) + **poignée de resize** (hauteur
|
|
2645
|
+
* persistée). Vanilla TS + **Shadow DOM** + sparklines **SVG maison** — 0 dep UI.
|
|
2646
|
+
*
|
|
2647
|
+
* Perf scroll (critique) : fond OPAQUE sur le panneau (le `backdrop-filter:blur`
|
|
2648
|
+
* sur le conteneur scrollable recompositait à chaque frame), `contain:content`
|
|
2649
|
+
* par pane, et la liste Network en **mise à jour incrémentale** (nœuds stables,
|
|
2650
|
+
* jamais de rebuild `innerHTML` global → le clic n'est plus perdu, le scroll ne
|
|
2651
|
+
* saute plus).
|
|
2652
|
+
*
|
|
2653
|
+
* Panneau **Network** (dev-only) : intercepte `fetch`/`XHR` (header-only,
|
|
2654
|
+
* défensif, réversible) → liste des appels AJAX ; clic sur un appel → fetch du
|
|
2655
|
+
* profil serveur (`/nodefony/profiler/api/{requestId}`, corrélé via le header
|
|
2656
|
+
* `X-Request-Id`) → **waterfall des phases** du pipeline. SPA-first : on profile
|
|
2657
|
+
* les appels, pas la page. Le `traceparent` W3C (RFC-propre) est aussi remonté.
|
|
2658
|
+
*/
|
|
2659
|
+
/** Canaux realtime consommés (figés, alignés sur les providers Studio). */
|
|
2660
|
+
const CHANNELS = {
|
|
2661
|
+
stats: PLATFORM_CHANNELS.debugbar,
|
|
2662
|
+
syslog: PLATFORM_CHANNELS.syslog
|
|
2663
|
+
};
|
|
2664
|
+
/** Endpoint WS realtime par défaut (porté par Studio aujourd'hui, RealtimeService demain). */
|
|
2665
|
+
const DEFAULT_PATH = "/nodefony/studio/api/realtime";
|
|
2666
|
+
/** Base du data-plane profiler (data-plane admin `IAdminApi` namespace `profiler`). */
|
|
2667
|
+
const DEFAULT_PROFILER_BASE = "/nodefony/profiler/api";
|
|
2668
|
+
const HOST_ID = "nodefony-debugbar";
|
|
2669
|
+
/** Dimensions des sparklines (unités viewBox SVG). */
|
|
2670
|
+
const MINI_W = 58;
|
|
2671
|
+
const MINI_H = 20;
|
|
2672
|
+
const CHART_W = 260;
|
|
2673
|
+
const CHART_H = 46;
|
|
2674
|
+
const RT_POINTS = 60;
|
|
2675
|
+
/** Borne basse de hauteur du panneau (px). */
|
|
2676
|
+
const PANEL_H_MIN = 140;
|
|
2677
|
+
/**
|
|
2678
|
+
* Hauteur par défaut = **fraction de l'écran** (≈48 % du viewport) — gros écran
|
|
2679
|
+
* → grand panneau, petit écran → panneau modeste. Bornée [300, 640] pour rester
|
|
2680
|
+
* raisonnable aux extrêmes.
|
|
2681
|
+
*/
|
|
2682
|
+
function defaultPanelH() {
|
|
2683
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : 800;
|
|
2684
|
+
return Math.min(640, Math.max(300, Math.round(vh * .48)));
|
|
2685
|
+
}
|
|
2686
|
+
/** Hauteur mini à l'ouverture d'un profil (waterfall confortable) ≈55 % écran. */
|
|
2687
|
+
function detailPanelH() {
|
|
2688
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : 800;
|
|
2689
|
+
return Math.min(680, Math.max(360, Math.round(vh * .55)));
|
|
2690
|
+
}
|
|
2691
|
+
/** Métadonnées d'affichage par framework (couleur de marque officielle). */
|
|
2692
|
+
const FRAMEWORKS = {
|
|
2693
|
+
react19: {
|
|
2694
|
+
label: "React 19",
|
|
2695
|
+
color: "#61dafb"
|
|
2696
|
+
},
|
|
2697
|
+
react: {
|
|
2698
|
+
label: "React",
|
|
2699
|
+
color: "#61dafb"
|
|
2700
|
+
},
|
|
2701
|
+
vue3: {
|
|
2702
|
+
label: "Vue 3",
|
|
2703
|
+
color: "#42b883"
|
|
2704
|
+
},
|
|
2705
|
+
vue: {
|
|
2706
|
+
label: "Vue",
|
|
2707
|
+
color: "#42b883"
|
|
2708
|
+
},
|
|
2709
|
+
angular: {
|
|
2710
|
+
label: "Angular",
|
|
2711
|
+
color: "#dd0031"
|
|
2712
|
+
},
|
|
2713
|
+
vanilla: {
|
|
2714
|
+
label: "Vanilla",
|
|
2715
|
+
color: "#f7df1e"
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
const STYLES = `
|
|
2719
|
+
:host { all: initial; }
|
|
2720
|
+
* { box-sizing: border-box; }
|
|
2721
|
+
.bar {
|
|
2722
|
+
position: fixed; left: 0; right: 0; z-index: 2147483000;
|
|
2723
|
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2724
|
+
color: #e8eaed;
|
|
2725
|
+
/* Fond OPAQUE (pas de backdrop-filter sur le conteneur scrollable : il
|
|
2726
|
+
recompositait le blur à chaque frame de scroll → lag). Blur seulement sur
|
|
2727
|
+
.strip (fin, non scrollé). */
|
|
2728
|
+
background: #14161a;
|
|
2729
|
+
--blue:#0067ba; --blue2:#3aa0ff; --orange:#ff8a3d; --ok:#36b37e; --warn:#ffab00;
|
|
2730
|
+
--crit:#ff5630; --info:#4c9aff; --muted:#8a9099; --line:#2a2e36; --card:#1c1f26;
|
|
2731
|
+
box-shadow: 0 -8px 40px rgba(0,0,0,.5);
|
|
2732
|
+
}
|
|
2733
|
+
.bar.bottom { bottom: 0; }
|
|
2734
|
+
.bar.top { top: 0; }
|
|
2735
|
+
.bar::before {
|
|
2736
|
+
content:""; position:absolute; left:0; right:0; height:2px;
|
|
2737
|
+
background: linear-gradient(90deg, var(--blue), var(--blue2), var(--orange));
|
|
2738
|
+
background-size: 200% 100%; animation: flow 4s linear infinite; opacity:.9;
|
|
2739
|
+
}
|
|
2740
|
+
.bar.bottom::before { top:0; } .bar.top::before { bottom:0; }
|
|
2741
|
+
@keyframes flow { to { background-position: 200% 0; } }
|
|
2742
|
+
|
|
2743
|
+
/* Strip responsive : police (et tout le contenu en em) scale avec la largeur
|
|
2744
|
+
d'écran, bornée 12→15px. Padding scale aussi. */
|
|
2745
|
+
.strip { display: flex; align-items: center; gap: clamp(12px,1.1vw,20px);
|
|
2746
|
+
padding: clamp(8px,1vh,13px) clamp(14px,1.4vw,26px); cursor: pointer;
|
|
2747
|
+
font-size: clamp(12px, 0.35vw + 8px, 16px); flex-wrap: nowrap; overflow: hidden;
|
|
2748
|
+
background: rgba(20,22,26,.6); backdrop-filter: blur(14px) saturate(140%); }
|
|
2749
|
+
.strip:hover { background: rgba(255,255,255,.03); }
|
|
2750
|
+
.brand { display:flex; align-items:center; gap:8px; font-weight:800; letter-spacing:.2px; flex:none; }
|
|
2751
|
+
.brand .logo { color: var(--blue2); font-size:1.05em; filter: drop-shadow(0 0 6px rgba(58,160,255,.6)); }
|
|
2752
|
+
.brand .name { background: linear-gradient(90deg,#fff,var(--blue2)); -webkit-background-clip:text;
|
|
2753
|
+
background-clip:text; -webkit-text-fill-color:transparent; }
|
|
2754
|
+
.rt-pill { display:flex; align-items:center; gap:5px; padding:.2em .7em; border-radius:11px; flex:none;
|
|
2755
|
+
font-size:.76em; font-weight:800; letter-spacing:.6px; text-transform:uppercase;
|
|
2756
|
+
color:var(--muted); background:#22262e; border:1px solid var(--line); }
|
|
2757
|
+
.rt-pill.connected { color:#fff; border-color:rgba(58,160,255,.5);
|
|
2758
|
+
background: linear-gradient(90deg, rgba(0,103,186,.35), rgba(255,138,61,.25));
|
|
2759
|
+
box-shadow: 0 0 14px rgba(58,160,255,.35); }
|
|
2760
|
+
.rt-pill .bolt { animation: bolt 1.6s ease-in-out infinite; }
|
|
2761
|
+
.rt-pill.connected .bolt { color: var(--orange); }
|
|
2762
|
+
@keyframes bolt { 0%,100%{opacity:.5;transform:scale(.9)} 50%{opacity:1;transform:scale(1.15)} }
|
|
2763
|
+
|
|
2764
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; color: var(--muted); }
|
|
2765
|
+
.dot.connected { background: var(--ok); color: var(--ok); animation: pulse 2s infinite; }
|
|
2766
|
+
.dot.connecting, .dot.reconnecting { background: var(--warn); color: var(--warn); }
|
|
2767
|
+
.dot.error, .dot.disconnected { background: var(--crit); color: var(--crit); }
|
|
2768
|
+
@keyframes pulse { 0%{box-shadow:0 0 0 0 currentColor} 70%{box-shadow:0 0 0 5px transparent} 100%{box-shadow:0 0 0 0 transparent} }
|
|
2769
|
+
|
|
2770
|
+
.metric { display: flex; align-items: center; gap: 6px; white-space: nowrap; flex:none; }
|
|
2771
|
+
.metric .k { color: var(--muted); text-transform: uppercase; font-size: .76em; letter-spacing:.5px; }
|
|
2772
|
+
.metric .v { font-weight: 700; min-width: 2.6em; font-size: 1em; }
|
|
2773
|
+
.mini { width: 3.8em; height: 1.35em; display:block; }
|
|
2774
|
+
.mini polyline { fill:none; stroke-width:1.5; vector-effect:non-scaling-stroke; }
|
|
2775
|
+
.chip { display:flex; align-items:center; gap:6px; padding:.2em .7em; border-radius:11px; flex:none;
|
|
2776
|
+
background:#22262e; font-weight:700; font-size:.92em; white-space:nowrap; }
|
|
2777
|
+
.chip .k { color: var(--muted); font-size:.82em; text-transform:uppercase; }
|
|
2778
|
+
.spacer { flex: 1 1 auto; min-width: 8px; }
|
|
2779
|
+
.toggle { color: var(--muted); font-size: .85em; transition: transform .25s; flex:none; }
|
|
2780
|
+
.bar.open .toggle { transform: rotate(180deg); }
|
|
2781
|
+
.ok{color:var(--ok)} .warn{color:var(--warn)} .crit{color:var(--crit)} .info{color:var(--info)} .muted{color:var(--muted)} .blue{color:var(--blue2)}
|
|
2782
|
+
.spark.ok{stroke:var(--ok)} .spark.warn{stroke:var(--warn)} .spark.crit{stroke:var(--crit)} .spark.rt{stroke:var(--blue2)}
|
|
2783
|
+
.area.ok{fill:rgba(54,179,126,.12)} .area.warn{fill:rgba(255,171,0,.14)} .area.crit{fill:rgba(255,86,48,.16)} .area.rt{fill:rgba(58,160,255,.16)}
|
|
2784
|
+
|
|
2785
|
+
/* ── Panneau : resize + onglets + panes ──────────────────────────────────── */
|
|
2786
|
+
.panelwrap { display:none; flex-direction:column; border-top:1px solid var(--line); }
|
|
2787
|
+
.bar.open .panelwrap { display:flex; }
|
|
2788
|
+
.resize { height:8px; cursor:ns-resize; display:flex; align-items:center; justify-content:center;
|
|
2789
|
+
flex:none; background:rgba(255,255,255,.015); }
|
|
2790
|
+
.resize::after { content:""; width:42px; height:3px; border-radius:2px; background:var(--line); transition:background .15s; }
|
|
2791
|
+
.resize:hover::after { background:var(--blue2); }
|
|
2792
|
+
.tabs { display:flex; gap:2px; padding:0 8px; flex:none; border-bottom:1px solid var(--line);
|
|
2793
|
+
overflow-x:auto; scrollbar-width:none; }
|
|
2794
|
+
.tabs::-webkit-scrollbar { display:none; }
|
|
2795
|
+
.tab { padding:7px 12px; font:inherit; font-size:11px; font-weight:700; color:var(--muted);
|
|
2796
|
+
cursor:pointer; border:0; background:none; border-bottom:2px solid transparent;
|
|
2797
|
+
text-transform:uppercase; letter-spacing:.4px; white-space:nowrap; display:flex; align-items:center; gap:5px; }
|
|
2798
|
+
.tab:hover { color:#fff; }
|
|
2799
|
+
.tab.active { color:#fff; border-bottom-color:var(--blue2); }
|
|
2800
|
+
.tab .tcount { font-size:9px; padding:0 5px; border-radius:8px; background:#22262e; color:var(--muted); }
|
|
2801
|
+
.tab.active .tcount { background:rgba(58,160,255,.25); color:#fff; }
|
|
2802
|
+
.tab .tcount.crit { background:rgba(255,86,48,.3); color:#fff; }
|
|
2803
|
+
.panes { overflow:hidden; }
|
|
2804
|
+
.pane { display:none; height:100%; overflow:auto; padding:14px; contain:content; }
|
|
2805
|
+
.pane.active { display:block; }
|
|
2806
|
+
.cards { display:grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap:14px; align-items:start; }
|
|
2807
|
+
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:11px 13px; }
|
|
2808
|
+
.card.hero { border-color: rgba(58,160,255,.35);
|
|
2809
|
+
background: linear-gradient(160deg, rgba(0,103,186,.16), rgba(28,31,38,.7) 60%); }
|
|
2810
|
+
.card > h4 { margin:0 0 9px; font-size:10px; letter-spacing:1px; text-transform:uppercase; color:var(--muted); font-weight:800; display:flex; gap:6px; align-items:center; }
|
|
2811
|
+
|
|
2812
|
+
.hero .big { font-size:30px; font-weight:800; line-height:1; letter-spacing:-.5px;
|
|
2813
|
+
background:linear-gradient(90deg,var(--blue2),var(--orange)); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
|
2814
|
+
.hero .big small { font-size:12px; -webkit-text-fill-color:var(--muted); color:var(--muted); font-weight:700; margin-left:4px; }
|
|
2815
|
+
.hero svg { width:100%; height:38px; display:block; margin:6px 0 8px; }
|
|
2816
|
+
.hero polyline { fill:none; stroke-width:1.75; vector-effect:non-scaling-stroke; }
|
|
2817
|
+
.tag { margin-top:9px; padding-top:8px; border-top:1px solid var(--line); color:var(--muted);
|
|
2818
|
+
font-size:10px; line-height:1.5; }
|
|
2819
|
+
.tag b { color:#cfd3d8; }
|
|
2820
|
+
|
|
2821
|
+
.chart { margin-bottom:10px; } .chart:last-child { margin-bottom:0; }
|
|
2822
|
+
.chart .hd { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:2px; }
|
|
2823
|
+
.chart .lbl { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; }
|
|
2824
|
+
.chart .val { font-weight:700; font-size:13px; }
|
|
2825
|
+
.chart .peak { color:var(--muted); font-size:9px; }
|
|
2826
|
+
.chart svg { width:100%; height:${CHART_H}px; display:block; }
|
|
2827
|
+
.chart polyline { fill:none; stroke-width:1.5; vector-effect:non-scaling-stroke; }
|
|
2828
|
+
|
|
2829
|
+
.kv { display:flex; justify-content:space-between; gap:8px; padding:2px 0; }
|
|
2830
|
+
.kv .k { color:var(--muted); } .kv .v { font-weight:600; text-align:right; overflow:hidden; text-overflow:ellipsis; }
|
|
2831
|
+
|
|
2832
|
+
.counts { display:flex; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
|
|
2833
|
+
.feed { font-size:11px; }
|
|
2834
|
+
/* ── Explorateur de journal ─────────────────────────────────────────────── */
|
|
2835
|
+
.counts { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:6px; }
|
|
2836
|
+
.counts .spacer { flex:1 1 auto; }
|
|
2837
|
+
.counts button { font: inherit; color: inherit; background:none; border:1px solid transparent; cursor:pointer; }
|
|
2838
|
+
.chip.sevf { border-color: var(--line); }
|
|
2839
|
+
.chip.sevf.on { border-color: var(--ok); color:#fff; }
|
|
2840
|
+
.search { font: inherit; font-size:11px; color:inherit; background:rgba(255,255,255,.04);
|
|
2841
|
+
border:1px solid var(--line); border-radius:6px; padding:3px 8px; min-width:120px; max-width:220px; }
|
|
2842
|
+
.search::placeholder { color: var(--muted); }
|
|
2843
|
+
.toolbtn { font: inherit; font-size:12px; line-height:1; color:var(--muted); background:none;
|
|
2844
|
+
border:1px solid var(--line); border-radius:6px; padding:3px 7px; cursor:pointer; }
|
|
2845
|
+
.toolbtn:hover { color:#fff; }
|
|
2846
|
+
.toolbtn.on { color:var(--ok); border-color:var(--ok); }
|
|
2847
|
+
.log { display:flex; align-items:baseline; gap:8px; padding:2px 0; cursor:pointer; }
|
|
2848
|
+
.log:hover { background:rgba(255,255,255,.04); }
|
|
2849
|
+
.log .ts { color:var(--muted); font-variant-numeric: tabular-nums; flex:none; }
|
|
2850
|
+
.log .rid { color:var(--muted); font-family:monospace; flex:none; margin-left:auto;
|
|
2851
|
+
border:1px solid var(--line); border-radius:4px; padding:0 4px; }
|
|
2852
|
+
.logdetail { margin:2px 0 6px 0; padding:6px 8px; border-left:2px solid var(--line);
|
|
2853
|
+
background:rgba(255,255,255,.03); border-radius:0 6px 6px 0; }
|
|
2854
|
+
.drow { display:flex; gap:8px; padding:1px 0; }
|
|
2855
|
+
.drow .dk { color:var(--muted); min-width:92px; flex:none; }
|
|
2856
|
+
.drow .dv { word-break:break-word; }
|
|
2857
|
+
.feed .empty { color:var(--muted); padding:6px 0; }
|
|
2858
|
+
.log { display:flex; gap:7px; padding:2px 0; align-items:baseline; border-bottom:1px solid rgba(255,255,255,.04); }
|
|
2859
|
+
.log .sev { flex:none; width:54px; font-size:9px; font-weight:800; text-transform:uppercase; }
|
|
2860
|
+
.log .mod { flex:none; color:var(--muted); max-width:90px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
2861
|
+
.log .txt { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
2862
|
+
|
|
2863
|
+
.card.fe { border-color: rgba(255,138,61,.3);
|
|
2864
|
+
background: linear-gradient(160deg, rgba(255,138,61,.12), rgba(28,31,38,.7) 60%); }
|
|
2865
|
+
.fw { display:flex; align-items:center; gap:9px; margin-bottom:9px; }
|
|
2866
|
+
.fw .badge { padding:2px 10px; border-radius:7px; font-weight:800; font-size:11px; color:#0b0d10; }
|
|
2867
|
+
.fw .name { color:var(--muted); }
|
|
2868
|
+
.fe .hd { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:2px; }
|
|
2869
|
+
.fe .lbl { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; }
|
|
2870
|
+
.hmr-big { font-size:26px; font-weight:800; line-height:1; transition: color .2s; }
|
|
2871
|
+
.hmr-big.hmr-flash { color: var(--orange); text-shadow: 0 0 14px rgba(255,138,61,.7); }
|
|
2872
|
+
.fe svg { width:100%; height:34px; display:block; margin:6px 0 8px; }
|
|
2873
|
+
.fe polyline { fill:none; stroke-width:1.75; vector-effect:non-scaling-stroke; }
|
|
2874
|
+
.spark.fe { stroke:var(--orange); } .area.fe { fill:rgba(255,138,61,.16); }
|
|
2875
|
+
|
|
2876
|
+
.env-badge { padding:.2em .7em; border-radius:6px; font-size:.76em; font-weight:800;
|
|
2877
|
+
letter-spacing:.6px; text-transform:uppercase; color:#0b0d10; background:var(--muted); flex:none; }
|
|
2878
|
+
.env-badge.dev { background: var(--ok); } .env-badge.prod { background: var(--crit); color:#fff; }
|
|
2879
|
+
.env-badge.test { background: var(--warn); } .env-badge.staging { background:#a06bff; color:#fff; }
|
|
2880
|
+
.branch { display:flex; align-items:center; gap:5px; padding:2px 9px; border-radius:6px; flex:none;
|
|
2881
|
+
background:#22262e; font-weight:700; max-width:200px; cursor:help; }
|
|
2882
|
+
.branch .git { color:var(--blue2); } .branch span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
2883
|
+
/* Les contrôles et les indicateurs du bandeau sont de VRAIS <button> : ils
|
|
2884
|
+
s'atteignent au clavier et s'annoncent. Ce bloc leur retire l'apparence
|
|
2885
|
+
native que le navigateur leur donne, sans leur retirer leur nature. */
|
|
2886
|
+
.strip button { font: inherit; color: inherit; background: none; border: 0; margin: 0; padding: 0; cursor: pointer; }
|
|
2887
|
+
.strip button:focus-visible, .tab:focus-visible, .toolbtn:focus-visible {
|
|
2888
|
+
outline: 2px solid var(--ok); outline-offset: 2px; border-radius: 4px; }
|
|
2889
|
+
.goto:hover .v, .goto:hover .k { color: #fff; }
|
|
2890
|
+
|
|
2891
|
+
/* ── Infobulles ────────────────────────────────────────────────────────────
|
|
2892
|
+
Maison, et non l'attribut « title » du navigateur : celui-là met environ une
|
|
2893
|
+
seconde à venir, ne se style pas, et surtout n'apparaît JAMAIS au focus
|
|
2894
|
+
clavier — l'aide restait donc inaccessible à qui n'utilise pas la souris.
|
|
2895
|
+
Celle-ci s'ouvre au survol ET au focus, ce qui la rend utilisable par tout le
|
|
2896
|
+
monde. En CSS pur : aucun script, aucun écouteur, rien à libérer. */
|
|
2897
|
+
[data-tip] { position: relative; }
|
|
2898
|
+
[data-tip]::after {
|
|
2899
|
+
content: attr(data-tip);
|
|
2900
|
+
position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%);
|
|
2901
|
+
width: max-content; max-width: 280px; white-space: normal; text-align: left;
|
|
2902
|
+
background: #0b0b0d; color: #f2f2f2; border: 1px solid var(--line);
|
|
2903
|
+
border-radius: 6px; padding: 6px 9px; font-size: 11px; line-height: 1.45;
|
|
2904
|
+
font-weight: 400; text-transform: none; letter-spacing: 0;
|
|
2905
|
+
box-shadow: 0 6px 20px rgba(0,0,0,.5);
|
|
2906
|
+
opacity: 0; visibility: hidden; transition: opacity .12s ease; pointer-events: none; z-index: 20;
|
|
2907
|
+
}
|
|
2908
|
+
[data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; visibility: visible; }
|
|
2909
|
+
/* Barre ancrée en haut : l'infobulle bascule dessous, sinon elle sort de l'écran. */
|
|
2910
|
+
.bar.top [data-tip]::after { bottom: auto; top: calc(100% + 9px); }
|
|
2911
|
+
/* Les bords : l'infobulle se recale pour ne pas déborder de la fenêtre. */
|
|
2912
|
+
.strip > :first-child[data-tip]::after { left: 0; transform: none; }
|
|
2913
|
+
.strip > :last-child[data-tip]::after { left: auto; right: 0; transform: none; }
|
|
2914
|
+
@media (prefers-reduced-motion: reduce) { [data-tip]::after { transition: none; } }
|
|
2915
|
+
|
|
2916
|
+
.ctrl { color:var(--muted); cursor:pointer; padding:0 4px; font-weight:800; font-size:1.05em; line-height:1; flex:none; }
|
|
2917
|
+
.ctrl:hover { color:#fff; }
|
|
2918
|
+
.ctrl.live { font-size:.78em; font-weight:700; border:1px solid var(--line); border-radius:6px; padding:2px 7px; letter-spacing:.3px; }
|
|
2919
|
+
.ctrl.live.on { color:var(--ok); border-color:var(--ok); }
|
|
2920
|
+
.ctrl.live:not(.on) { color:var(--muted); }
|
|
2921
|
+
|
|
2922
|
+
.minbar { position:fixed; z-index:2147483000; display:none; align-items:center; gap:8px;
|
|
2923
|
+
padding:6px 13px; border-radius:22px; cursor:pointer;
|
|
2924
|
+
font:12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color:#e8eaed;
|
|
2925
|
+
background:rgba(18,20,25,.92); backdrop-filter:blur(12px);
|
|
2926
|
+
border:1px solid rgba(58,160,255,.45); box-shadow:0 4px 20px rgba(0,0,0,.5); }
|
|
2927
|
+
.minbar:hover { border-color:#3aa0ff; box-shadow:0 4px 26px rgba(58,160,255,.4); }
|
|
2928
|
+
.minbar.bottom { bottom:14px; } .minbar.top { top:14px; }
|
|
2929
|
+
.minbar.dock-left { left:14px; } .minbar.dock-right { right:14px; }
|
|
2930
|
+
.minbar .dot { width:8px; height:8px; border-radius:50%; background:#36b37e; }
|
|
2931
|
+
.minbar .dot.connected { background:#36b37e; } .minbar .dot.disconnected,.minbar .dot.error { background:#ff5630; }
|
|
2932
|
+
.minbar .dot.connecting,.minbar .dot.reconnecting { background:#ffab00; }
|
|
2933
|
+
.minbar .mlogo { color:#ff8a3d; } .minbar .mrate { font-weight:800; }
|
|
2934
|
+
.minbar .mbadge { font-size:9px; font-weight:800; text-transform:uppercase; color:#8a9099; }
|
|
2935
|
+
|
|
2936
|
+
/* ── Network ─────────────────────────────────────────────────────────────── */
|
|
2937
|
+
.net-head { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
|
|
2938
|
+
.net-clear { margin-left:auto; color:var(--muted); cursor:pointer; font-size:10px;
|
|
2939
|
+
text-transform:uppercase; letter-spacing:.5px; padding:2px 6px; border-radius:6px; }
|
|
2940
|
+
.net-clear:hover { color:#fff; background:rgba(255,255,255,.06); }
|
|
2941
|
+
.net-list { border-top:1px solid var(--line); }
|
|
2942
|
+
.net-list .empty { color:var(--muted); padding:8px 0; }
|
|
2943
|
+
.net-row { display:flex; align-items:center; gap:9px; padding:3px 4px; cursor:pointer;
|
|
2944
|
+
border-bottom:1px solid rgba(255,255,255,.04); }
|
|
2945
|
+
.net-row:hover { background:rgba(255,255,255,.04); }
|
|
2946
|
+
.net-row.sel { background:rgba(58,160,255,.14); }
|
|
2947
|
+
.net-row.err .net-path { color:#ffb4a6; }
|
|
2948
|
+
.net-method { flex:none; width:46px; text-align:center; font-weight:800; font-size:9px;
|
|
2949
|
+
padding:1px 0; border-radius:5px; text-transform:uppercase; color:#0b0d10; background:var(--muted); }
|
|
2950
|
+
.net-method.get { background:var(--blue2); } .net-method.post { background:var(--ok); }
|
|
2951
|
+
.net-method.put,.net-method.patch { background:var(--orange); }
|
|
2952
|
+
.net-method.delete { background:var(--crit); color:#fff; }
|
|
2953
|
+
.net-method.ws { background:#a06bff; color:#fff; }
|
|
2954
|
+
.net-path { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
2955
|
+
.net-status { flex:none; width:38px; text-align:center; font-weight:800; font-size:10px; }
|
|
2956
|
+
.net-status.s2 { color:var(--ok); } .net-status.s3 { color:var(--info); }
|
|
2957
|
+
.net-status.s4 { color:var(--warn); } .net-status.s5 { color:var(--crit); }
|
|
2958
|
+
.net-status.sp { color:var(--muted); }
|
|
2959
|
+
.net-dur { flex:none; width:58px; text-align:right; color:var(--muted); font-size:10px; }
|
|
2960
|
+
.net-rid { flex:none; color:var(--blue2); font-size:9px; opacity:.7; }
|
|
2961
|
+
|
|
2962
|
+
.net-detail { margin-top:10px; border-top:1px solid var(--line); padding-top:10px; }
|
|
2963
|
+
.net-detail .empty { color:var(--muted); }
|
|
2964
|
+
.det-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(160px,1fr)); gap:2px 14px; margin-bottom:10px; }
|
|
2965
|
+
.wf { display:flex; flex-direction:column; gap:3px; }
|
|
2966
|
+
.wf-title { color:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:.5px; margin-bottom:4px; }
|
|
2967
|
+
.wf-row { display:flex; align-items:center; gap:8px; }
|
|
2968
|
+
.wf-name { flex:none; width:74px; text-align:right; color:#cfd3d8; font-size:10px; }
|
|
2969
|
+
.wf-track { flex:1; position:relative; height:14px; background:rgba(255,255,255,.04); border-radius:3px; }
|
|
2970
|
+
.wf-bar { position:absolute; top:0; bottom:0; border-radius:3px; min-width:2px;
|
|
2971
|
+
box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }
|
|
2972
|
+
.wf-ms { flex:none; width:62px; text-align:right; color:var(--muted); font-size:10px; }
|
|
2973
|
+
.wf-bar.parse { background:#4c9aff; } .wf-bar.resolve { background:#3aa0ff; }
|
|
2974
|
+
.wf-bar.firewall { background:#ff8a3d; } .wf-bar.init { background:#a06bff; }
|
|
2975
|
+
.wf-bar.action { background:#36b37e; } .wf-bar.render { background:#ffab00; }
|
|
2976
|
+
.wf-bar.send { background:#00b8d9; } .wf-bar.other { background:#8a9099; }
|
|
2977
|
+
.wf-bar.identity { background:#ff5c8a; }
|
|
2978
|
+
.det-loading,.det-err { color:var(--muted); padding:6px 0; } .det-err { color:var(--crit); }
|
|
2979
|
+
|
|
2980
|
+
/* Network pane = split DevTools : liste (scroll) + détail (scroll), détail
|
|
2981
|
+
TOUJOURS visible (≠ tout dans un seul scroll où le détail finit hors écran). */
|
|
2982
|
+
.pane.np.active { display:flex; flex-direction:column; padding:0; }
|
|
2983
|
+
.np .net-head { padding:10px 14px 8px; flex:none; margin:0; }
|
|
2984
|
+
.np .net-list { flex:1 1 auto; min-height:56px; overflow:auto; padding:0 14px; border-top:1px solid var(--line); }
|
|
2985
|
+
/* Détail : placeholder = petit (flex:0). Sélection active (.np.sel) → le détail
|
|
2986
|
+
prend une vraie part (60%) avec son propre scroll → le waterfall est visible,
|
|
2987
|
+
la liste se réduit. */
|
|
2988
|
+
.np .net-detail { flex:0 1 auto; max-height:50%; overflow:auto; padding:8px 14px 12px; margin-top:0; border-top:1px solid var(--line); }
|
|
2989
|
+
.np.sel .net-list { flex:1 1 40%; min-height:48px; }
|
|
2990
|
+
.np.sel .net-detail { flex:1 1 60%; max-height:none; }
|
|
2991
|
+
.det-bar { display:flex; justify-content:flex-end; margin-bottom:2px; }
|
|
2992
|
+
.det-close { cursor:pointer; color:var(--muted); font-weight:800; font-size:15px; line-height:1; padding:0 4px; }
|
|
2993
|
+
.det-close:hover { color:#fff; }
|
|
2994
|
+
`;
|
|
2995
|
+
function pushCap(arr, v, cap) {
|
|
2996
|
+
arr.push(v);
|
|
2997
|
+
if (arr.length > cap) arr.shift();
|
|
2998
|
+
}
|
|
2999
|
+
function clampH(h) {
|
|
3000
|
+
const max = typeof window !== "undefined" ? window.innerHeight * .85 : 700;
|
|
3001
|
+
if (h < PANEL_H_MIN) return PANEL_H_MIN;
|
|
3002
|
+
if (h > max) return Math.round(max);
|
|
3003
|
+
return Math.round(h);
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Version du format de l'état persisté.
|
|
3007
|
+
*
|
|
3008
|
+
* Elle existe pour une raison précise : ces clés survivent aux mises à jour du
|
|
3009
|
+
* framework, dans un navigateur que personne ne nettoie. Le jour où l'une change
|
|
3010
|
+
* de sens, une valeur écrite par une version d'avant est lue comme si elle
|
|
3011
|
+
* disait la même chose — et la barre s'ouvre dans un état incohérent que
|
|
3012
|
+
* l'utilisateur ne sait pas défaire. Au changement de version, tout est purgé :
|
|
3013
|
+
* on perd une préférence d'affichage, ce qui ne coûte rien, plutôt que de
|
|
3014
|
+
* charger un état dont on ne sait rien.
|
|
3015
|
+
*/
|
|
3016
|
+
const LS_VERSION = "1";
|
|
3017
|
+
/** Clés de persistance localStorage (état chrome de la barre). */
|
|
3018
|
+
const LS = {
|
|
3019
|
+
/** Version du format — jamais purgée, c'est elle qui décide de la purge. */
|
|
3020
|
+
version: "nf.debugbar.v",
|
|
3021
|
+
visible: "nf.debugbar.visible",
|
|
3022
|
+
min: "nf.debugbar.min",
|
|
3023
|
+
side: "nf.debugbar.side",
|
|
3024
|
+
tab: "nf.debugbar.tab",
|
|
3025
|
+
h: "nf.debugbar.h",
|
|
3026
|
+
live: "nf.debugbar.live"
|
|
3027
|
+
};
|
|
3028
|
+
/**
|
|
3029
|
+
* Toutes les clés que la barre écrit sur CE navigateur.
|
|
3030
|
+
*
|
|
3031
|
+
* Cette liste n'est pas décorative : elle est ce que la barre AFFICHE dans son
|
|
3032
|
+
* onglet Runtime et ce que sa purge efface. Trois choses qui doivent rester
|
|
3033
|
+
* d'accord, donc une seule source.
|
|
3034
|
+
*/
|
|
3035
|
+
function lsKeys() {
|
|
3036
|
+
return Object.values(LS);
|
|
3037
|
+
}
|
|
3038
|
+
/** Efface l'état persisté de la barre — et rien d'autre du stockage de l'app. */
|
|
3039
|
+
function lsPurge() {
|
|
3040
|
+
try {
|
|
3041
|
+
for (const k of lsKeys()) localStorage.removeItem(k);
|
|
3042
|
+
} catch {}
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Purge l'état si le format a changé depuis la dernière visite.
|
|
3046
|
+
*
|
|
3047
|
+
* Appelée avant toute lecture — sinon on lirait justement les valeurs qu'on
|
|
3048
|
+
* s'apprête à jeter.
|
|
3049
|
+
*/
|
|
3050
|
+
function lsMigrate() {
|
|
3051
|
+
try {
|
|
3052
|
+
if (localStorage.getItem(LS.version) === LS_VERSION) return;
|
|
3053
|
+
lsPurge();
|
|
3054
|
+
localStorage.setItem(LS.version, LS_VERSION);
|
|
3055
|
+
} catch {}
|
|
3056
|
+
}
|
|
3057
|
+
function lsGet(key, def) {
|
|
3058
|
+
try {
|
|
3059
|
+
return localStorage.getItem(key) ?? def;
|
|
3060
|
+
} catch {
|
|
3061
|
+
return def;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function lsSet(key, val) {
|
|
3065
|
+
try {
|
|
3066
|
+
localStorage.setItem(key, val);
|
|
3067
|
+
} catch {}
|
|
3068
|
+
}
|
|
3069
|
+
const GLOBAL_KEY = "__NODEFONY_DEBUGBAR__";
|
|
3070
|
+
/**
|
|
3071
|
+
* Widget DOM. Lazy : ne construit le Shadow DOM qu'au `mount()`. Idempotent
|
|
3072
|
+
* (un seul `#nodefony-debugbar` par page).
|
|
3073
|
+
*/
|
|
3074
|
+
var DebugBar = class DebugBar {
|
|
3075
|
+
client;
|
|
3076
|
+
model = new DebugBarModel();
|
|
3077
|
+
url;
|
|
3078
|
+
position;
|
|
3079
|
+
startOpen;
|
|
3080
|
+
ownClient;
|
|
3081
|
+
frontend;
|
|
3082
|
+
host = null;
|
|
3083
|
+
bar = null;
|
|
3084
|
+
visible;
|
|
3085
|
+
minimized;
|
|
3086
|
+
side;
|
|
3087
|
+
activeTab;
|
|
3088
|
+
panelH;
|
|
3089
|
+
live;
|
|
3090
|
+
/** dispose des abonnements realtime (null quand OFF). */
|
|
3091
|
+
liveOff = null;
|
|
3092
|
+
rafPending = false;
|
|
3093
|
+
feedLen = -1;
|
|
3094
|
+
/** Filtre de sévérité de l'onglet Logs — `all` par défaut. */
|
|
3095
|
+
feedSev = "all";
|
|
3096
|
+
/** Recherche courante, déjà en minuscules (comparée telle quelle). */
|
|
3097
|
+
feedQuery = "";
|
|
3098
|
+
/**
|
|
3099
|
+
* Affichage suspendu. Les entrées continuent d'arriver dans le modèle : à la
|
|
3100
|
+
* reprise, la liste se reconstruit complète — mettre en pause ne fait pas
|
|
3101
|
+
* perdre ce qui s'est passé pendant, ce qui serait le pire des deux mondes.
|
|
3102
|
+
*/
|
|
3103
|
+
feedPaused = false;
|
|
3104
|
+
/** La liste affichée doit être refaite (changement de filtre ou de recherche). */
|
|
3105
|
+
feedDirty = false;
|
|
3106
|
+
prevFrames = 0;
|
|
3107
|
+
rtRate = 0;
|
|
3108
|
+
rtPeak = 0;
|
|
3109
|
+
rtSeries = [];
|
|
3110
|
+
viteConnected = false;
|
|
3111
|
+
hmrCount = 0;
|
|
3112
|
+
hmrPrev = 0;
|
|
3113
|
+
hmrRate = 0;
|
|
3114
|
+
hmrLast = "—";
|
|
3115
|
+
hmrSeries = [];
|
|
3116
|
+
flashTimer = null;
|
|
3117
|
+
disposers = [];
|
|
3118
|
+
el = Object.create(null);
|
|
3119
|
+
networkEnabled;
|
|
3120
|
+
profilerBase;
|
|
3121
|
+
net = new NetworkModel();
|
|
3122
|
+
/** Observe la barre → publie sa hauteur (var CSS) pour l'app hôte. */
|
|
3123
|
+
ro = null;
|
|
3124
|
+
netRows = /* @__PURE__ */ new Map();
|
|
3125
|
+
selectedRid = null;
|
|
3126
|
+
selRowId = null;
|
|
3127
|
+
selNoRid = false;
|
|
3128
|
+
detailVersion = 0;
|
|
3129
|
+
detailRendered = -1;
|
|
3130
|
+
constructor(opts = {}) {
|
|
3131
|
+
this.url = opts.url ?? DEFAULT_PATH;
|
|
3132
|
+
this.position = opts.position ?? "bottom";
|
|
3133
|
+
this.startOpen = opts.open ?? false;
|
|
3134
|
+
this.ownClient = false;
|
|
3135
|
+
this.frontend = opts.frontend ?? null;
|
|
3136
|
+
this.networkEnabled = opts.network !== false;
|
|
3137
|
+
this.profilerBase = (opts.profilerBase ?? DEFAULT_PROFILER_BASE).replace(/\/$/, "");
|
|
3138
|
+
this.client = opts.client ?? RealtimeClient.shared({ url: this.url });
|
|
3139
|
+
lsMigrate();
|
|
3140
|
+
this.visible = lsGet(LS.visible, "1") !== "0";
|
|
3141
|
+
this.minimized = lsGet(LS.min, "0") === "1";
|
|
3142
|
+
this.side = lsGet(LS.side, "right") === "left" ? "left" : "right";
|
|
3143
|
+
this.activeTab = lsGet(LS.tab, "realtime") || "realtime";
|
|
3144
|
+
this.panelH = clampH(parseInt(lsGet(LS.h, String(defaultPanelH())), 10) || defaultPanelH());
|
|
3145
|
+
this.live = lsGet(LS.live, "0") === "1";
|
|
3146
|
+
}
|
|
3147
|
+
/** Construit le DOM, branche le realtime et ouvre la connexion. No-op si déjà monté. */
|
|
3148
|
+
mount() {
|
|
3149
|
+
if (typeof document === "undefined") return this;
|
|
3150
|
+
if (document.getElementById(HOST_ID)) return this;
|
|
3151
|
+
this.buildDom();
|
|
3152
|
+
this.wireRealtime();
|
|
3153
|
+
this.wireHmr();
|
|
3154
|
+
this.wireNetwork();
|
|
3155
|
+
this.applyChrome();
|
|
3156
|
+
this.registerHandle();
|
|
3157
|
+
this.client.connect().catch(() => {});
|
|
3158
|
+
this.render();
|
|
3159
|
+
return this;
|
|
3160
|
+
}
|
|
3161
|
+
/** Détache listeners, ferme la connexion (si propriétaire) et retire le DOM. */
|
|
3162
|
+
unmount() {
|
|
3163
|
+
for (const d of this.disposers) d();
|
|
3164
|
+
this.disposers.length = 0;
|
|
3165
|
+
if (this.flashTimer) clearTimeout(this.flashTimer);
|
|
3166
|
+
this.flashTimer = null;
|
|
3167
|
+
if (this.ownClient) this.client.disconnect();
|
|
3168
|
+
this.unregisterHandle();
|
|
3169
|
+
this.ro?.disconnect();
|
|
3170
|
+
this.ro = null;
|
|
3171
|
+
try {
|
|
3172
|
+
document.documentElement.style.removeProperty("--nodefony-debugbar-height");
|
|
3173
|
+
} catch {}
|
|
3174
|
+
this.host?.remove();
|
|
3175
|
+
this.host = null;
|
|
3176
|
+
this.bar = null;
|
|
3177
|
+
}
|
|
3178
|
+
/** Affiche/masque la barre (pilotable depuis une app via le handle global). */
|
|
3179
|
+
setVisible(v) {
|
|
3180
|
+
this.visible = v;
|
|
3181
|
+
lsSet(LS.visible, v ? "1" : "0");
|
|
3182
|
+
this.applyChrome();
|
|
3183
|
+
}
|
|
3184
|
+
setMinimized(v) {
|
|
3185
|
+
this.minimized = v;
|
|
3186
|
+
lsSet(LS.min, v ? "1" : "0");
|
|
3187
|
+
this.applyChrome();
|
|
3188
|
+
}
|
|
3189
|
+
toggleSide() {
|
|
3190
|
+
this.side = this.side === "left" ? "right" : "left";
|
|
3191
|
+
lsSet(LS.side, this.side);
|
|
3192
|
+
this.applyChrome();
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Déplie ou replie le panneau — le SEUL endroit qui bascule cet état.
|
|
3196
|
+
*
|
|
3197
|
+
* Il porte aussi ce qu'un lecteur d'écran doit entendre : `aria-expanded` sur
|
|
3198
|
+
* le bouton qui commande, et un nom qui dit l'action à venir (« Déplier » /
|
|
3199
|
+
* « Replier »), pas l'état courant — un nom qui décrit l'état laisse
|
|
3200
|
+
* l'utilisateur deviner ce que le bouton va faire.
|
|
3201
|
+
*/
|
|
3202
|
+
setOpen(open) {
|
|
3203
|
+
const bar = this.bar;
|
|
3204
|
+
if (!bar) return;
|
|
3205
|
+
bar.classList.toggle("open", open);
|
|
3206
|
+
const btn = this.el.btnToggle;
|
|
3207
|
+
if (btn) {
|
|
3208
|
+
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
3209
|
+
const label = open ? "Replier le panneau" : "Déplier le panneau";
|
|
3210
|
+
btn.setAttribute("aria-label", label);
|
|
3211
|
+
btn.setAttribute("data-tip", label);
|
|
3212
|
+
}
|
|
3213
|
+
this.render();
|
|
3214
|
+
}
|
|
3215
|
+
setTab(tab) {
|
|
3216
|
+
this.activeTab = tab;
|
|
3217
|
+
lsSet(LS.tab, tab);
|
|
3218
|
+
this.applyTab();
|
|
3219
|
+
this.render();
|
|
3220
|
+
}
|
|
3221
|
+
setPanelH(h) {
|
|
3222
|
+
this.panelH = clampH(h);
|
|
3223
|
+
const panes = this.el.panes;
|
|
3224
|
+
if (panes) panes.style.height = `${this.panelH}px`;
|
|
3225
|
+
}
|
|
3226
|
+
/** Reflète l'onglet actif sur les boutons + panes. */
|
|
3227
|
+
applyTab() {
|
|
3228
|
+
(this.bar?.querySelectorAll(".tab"))?.forEach((t) => t.classList.toggle("active", t.getAttribute("data-tab") === this.activeTab));
|
|
3229
|
+
(this.bar?.querySelectorAll(".pane"))?.forEach((p) => p.classList.toggle("active", p.getAttribute("data-pane") === this.activeTab));
|
|
3230
|
+
}
|
|
3231
|
+
/** Applique l'état chrome au DOM (display + classes de dock + hauteur). */
|
|
3232
|
+
applyChrome() {
|
|
3233
|
+
if (!this.host || !this.bar) return;
|
|
3234
|
+
this.host.style.display = this.visible ? "" : "none";
|
|
3235
|
+
this.bar.style.display = this.minimized ? "none" : "";
|
|
3236
|
+
const min = this.el.minbar;
|
|
3237
|
+
if (min) {
|
|
3238
|
+
min.style.display = this.minimized ? "flex" : "none";
|
|
3239
|
+
min.setAttribute("class", `minbar ${this.position} dock-${this.side}`);
|
|
3240
|
+
}
|
|
3241
|
+
this.setPanelH(this.panelH);
|
|
3242
|
+
this.applyTab();
|
|
3243
|
+
this.publishHeight();
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Publie la hauteur occupée par la barre (dock bas, visible, dépliée) en
|
|
3247
|
+
* variable CSS `--nodefony-debugbar-height` sur `:root`. L'app hôte (Studio)
|
|
3248
|
+
* la réserve en `padding-bottom` → le contenu n'est jamais masqué. `0px` si
|
|
3249
|
+
* masquée / réduite (chip flottante) / dockée en haut.
|
|
3250
|
+
*/
|
|
3251
|
+
publishHeight() {
|
|
3252
|
+
if (typeof document === "undefined") return;
|
|
3253
|
+
const h = this.visible && !this.minimized && this.position === "bottom" && this.bar ? this.bar.offsetHeight : 0;
|
|
3254
|
+
document.documentElement.style.setProperty("--nodefony-debugbar-height", `${h}px`);
|
|
3255
|
+
}
|
|
3256
|
+
registerHandle() {
|
|
3257
|
+
globalThis[GLOBAL_KEY] = {
|
|
3258
|
+
isVisible: () => this.visible,
|
|
3259
|
+
setVisible: (v) => this.setVisible(v),
|
|
3260
|
+
toggle: () => this.setVisible(!this.visible),
|
|
3261
|
+
minimize: () => this.setMinimized(true),
|
|
3262
|
+
restore: () => this.setMinimized(false)
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
unregisterHandle() {
|
|
3266
|
+
try {
|
|
3267
|
+
delete globalThis[GLOBAL_KEY];
|
|
3268
|
+
} catch {}
|
|
3269
|
+
}
|
|
3270
|
+
buildDom() {
|
|
3271
|
+
const host = document.createElement("div");
|
|
3272
|
+
host.id = HOST_ID;
|
|
3273
|
+
host.setAttribute("role", "complementary");
|
|
3274
|
+
host.setAttribute("aria-label", "Barre de débogage Nodefony");
|
|
3275
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
3276
|
+
const style = document.createElement("style");
|
|
3277
|
+
style.textContent = STYLES;
|
|
3278
|
+
const bar = document.createElement("div");
|
|
3279
|
+
bar.className = `bar ${this.position}${this.startOpen ? " open" : ""}`;
|
|
3280
|
+
bar.innerHTML = this.template();
|
|
3281
|
+
const minbar = document.createElement("div");
|
|
3282
|
+
minbar.className = `minbar ${this.position} dock-${this.side}`;
|
|
3283
|
+
minbar.innerHTML = `<span class="dot" data-el="mdot"></span><span class="mlogo">⚡</span><span class="mrate" data-el="mrate">0/s</span><span class="mbadge" data-el="mEnv"></span>`;
|
|
3284
|
+
shadow.append(style, bar, minbar);
|
|
3285
|
+
document.body.appendChild(host);
|
|
3286
|
+
this.host = host;
|
|
3287
|
+
this.bar = bar;
|
|
3288
|
+
this.el.minbar = minbar;
|
|
3289
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
3290
|
+
this.ro = new ResizeObserver(() => this.publishHeight());
|
|
3291
|
+
this.ro.observe(bar);
|
|
3292
|
+
}
|
|
3293
|
+
shadow.querySelectorAll("[data-el]").forEach((node) => {
|
|
3294
|
+
const key = node.getAttribute("data-el");
|
|
3295
|
+
if (key) this.el[key] = node;
|
|
3296
|
+
});
|
|
3297
|
+
const strip = bar.querySelector(".strip");
|
|
3298
|
+
this.wireBtn("btnToggle", () => this.setOpen(!bar.classList.contains("open")));
|
|
3299
|
+
const onGoto = (ev) => {
|
|
3300
|
+
const id = (ev.target?.closest?.(".goto"))?.getAttribute("data-goto");
|
|
3301
|
+
if (!id) return;
|
|
3302
|
+
this.setOpen(true);
|
|
3303
|
+
this.setTab(id);
|
|
3304
|
+
};
|
|
3305
|
+
strip.addEventListener("click", onGoto);
|
|
3306
|
+
this.disposers.push(() => strip.removeEventListener("click", onGoto));
|
|
3307
|
+
this.wireBtn("btnMin", () => this.setMinimized(true));
|
|
3308
|
+
this.wireBtn("btnSide", () => this.toggleSide());
|
|
3309
|
+
this.wireBtn("btnLive", () => this.setLive(!this.live));
|
|
3310
|
+
this.updateLiveBtn();
|
|
3311
|
+
const tabsBar = bar.querySelector(".tabs");
|
|
3312
|
+
if (tabsBar) {
|
|
3313
|
+
const onTab = (ev) => {
|
|
3314
|
+
const id = (ev.target?.closest?.(".tab"))?.getAttribute("data-tab");
|
|
3315
|
+
if (id) this.setTab(id);
|
|
3316
|
+
};
|
|
3317
|
+
tabsBar.addEventListener("click", onTab);
|
|
3318
|
+
this.disposers.push(() => tabsBar.removeEventListener("click", onTab));
|
|
3319
|
+
}
|
|
3320
|
+
const resize = this.el.resize;
|
|
3321
|
+
if (resize) {
|
|
3322
|
+
const onDown = (e) => this.startResize(e);
|
|
3323
|
+
resize.addEventListener("pointerdown", onDown);
|
|
3324
|
+
this.disposers.push(() => resize.removeEventListener("pointerdown", onDown));
|
|
3325
|
+
}
|
|
3326
|
+
const counts = bar.querySelector(".counts");
|
|
3327
|
+
if (counts) {
|
|
3328
|
+
const onSev = (ev) => {
|
|
3329
|
+
const sev = (ev.target?.closest?.(".sevf"))?.getAttribute("data-sev");
|
|
3330
|
+
if (sev !== "all" && sev !== "err" && sev !== "warn") return;
|
|
3331
|
+
this.feedSev = this.feedSev === sev ? "all" : sev;
|
|
3332
|
+
counts.querySelectorAll(".sevf").forEach((n) => n.classList.toggle("on", n.getAttribute("data-sev") === this.feedSev && this.feedSev !== "all"));
|
|
3333
|
+
this.feedDirty = true;
|
|
3334
|
+
this.render();
|
|
3335
|
+
};
|
|
3336
|
+
counts.addEventListener("click", onSev);
|
|
3337
|
+
this.disposers.push(() => counts.removeEventListener("click", onSev));
|
|
3338
|
+
}
|
|
3339
|
+
const search = this.el.logSearch;
|
|
3340
|
+
if (search) {
|
|
3341
|
+
const onSearch = () => {
|
|
3342
|
+
this.feedQuery = search.value.trim().toLowerCase();
|
|
3343
|
+
this.feedDirty = true;
|
|
3344
|
+
this.render();
|
|
3345
|
+
};
|
|
3346
|
+
search.addEventListener("input", onSearch);
|
|
3347
|
+
this.disposers.push(() => search.removeEventListener("input", onSearch));
|
|
3348
|
+
}
|
|
3349
|
+
this.wireBtn("btnPause", () => {
|
|
3350
|
+
this.feedPaused = !this.feedPaused;
|
|
3351
|
+
const b = this.el.btnPause;
|
|
3352
|
+
if (b) {
|
|
3353
|
+
b.setAttribute("aria-pressed", this.feedPaused ? "true" : "false");
|
|
3354
|
+
b.classList.toggle("on", this.feedPaused);
|
|
3355
|
+
b.textContent = this.feedPaused ? "▶" : "⏸";
|
|
3356
|
+
const tip = this.feedPaused ? "Reprendre l'affichage — la liste se recompose avec ce qui est arrivé pendant la pause" : "Suspendre l'affichage — les entrées continuent d'arriver et se rattrapent à la reprise";
|
|
3357
|
+
b.setAttribute("data-tip", tip);
|
|
3358
|
+
}
|
|
3359
|
+
if (!this.feedPaused) {
|
|
3360
|
+
this.feedDirty = true;
|
|
3361
|
+
this.render();
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
this.wireBtn("btnCopy", () => {
|
|
3365
|
+
const node = this.el.feed;
|
|
3366
|
+
const text = node ? node.textContent ?? "" : "";
|
|
3367
|
+
navigator.clipboard?.writeText(text).then(() => this.flashBtn("btnCopy", "✓"), () => this.flashBtn("btnCopy", "✗"));
|
|
3368
|
+
});
|
|
3369
|
+
this.wireBtn("btnClearFeed", () => {
|
|
3370
|
+
const node = this.el.feed;
|
|
3371
|
+
if (node) node.innerHTML = `<div class="empty">liste vidée — les nouvelles entrées s'afficheront ici.</div>`;
|
|
3372
|
+
this.feedLen = this.model.view.feed.length;
|
|
3373
|
+
});
|
|
3374
|
+
const feed = this.el.feed;
|
|
3375
|
+
if (feed) {
|
|
3376
|
+
const onFeed = (ev) => {
|
|
3377
|
+
const row = ev.target?.closest?.(".log");
|
|
3378
|
+
if (!row) return;
|
|
3379
|
+
if (row.nextElementSibling?.classList.contains("logdetail")) {
|
|
3380
|
+
row.nextElementSibling?.remove();
|
|
3381
|
+
row.setAttribute("aria-expanded", "false");
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const uid = Number(row.getAttribute("data-uid"));
|
|
3385
|
+
const entry = this.model.view.feed.find((l) => l.uid === uid);
|
|
3386
|
+
if (!entry) return;
|
|
3387
|
+
feed.querySelectorAll(".logdetail").forEach((n) => n.remove());
|
|
3388
|
+
feed.querySelectorAll(".log[aria-expanded='true']").forEach((n) => n.setAttribute("aria-expanded", "false"));
|
|
3389
|
+
row.insertAdjacentHTML("afterend", this.feedDetail(entry));
|
|
3390
|
+
row.setAttribute("aria-expanded", "true");
|
|
3391
|
+
};
|
|
3392
|
+
const onFeedKey = (ev) => {
|
|
3393
|
+
const k = ev.key;
|
|
3394
|
+
if (k !== "Enter" && k !== " ") return;
|
|
3395
|
+
const row = ev.target?.closest?.(".log");
|
|
3396
|
+
if (!row) return;
|
|
3397
|
+
ev.preventDefault();
|
|
3398
|
+
row.click();
|
|
3399
|
+
};
|
|
3400
|
+
feed.addEventListener("click", onFeed);
|
|
3401
|
+
feed.addEventListener("keydown", onFeedKey);
|
|
3402
|
+
this.disposers.push(() => {
|
|
3403
|
+
feed.removeEventListener("click", onFeed);
|
|
3404
|
+
feed.removeEventListener("keydown", onFeedKey);
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
this.wireBtn("btnPurge", () => {
|
|
3408
|
+
lsPurge();
|
|
3409
|
+
this.flashBtn("btnPurge", "état effacé ✓");
|
|
3410
|
+
});
|
|
3411
|
+
const onMin = () => this.setMinimized(false);
|
|
3412
|
+
minbar.addEventListener("click", onMin);
|
|
3413
|
+
this.disposers.push(() => minbar.removeEventListener("click", onMin));
|
|
3414
|
+
let resizePending = false;
|
|
3415
|
+
const onWinResize = () => {
|
|
3416
|
+
if (resizePending) return;
|
|
3417
|
+
resizePending = true;
|
|
3418
|
+
requestAnimationFrame(() => {
|
|
3419
|
+
resizePending = false;
|
|
3420
|
+
this.setPanelH(this.panelH);
|
|
3421
|
+
});
|
|
3422
|
+
};
|
|
3423
|
+
window.addEventListener("resize", onWinResize);
|
|
3424
|
+
this.disposers.push(() => window.removeEventListener("resize", onWinResize));
|
|
3425
|
+
}
|
|
3426
|
+
startResize(ev) {
|
|
3427
|
+
ev.preventDefault();
|
|
3428
|
+
const startY = ev.clientY;
|
|
3429
|
+
const startH = this.panelH;
|
|
3430
|
+
const onMove = (e) => {
|
|
3431
|
+
const delta = this.position === "bottom" ? startY - e.clientY : e.clientY - startY;
|
|
3432
|
+
this.setPanelH(startH + delta);
|
|
3433
|
+
};
|
|
3434
|
+
const onUp = () => {
|
|
3435
|
+
document.removeEventListener("pointermove", onMove);
|
|
3436
|
+
document.removeEventListener("pointerup", onUp);
|
|
3437
|
+
lsSet(LS.h, String(this.panelH));
|
|
3438
|
+
};
|
|
3439
|
+
document.addEventListener("pointermove", onMove);
|
|
3440
|
+
document.addEventListener("pointerup", onUp);
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Retour visuel bref sur un bouton d'action.
|
|
3444
|
+
*
|
|
3445
|
+
* Une copie réussie ne change rien à l'écran : sans ce clin d'œil, on ne sait
|
|
3446
|
+
* pas si le clic a porté, et on reclique.
|
|
3447
|
+
*/
|
|
3448
|
+
flashBtn(key, glyph) {
|
|
3449
|
+
const b = this.el[key];
|
|
3450
|
+
if (!b) return;
|
|
3451
|
+
const before = b.textContent ?? "";
|
|
3452
|
+
b.textContent = glyph;
|
|
3453
|
+
const t = setTimeout(() => {
|
|
3454
|
+
b.textContent = before;
|
|
3455
|
+
}, 900);
|
|
3456
|
+
this.disposers.push(() => clearTimeout(t));
|
|
3457
|
+
}
|
|
3458
|
+
wireBtn(key, handler) {
|
|
3459
|
+
const node = this.el[key];
|
|
3460
|
+
if (!node) return;
|
|
3461
|
+
node.addEventListener("click", handler);
|
|
3462
|
+
this.disposers.push(() => node.removeEventListener("click", handler));
|
|
3463
|
+
}
|
|
3464
|
+
template() {
|
|
3465
|
+
return `
|
|
3466
|
+
<div class="strip">
|
|
3467
|
+
<span class="dot" data-el="dot" data-tip="État de la connexion temps réel"></span>
|
|
3468
|
+
<span class="brand" data-tip="Barre de débogage Nodefony — visible en développement uniquement"><span class="logo">◆</span><span class="name">nodefony</span></span>
|
|
3469
|
+
<span class="env-badge" data-el="envBadge" data-tip="Environnement dans lequel tourne l'application">env</span>
|
|
3470
|
+
<span class="branch" data-el="branch" data-tip="Branche git de la copie de travail"><span class="git">⎇</span><span data-el="branchName">—</span></span>
|
|
3471
|
+
<span class="rt-pill" data-el="rtPill" data-tip="Transport temps réel et état de la socket"><span class="bolt">⚡</span> realtime</span>
|
|
3472
|
+
${this.miniMetric("rt", "rt", "rtMini", "0/s", "realtime", "Messages temps réel reçus par seconde — cliquer pour ouvrir l'onglet Realtime")}
|
|
3473
|
+
${this.miniMetric("cpu", "cpu", "cpuMini", "0%", "perf", "Charge processeur du serveur — cliquer pour ouvrir l'onglet Perf")}
|
|
3474
|
+
${this.miniMetric("mem", "mem", "memMini", "0%", "perf", "Mémoire utilisée par le serveur — cliquer pour ouvrir l'onglet Perf")}
|
|
3475
|
+
<span class="spacer"></span>
|
|
3476
|
+
${this.networkEnabled ? `<button type="button" class="chip goto" data-goto="network" data-tip="Requêtes réseau observées — cliquer pour ouvrir l'onglet Network"><span class="k">net</span><span class="blue" data-el="netChip">0</span></button>` : ""}
|
|
3477
|
+
<button type="button" class="chip goto" data-goto="logs" data-tip="Entrées de journal reçues — cliquer pour ouvrir l'onglet Logs"><span class="k">logs</span><span data-el="logs">0</span></button>
|
|
3478
|
+
<button type="button" class="chip goto" data-goto="logs" data-tip="Erreurs et alertes — cliquer pour ouvrir l'onglet Logs"><span class="k">err</span><span class="crit" data-el="err">0</span></button>
|
|
3479
|
+
<button type="button" class="ctrl live" data-el="btnLive" aria-pressed="false" data-tip="Recevoir les mesures et les journaux en direct. Coupé par défaut : le flux maintient des compteurs côté serveur.">flux OFF</button>
|
|
3480
|
+
<button type="button" class="ctrl" data-el="btnSide" aria-label="Changer la barre de côté" data-tip="Changer la barre de côté">⇄</button>
|
|
3481
|
+
<button type="button" class="ctrl" data-el="btnMin" aria-label="Réduire la barre en pastille" data-tip="Réduire en pastille">—</button>
|
|
3482
|
+
<button type="button" class="toggle" data-el="btnToggle" aria-expanded="${this.startOpen}" aria-controls="nf-db-panel" aria-label="${this.startOpen ? "Replier" : "Déplier"} le panneau" data-tip="${this.startOpen ? "Replier" : "Déplier"} le panneau">▴</button>
|
|
3483
|
+
</div>
|
|
3484
|
+
<div class="panelwrap" id="nf-db-panel">
|
|
3485
|
+
<div class="resize" data-el="resize" title="Redimensionner"></div>
|
|
3486
|
+
<div class="tabs">
|
|
3487
|
+
<button class="tab" data-tab="realtime">Realtime</button>
|
|
3488
|
+
${this.networkEnabled ? `<button class="tab" data-tab="network">Network <span class="tcount" data-el="netTab">0</span></button>` : ""}
|
|
3489
|
+
<button class="tab" data-tab="perf">Perf</button>
|
|
3490
|
+
<button class="tab" data-tab="logs">Logs <span class="tcount" data-el="logsTab">0</span></button>
|
|
3491
|
+
<button class="tab" data-tab="runtime">Runtime</button>
|
|
3492
|
+
</div>
|
|
3493
|
+
<div class="panes" data-el="panes">
|
|
3494
|
+
${this.realtimePane()}
|
|
3495
|
+
${this.networkEnabled ? this.networkPane() : ""}
|
|
3496
|
+
${this.perfPane()}
|
|
3497
|
+
${this.logsPane()}
|
|
3498
|
+
${this.runtimePane()}
|
|
3499
|
+
</div>
|
|
3500
|
+
</div>`;
|
|
3501
|
+
}
|
|
3502
|
+
realtimePane() {
|
|
3503
|
+
return `<div class="pane" data-pane="realtime"><div class="cards">
|
|
3504
|
+
<div class="card hero">
|
|
3505
|
+
<h4><span class="blue">⚡</span> Realtime</h4>
|
|
3506
|
+
<div class="big"><span data-el="rtBig">0</span><small>msg/s</small></div>
|
|
3507
|
+
<svg viewBox="0 0 ${CHART_W} 38" preserveAspectRatio="none">
|
|
3508
|
+
<polygon class="area rt" data-el="rtArea" points=""/>
|
|
3509
|
+
<polyline class="spark rt" data-el="rtLine" points=""/>
|
|
3510
|
+
</svg>
|
|
3511
|
+
${this.kv("transport", "rtTransport")}
|
|
3512
|
+
${this.kv("protocole", "rtProto")}
|
|
3513
|
+
${this.kv("état", "rtState")}
|
|
3514
|
+
${this.kv("frames reçues", "rtFrames")}
|
|
3515
|
+
${this.kv("pic", "rtPeak")}
|
|
3516
|
+
<div class="tag"><b>HTTP & WebSocket, même contexte.</b> Push natif, <b>0 polling</b> — chaque chiffre arrive en temps réel par le Core isomorphe.</div>
|
|
3517
|
+
</div>
|
|
3518
|
+
${this.frontendCard()}
|
|
3519
|
+
</div></div>`;
|
|
3520
|
+
}
|
|
3521
|
+
networkPane() {
|
|
3522
|
+
return `<div class="pane np" data-pane="network">
|
|
3523
|
+
<div class="net-head">
|
|
3524
|
+
<span class="chip"><span class="k">total</span><span data-el="netTotal">0</span></span>
|
|
3525
|
+
<span class="chip"><span class="k">err</span><span class="crit" data-el="netErr">0</span></span>
|
|
3526
|
+
<span class="chip"><span class="k">pending</span><span class="muted" data-el="netPend">0</span></span>
|
|
3527
|
+
<span class="net-clear" data-el="netClear" title="vider la liste">vider</span>
|
|
3528
|
+
</div>
|
|
3529
|
+
<div class="net-list" data-el="netList"><div class="empty">en attente d'appels AJAX (fetch / XHR)…</div></div>
|
|
3530
|
+
<div class="net-detail" data-el="netDetail"><div class="empty">clique un appel → profil serveur (waterfall des phases, route, user).</div></div>
|
|
3531
|
+
</div>`;
|
|
3532
|
+
}
|
|
3533
|
+
perfPane() {
|
|
3534
|
+
return `<div class="pane" data-pane="perf"><div class="cards">
|
|
3535
|
+
<div class="card">
|
|
3536
|
+
<h4>Performance</h4>
|
|
3537
|
+
${this.chart("CPU", "cpuVal", "cpuPeak", "cpuLine", "cpuArea")}
|
|
3538
|
+
${this.chart("Heap", "heapVal", "heapPeak", "heapLine", "heapArea")}
|
|
3539
|
+
${this.chart("Event loop", "loopVal", "loopPeak", "loopLine", "loopArea")}
|
|
3540
|
+
</div>
|
|
3541
|
+
</div></div>`;
|
|
3542
|
+
}
|
|
3543
|
+
/**
|
|
3544
|
+
* L'onglet Logs — une petite console de lecture, pas une liste qui défile.
|
|
3545
|
+
*
|
|
3546
|
+
* Ce qu'elle porte, et pourquoi : un FILTRE par sévérité et une RECHERCHE
|
|
3547
|
+
* (sans quoi on lit le bruit au lieu de son incident) ; une PAUSE (sans quoi
|
|
3548
|
+
* la ligne qu'on essaie de lire s'en va) ; une COPIE (une trace se colle dans
|
|
3549
|
+
* un ticket) ; et le DÉTAIL d'une entrée, où vit le `requestId` — le champ qui
|
|
3550
|
+
* relie ce journal à sa requête, et que la barre recevait sans jamais le
|
|
3551
|
+
* montrer.
|
|
3552
|
+
*/
|
|
3553
|
+
logsPane() {
|
|
3554
|
+
return `<div class="pane" data-pane="logs">
|
|
3555
|
+
<div class="counts">
|
|
3556
|
+
<button type="button" class="chip sevf" data-sev="all" data-tip="Tout afficher"><span class="k">total</span><span data-el="cTotal">0</span></button>
|
|
3557
|
+
<button type="button" class="chip sevf" data-sev="err" data-tip="N'afficher que les erreurs (sévérité 0 à 3)"><span class="k">err</span><span class="crit" data-el="cErr">0</span></button>
|
|
3558
|
+
<button type="button" class="chip sevf" data-sev="warn" data-tip="N'afficher que les alertes (sévérité 4)"><span class="k">warn</span><span class="warn" data-el="cWarn">0</span></button>
|
|
3559
|
+
<span class="chip" data-tip="Entrées écartées par le serveur faute de place dans le tampon — le flux était plus rapide que l'envoi"><span class="k">dropped</span><span class="muted" data-el="cDrop">0</span></span>
|
|
3560
|
+
<span class="spacer"></span>
|
|
3561
|
+
<input class="search" data-el="logSearch" type="search" placeholder="rechercher…"
|
|
3562
|
+
aria-label="Filtrer les journaux sur leur texte, leur module ou leur requête" />
|
|
3563
|
+
<button type="button" class="toolbtn" data-el="btnPause" aria-pressed="false" data-tip="Suspendre l'affichage — les entrées continuent d'arriver et se rattrapent à la reprise">⏸</button>
|
|
3564
|
+
<button type="button" class="toolbtn" data-el="btnCopy" data-tip="Copier les entrées affichées dans le presse-papiers">⧉</button>
|
|
3565
|
+
<button type="button" class="toolbtn" data-el="btnClearFeed" data-tip="Vider la liste affichée (n'efface rien côté serveur)">⌫</button>
|
|
3566
|
+
</div>
|
|
3567
|
+
<div class="feed" data-el="feed" role="log" aria-label="Journaux du serveur"><div class="empty">en attente de logs…</div></div>
|
|
3568
|
+
</div>`;
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Ce que la barre garde sur CE navigateur — écrit noir sur blanc.
|
|
3572
|
+
*
|
|
3573
|
+
* Une console de développement qui écrit dans le stockage local sans le dire
|
|
3574
|
+
* est exactement ce qu'on reproche aux autres. La liste est dérivée des clés
|
|
3575
|
+
* réelles, donc elle ne peut pas mentir par oubli, et le bouton efface
|
|
3576
|
+
* précisément celles-là — jamais le stockage de l'application hôte.
|
|
3577
|
+
*/
|
|
3578
|
+
storageCard() {
|
|
3579
|
+
return `<div class="card">
|
|
3580
|
+
<h4>Stockage local</h4>
|
|
3581
|
+
<div class="tag">Ces valeurs vivent dans ce navigateur, sur cet appareil. Elles ne quittent jamais la page et ne concernent que l'apparence de la barre.</div>
|
|
3582
|
+
${lsKeys().map((k) => `<div class="drow"><span class="dk">${escapeHtml(k.replace("nf.debugbar.", ""))}</span><span class="dv">${escapeHtml(lsGet(k, "—"))}</span></div>`).join("")}
|
|
3583
|
+
<div style="margin-top:8px"><button type="button" class="toolbtn" data-el="btnPurge" data-tip="Effacer les préférences de la barre sur ce navigateur — l'application n'est pas touchée">Purger l'état de la barre</button></div>
|
|
3584
|
+
</div>`;
|
|
3585
|
+
}
|
|
3586
|
+
runtimePane() {
|
|
3587
|
+
return `<div class="pane" data-pane="runtime"><div class="cards">
|
|
3588
|
+
${this.storageCard()}
|
|
3589
|
+
<div class="card">
|
|
3590
|
+
<h4>Runtime</h4>
|
|
3591
|
+
${this.kv("app", "appName")}
|
|
3592
|
+
${this.kv("version", "appVersion")}
|
|
3593
|
+
${this.kv("environnement", "envRow")}
|
|
3594
|
+
${this.kv("branche", "branchRow")}
|
|
3595
|
+
${this.kv("pid", "pid")}
|
|
3596
|
+
${this.kv("uptime", "uptime")}
|
|
3597
|
+
${this.kv("instance", "instance")}
|
|
3598
|
+
${this.kv("cpu cores", "cores")}
|
|
3599
|
+
${this.kv("loadavg", "load")}
|
|
3600
|
+
${this.kv("rss", "rss")}
|
|
3601
|
+
${this.kv("heap used", "heapUsed")}
|
|
3602
|
+
${this.kv("heap total", "heapTotal")}
|
|
3603
|
+
${this.kv("heap limit", "heapLimit")}
|
|
3604
|
+
${this.kv("external", "external")}
|
|
3605
|
+
</div>
|
|
3606
|
+
</div></div>`;
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Un indicateur du bandeau — et un RACCOURCI vers l'onglet qui le détaille.
|
|
3610
|
+
*
|
|
3611
|
+
* C'est un `<button>` et non un `<span>` : le bandeau ne se replie plus au
|
|
3612
|
+
* clic (c'était le défaut principal — lire une valeur refermait le panneau),
|
|
3613
|
+
* donc chaque indicateur peut faire quelque chose d'utile de son propre clic.
|
|
3614
|
+
* Mener à l'onglet qui explique le chiffre est ce que l'utilisateur attend,
|
|
3615
|
+
* et un bouton l'annonce et s'atteint au clavier.
|
|
3616
|
+
*/
|
|
3617
|
+
miniMetric(label, valKey, sparkKey, init, goto, tip) {
|
|
3618
|
+
return `<button type="button" class="metric goto" data-goto="${goto}" data-tip="${escapeHtml(tip)}">
|
|
3619
|
+
<span class="k">${label}</span>
|
|
3620
|
+
<span class="v" data-el="${valKey}">${init}</span>
|
|
3621
|
+
<svg class="mini" viewBox="0 0 ${MINI_W} ${MINI_H}" preserveAspectRatio="none">
|
|
3622
|
+
<polyline class="spark ok" data-el="${sparkKey}" points=""/>
|
|
3623
|
+
</svg>
|
|
3624
|
+
</button>`;
|
|
3625
|
+
}
|
|
3626
|
+
chart(label, valKey, peakKey, lineKey, areaKey) {
|
|
3627
|
+
return `<div class="chart">
|
|
3628
|
+
<div class="hd">
|
|
3629
|
+
<span class="lbl">${label}</span>
|
|
3630
|
+
<span><span class="val" data-el="${valKey}">—</span> <span class="peak" data-el="${peakKey}"></span></span>
|
|
3631
|
+
</div>
|
|
3632
|
+
<svg viewBox="0 0 ${CHART_W} ${CHART_H}" preserveAspectRatio="none">
|
|
3633
|
+
<polygon class="area ok" data-el="${areaKey}" points=""/>
|
|
3634
|
+
<polyline class="spark ok" data-el="${lineKey}" points=""/>
|
|
3635
|
+
</svg>
|
|
3636
|
+
</div>`;
|
|
3637
|
+
}
|
|
3638
|
+
kv(label, key) {
|
|
3639
|
+
return `<div class="kv"><span class="k">${label}</span><span class="v" data-el="${key}">—</span></div>`;
|
|
3640
|
+
}
|
|
3641
|
+
/** Carte Frontend / HMR — affichée seulement si un contexte frontend est fourni. */
|
|
3642
|
+
frontendCard() {
|
|
3643
|
+
if (!this.frontend) return "";
|
|
3644
|
+
const fw = FRAMEWORKS[this.frontend.framework ?? ""] ?? {
|
|
3645
|
+
label: this.frontend.framework ?? "Frontend",
|
|
3646
|
+
color: "#3aa0ff"
|
|
3647
|
+
};
|
|
3648
|
+
return `<div class="card fe">
|
|
3649
|
+
<h4><span style="color:${fw.color}">●</span> Frontend</h4>
|
|
3650
|
+
<div class="fw">
|
|
3651
|
+
<span class="badge" style="background:${fw.color}">${escapeHtml(fw.label)}</span>
|
|
3652
|
+
<span class="name">${escapeHtml(this.frontend.name ?? "")}</span>
|
|
3653
|
+
</div>
|
|
3654
|
+
<div class="hd"><span class="lbl">HMR hot updates</span><span class="hmr-big" data-el="hmrBig">0</span></div>
|
|
3655
|
+
<svg viewBox="0 0 ${CHART_W} 34" preserveAspectRatio="none">
|
|
3656
|
+
<polygon class="area fe" data-el="hmrArea" points=""/>
|
|
3657
|
+
<polyline class="spark fe" data-el="hmrLine" points=""/>
|
|
3658
|
+
</svg>
|
|
3659
|
+
${this.kv("bundler", "feVite")}
|
|
3660
|
+
${this.kv("dernier module", "hmrLast")}
|
|
3661
|
+
<div class="tag"><b>Backend & frontend, un seul process.</b> HMR Vite branché au runtime — sauvegarde un composant, la barre pulse.</div>
|
|
3662
|
+
</div>`;
|
|
3663
|
+
}
|
|
3664
|
+
wireRealtime() {
|
|
3665
|
+
const offState = this.client.onState((state) => {
|
|
3666
|
+
this.model.setState(state);
|
|
3667
|
+
this.scheduleRender();
|
|
3668
|
+
});
|
|
3669
|
+
const offTick = this.client.onStats(() => {
|
|
3670
|
+
if (!this.live) return;
|
|
3671
|
+
this.sampleThroughput();
|
|
3672
|
+
this.scheduleRender();
|
|
3673
|
+
});
|
|
3674
|
+
const offStats = this.client.on(CHANNELS.stats, (...a) => {
|
|
3675
|
+
const p = a[0];
|
|
3676
|
+
if (p && typeof p === "object") this.model.ingestStats(p);
|
|
3677
|
+
this.scheduleRender();
|
|
3678
|
+
});
|
|
3679
|
+
const offSyslog = this.client.on(CHANNELS.syslog, (...a) => {
|
|
3680
|
+
const p = a[0];
|
|
3681
|
+
if (p && typeof p === "object") this.model.ingestSyslog(p);
|
|
3682
|
+
this.scheduleRender();
|
|
3683
|
+
});
|
|
3684
|
+
this.model.setState(this.client.state);
|
|
3685
|
+
this.disposers.push(offState, offTick, offStats, offSyslog, () => {
|
|
3686
|
+
this.stopLive();
|
|
3687
|
+
});
|
|
3688
|
+
if (this.live) this.startLive();
|
|
3689
|
+
}
|
|
3690
|
+
/**
|
|
3691
|
+
* Démarre les abonnements realtime (tickers serveur stats+syslog). REF-COMPTÉ
|
|
3692
|
+
* (UNE fois) sur le client PARTAGÉ : un canal reste actif tant qu'un
|
|
3693
|
+
* consommateur le veut ; le client ré-abonne seul au reconnect. Idempotent.
|
|
3694
|
+
*/
|
|
3695
|
+
startLive() {
|
|
3696
|
+
if (this.liveOff) return;
|
|
3697
|
+
this.client.subscribe(CHANNELS.stats);
|
|
3698
|
+
this.client.subscribe(CHANNELS.syslog);
|
|
3699
|
+
this.liveOff = () => {
|
|
3700
|
+
this.client.unsubscribe(CHANNELS.stats);
|
|
3701
|
+
this.client.unsubscribe(CHANNELS.syslog);
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
/** Arrête les abonnements realtime (relâche la réf de la barre). Idempotent. */
|
|
3705
|
+
stopLive() {
|
|
3706
|
+
if (!this.liveOff) return;
|
|
3707
|
+
this.liveOff();
|
|
3708
|
+
this.liveOff = null;
|
|
3709
|
+
}
|
|
3710
|
+
/** Active/désactive le temps réel (bouton de la barre) + persiste la préférence. */
|
|
3711
|
+
setLive(v) {
|
|
3712
|
+
if (v === this.live && !!this.liveOff === v) return;
|
|
3713
|
+
this.live = v;
|
|
3714
|
+
lsSet(LS.live, v ? "1" : "0");
|
|
3715
|
+
if (v) {
|
|
3716
|
+
this.prevFrames = this.client.framesReceived;
|
|
3717
|
+
this.startLive();
|
|
3718
|
+
} else {
|
|
3719
|
+
this.stopLive();
|
|
3720
|
+
this.rtRate = 0;
|
|
3721
|
+
pushCap(this.rtSeries, 0, RT_POINTS);
|
|
3722
|
+
}
|
|
3723
|
+
this.updateLiveBtn();
|
|
3724
|
+
this.scheduleRender();
|
|
3725
|
+
}
|
|
3726
|
+
/** Met à jour l'aspect du bouton « Temps réel » selon l'état. */
|
|
3727
|
+
updateLiveBtn() {
|
|
3728
|
+
const btn = this.el["btnLive"];
|
|
3729
|
+
if (!btn) return;
|
|
3730
|
+
btn.textContent = this.live ? "flux ON" : "flux OFF";
|
|
3731
|
+
btn.title = this.live ? "Flux push ACTIF (stats + logs) — clic pour couper. ≠ état de la connexion (pastille « realtime »)." : "Flux push coupé (perf) — clic pour activer (stats + logs). ≠ état de la connexion (pastille « realtime »).";
|
|
3732
|
+
btn.setAttribute("aria-pressed", this.live ? "true" : "false");
|
|
3733
|
+
btn.classList.toggle("on", this.live);
|
|
3734
|
+
}
|
|
3735
|
+
sampleThroughput() {
|
|
3736
|
+
const total = this.client.framesReceived;
|
|
3737
|
+
this.rtRate = Math.max(0, total - this.prevFrames);
|
|
3738
|
+
this.prevFrames = total;
|
|
3739
|
+
if (this.rtRate > this.rtPeak) this.rtPeak = this.rtRate;
|
|
3740
|
+
pushCap(this.rtSeries, this.rtRate, RT_POINTS);
|
|
3741
|
+
this.hmrRate = Math.max(0, this.hmrCount - this.hmrPrev);
|
|
3742
|
+
this.hmrPrev = this.hmrCount;
|
|
3743
|
+
pushCap(this.hmrSeries, this.hmrRate, RT_POINTS);
|
|
3744
|
+
}
|
|
3745
|
+
wireHmr() {
|
|
3746
|
+
if (!this.frontend) return;
|
|
3747
|
+
const dispose = observeViteHmr((e) => this.onHmr(e));
|
|
3748
|
+
this.disposers.push(dispose);
|
|
3749
|
+
}
|
|
3750
|
+
onHmr(e) {
|
|
3751
|
+
if (e.kind === "connected") this.viteConnected = true;
|
|
3752
|
+
else if (e.kind === "update") {
|
|
3753
|
+
this.hmrCount++;
|
|
3754
|
+
this.hmrLast = e.path ?? "(module)";
|
|
3755
|
+
this.flashHmr();
|
|
3756
|
+
} else if (e.kind === "full-reload") {
|
|
3757
|
+
this.hmrCount++;
|
|
3758
|
+
this.hmrLast = e.path ?? "full reload";
|
|
3759
|
+
this.flashHmr();
|
|
3760
|
+
}
|
|
3761
|
+
this.scheduleRender();
|
|
3762
|
+
}
|
|
3763
|
+
flashHmr() {
|
|
3764
|
+
const node = this.el.hmrBig;
|
|
3765
|
+
if (!node) return;
|
|
3766
|
+
node.setAttribute("class", "hmr-big hmr-flash");
|
|
3767
|
+
if (this.flashTimer) clearTimeout(this.flashTimer);
|
|
3768
|
+
this.flashTimer = setTimeout(() => {
|
|
3769
|
+
node.setAttribute("class", "hmr-big");
|
|
3770
|
+
}, 600);
|
|
3771
|
+
}
|
|
3772
|
+
wireNetwork() {
|
|
3773
|
+
if (!this.networkEnabled) return;
|
|
3774
|
+
const viteOrigin = this.frontend?.viteOrigin ?? "";
|
|
3775
|
+
const uninstall = installNetworkInterceptor({
|
|
3776
|
+
onChange: (e) => this.onNetEntry(e),
|
|
3777
|
+
ignore: (url) => url.includes(this.profilerBase) || url.includes("/@vite/") || url.includes("/@fs/") || url.includes("/node_modules/.vite/") || viteOrigin !== "" && url.includes(viteOrigin)
|
|
3778
|
+
});
|
|
3779
|
+
this.disposers.push(uninstall);
|
|
3780
|
+
const list = this.el.netList;
|
|
3781
|
+
if (list) {
|
|
3782
|
+
const onClick = (ev) => {
|
|
3783
|
+
const row = ev.target?.closest?.(".net-row");
|
|
3784
|
+
if (!row) return;
|
|
3785
|
+
const rid = row.dataset.rid;
|
|
3786
|
+
this.selectRow(row, rid || null);
|
|
3787
|
+
};
|
|
3788
|
+
list.addEventListener("click", onClick);
|
|
3789
|
+
this.disposers.push(() => list.removeEventListener("click", onClick));
|
|
3790
|
+
}
|
|
3791
|
+
const detail = this.el.netDetail;
|
|
3792
|
+
if (detail) {
|
|
3793
|
+
const onDetail = (ev) => {
|
|
3794
|
+
if (ev.target?.closest?.("[data-act=\"close\"]")) this.deselect();
|
|
3795
|
+
};
|
|
3796
|
+
detail.addEventListener("click", onDetail);
|
|
3797
|
+
this.disposers.push(() => detail.removeEventListener("click", onDetail));
|
|
3798
|
+
}
|
|
3799
|
+
const clear = this.el.netClear;
|
|
3800
|
+
if (clear) {
|
|
3801
|
+
const onClear = (e) => {
|
|
3802
|
+
e.stopPropagation();
|
|
3803
|
+
this.net.clear();
|
|
3804
|
+
this.netRows.clear();
|
|
3805
|
+
this.selectedRid = null;
|
|
3806
|
+
this.selRowId = null;
|
|
3807
|
+
const node = this.el.netList;
|
|
3808
|
+
if (node) node.innerHTML = `<div class="empty">en attente d'appels AJAX (fetch / XHR)…</div>`;
|
|
3809
|
+
this.detailVersion++;
|
|
3810
|
+
this.scheduleRender();
|
|
3811
|
+
};
|
|
3812
|
+
clear.addEventListener("click", onClear);
|
|
3813
|
+
this.disposers.push(() => clear.removeEventListener("click", onClear));
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
onNetEntry(e) {
|
|
3817
|
+
this.net.ingest(e);
|
|
3818
|
+
this.upsertNetRow(e);
|
|
3819
|
+
if (e.requestId && e.requestId === this.selectedRid) this.detailVersion++;
|
|
3820
|
+
this.scheduleRender();
|
|
3821
|
+
}
|
|
3822
|
+
/** Crée ou met à jour le nœud de ligne d'un appel (jamais d'innerHTML global). */
|
|
3823
|
+
upsertNetRow(e) {
|
|
3824
|
+
const list = this.el.netList;
|
|
3825
|
+
if (!list) return;
|
|
3826
|
+
let row = this.netRows.get(e.id);
|
|
3827
|
+
if (!row) {
|
|
3828
|
+
if (this.netRows.size === 0) list.textContent = "";
|
|
3829
|
+
row = document.createElement("div");
|
|
3830
|
+
row.className = "net-row";
|
|
3831
|
+
row.innerHTML = "<span class=\"net-method\"></span><span class=\"net-path\"></span><span class=\"net-rid\"></span><span class=\"net-status\"></span><span class=\"net-dur\"></span>";
|
|
3832
|
+
this.netRows.set(e.id, row);
|
|
3833
|
+
list.insertBefore(row, list.firstChild);
|
|
3834
|
+
while (this.netRows.size > 80 && list.lastElementChild) {
|
|
3835
|
+
const last = list.lastElementChild;
|
|
3836
|
+
for (const [id, n] of this.netRows) if (n === last) {
|
|
3837
|
+
this.netRows.delete(id);
|
|
3838
|
+
break;
|
|
3839
|
+
}
|
|
3840
|
+
last.remove();
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
this.fillNetRow(row, e);
|
|
3844
|
+
}
|
|
3845
|
+
/** Remplit/actualise les cellules d'une ligne (textContent → 0 échappement). */
|
|
3846
|
+
fillNetRow(row, e) {
|
|
3847
|
+
row.dataset.rid = e.requestId ?? "";
|
|
3848
|
+
row.className = `net-row${e.requestId && e.requestId === this.selectedRid ? " sel" : ""}${isError(e) ? " err" : ""}`;
|
|
3849
|
+
const [m, path, rid, status, dur] = row.children;
|
|
3850
|
+
m.className = `net-method ${methodClass(e.method)}`;
|
|
3851
|
+
m.textContent = e.method;
|
|
3852
|
+
path.textContent = e.path;
|
|
3853
|
+
path.title = e.url;
|
|
3854
|
+
rid.textContent = e.requestId ? shortId(e.requestId) : "";
|
|
3855
|
+
if (e.pending) {
|
|
3856
|
+
status.className = "net-status sp";
|
|
3857
|
+
status.textContent = "···";
|
|
3858
|
+
} else if (e.error) {
|
|
3859
|
+
status.className = "net-status s5";
|
|
3860
|
+
status.textContent = "ERR";
|
|
3861
|
+
} else {
|
|
3862
|
+
status.className = `net-status s${statusFamily(e.status)}`;
|
|
3863
|
+
status.textContent = e.status === null ? "—" : String(e.status);
|
|
3864
|
+
}
|
|
3865
|
+
dur.textContent = e.durationMs === null ? "" : `${e.durationMs}ms`;
|
|
3866
|
+
}
|
|
3867
|
+
/** Sélectionne une ligne (highlight) + déclenche le fetch du profil. */
|
|
3868
|
+
selectRow(row, rid) {
|
|
3869
|
+
if (this.selRowId !== null) this.netRows.get(this.selRowId)?.classList.remove("sel");
|
|
3870
|
+
row.classList.add("sel");
|
|
3871
|
+
const idEntry = [...this.netRows.entries()].find(([, n]) => n === row);
|
|
3872
|
+
this.selRowId = idEntry ? idEntry[0] : null;
|
|
3873
|
+
this.selectedRid = rid;
|
|
3874
|
+
this.selNoRid = !rid;
|
|
3875
|
+
this.detailVersion++;
|
|
3876
|
+
const minDetail = detailPanelH();
|
|
3877
|
+
if (rid && this.panelH < minDetail) this.setPanelH(minDetail);
|
|
3878
|
+
if (rid && !this.net.profileState(rid)) this.fetchProfile(rid);
|
|
3879
|
+
if (rid && typeof window !== "undefined") try {
|
|
3880
|
+
window.dispatchEvent(new CustomEvent(PLATFORM_EVENTS.debugbarSelect, { detail: { requestId: rid } }));
|
|
3881
|
+
} catch {}
|
|
3882
|
+
this.scheduleRender();
|
|
3883
|
+
}
|
|
3884
|
+
/** Fetch `/{profilerBase}/{requestId}` (ignoré par l'intercepteur). */
|
|
3885
|
+
fetchProfile(requestId) {
|
|
3886
|
+
if (typeof fetch === "undefined") return;
|
|
3887
|
+
this.net.setProfileState(requestId, { status: "loading" });
|
|
3888
|
+
fetch(`${this.profilerBase}/${encodeURIComponent(requestId)}`, { headers: { accept: "application/json" } }).then(async (res) => {
|
|
3889
|
+
if (res.status === 404) {
|
|
3890
|
+
this.net.setProfileState(requestId, { status: "missing" });
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
3894
|
+
if (!res.ok || !ct.includes("application/json")) {
|
|
3895
|
+
this.net.setProfileState(requestId, {
|
|
3896
|
+
status: "error",
|
|
3897
|
+
message: res.ok ? "réponse non-JSON (path profiler non proxifié par Vite ?)" : `HTTP ${res.status}`
|
|
3898
|
+
});
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
const profile = await res.json();
|
|
3902
|
+
this.net.setProfileState(requestId, {
|
|
3903
|
+
status: "ready",
|
|
3904
|
+
profile
|
|
3905
|
+
});
|
|
3906
|
+
}).catch((err) => {
|
|
3907
|
+
this.net.setProfileState(requestId, {
|
|
3908
|
+
status: "error",
|
|
3909
|
+
message: err instanceof Error ? err.message : "fetch failed"
|
|
3910
|
+
});
|
|
3911
|
+
}).finally(() => {
|
|
3912
|
+
if (requestId === this.selectedRid) this.detailVersion++;
|
|
3913
|
+
this.scheduleRender();
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
scheduleRender() {
|
|
3917
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
3918
|
+
this.render();
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (this.rafPending) return;
|
|
3922
|
+
this.rafPending = true;
|
|
3923
|
+
requestAnimationFrame(() => {
|
|
3924
|
+
this.rafPending = false;
|
|
3925
|
+
this.render();
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
3928
|
+
text(key, value) {
|
|
3929
|
+
const node = this.el[key];
|
|
3930
|
+
if (node) node.textContent = value;
|
|
3931
|
+
}
|
|
3932
|
+
/** className safe pour HTML *et* SVG (SVGElement.className n'est pas une string). */
|
|
3933
|
+
cls(key, value) {
|
|
3934
|
+
this.el[key]?.setAttribute("class", value);
|
|
3935
|
+
}
|
|
3936
|
+
setPoints(lineKey, areaKey, series, tier, height, max) {
|
|
3937
|
+
const pts = sparklinePoints(series, CHART_W, height, max);
|
|
3938
|
+
this.el[lineKey]?.setAttribute("points", pts);
|
|
3939
|
+
this.el[lineKey]?.setAttribute("class", `spark ${tier}`);
|
|
3940
|
+
if (areaKey) {
|
|
3941
|
+
const area = this.el[areaKey];
|
|
3942
|
+
if (area) {
|
|
3943
|
+
area.setAttribute("points", pts ? `${pts} ${CHART_W},${height} 0,${height}` : "");
|
|
3944
|
+
area.setAttribute("class", `area ${tier}`);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
render() {
|
|
3949
|
+
if (!this.bar) return;
|
|
3950
|
+
const v = this.model.view;
|
|
3951
|
+
this.renderStrip(v);
|
|
3952
|
+
if (!this.bar.classList.contains("open")) return;
|
|
3953
|
+
switch (this.activeTab) {
|
|
3954
|
+
case "realtime":
|
|
3955
|
+
this.renderRealtimePane(v);
|
|
3956
|
+
break;
|
|
3957
|
+
case "network":
|
|
3958
|
+
this.renderNetwork();
|
|
3959
|
+
break;
|
|
3960
|
+
case "perf":
|
|
3961
|
+
this.renderPerf(v);
|
|
3962
|
+
break;
|
|
3963
|
+
case "logs":
|
|
3964
|
+
this.renderLogsPane(v);
|
|
3965
|
+
break;
|
|
3966
|
+
case "runtime": this.renderRuntime(v);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
/** Bandeau toujours visible (chips + pouls) — léger. */
|
|
3970
|
+
renderStrip(v) {
|
|
3971
|
+
const connected = v.state === "connected";
|
|
3972
|
+
const cpuT = gauge(v.cpuPercent);
|
|
3973
|
+
const memT = gauge(v.heapPercent);
|
|
3974
|
+
this.text("envBadge", v.env || "env");
|
|
3975
|
+
this.cls("envBadge", `env-badge ${envClass(v.env)}`);
|
|
3976
|
+
this.text("branchName", v.branch || "—");
|
|
3977
|
+
this.el.branch?.setAttribute("title", v.branch ? `branche git : ${v.branch}` : "branche git");
|
|
3978
|
+
this.text("mEnv", v.env);
|
|
3979
|
+
this.cls("dot", `dot ${v.state}`);
|
|
3980
|
+
this.cls("mdot", `dot ${v.state}`);
|
|
3981
|
+
this.text("mrate", `${this.rtRate}/s`);
|
|
3982
|
+
this.cls("rtPill", connected ? "rt-pill connected" : "rt-pill");
|
|
3983
|
+
const connTitle = `Connexion temps réel : ${v.state}`;
|
|
3984
|
+
this.el.rtPill?.setAttribute("title", connTitle);
|
|
3985
|
+
this.el.dot?.setAttribute("title", connTitle);
|
|
3986
|
+
this.text("rt", `${this.rtRate}/s`);
|
|
3987
|
+
this.cls("rt", "v blue");
|
|
3988
|
+
this.el.rtMini?.setAttribute("points", sparklinePoints(this.rtSeries, MINI_W, MINI_H));
|
|
3989
|
+
this.el.rtMini?.setAttribute("class", "spark rt");
|
|
3990
|
+
this.text("cpu", `${v.cpuPercent}%`);
|
|
3991
|
+
this.cls("cpu", `v ${cpuT}`);
|
|
3992
|
+
this.text("mem", `${v.heapPercent}%`);
|
|
3993
|
+
this.cls("mem", `v ${memT}`);
|
|
3994
|
+
this.el.cpuMini?.setAttribute("points", sparklinePoints(v.cpuSeries, MINI_W, MINI_H, 100));
|
|
3995
|
+
this.el.cpuMini?.setAttribute("class", `spark ${cpuT}`);
|
|
3996
|
+
this.el.memMini?.setAttribute("points", sparklinePoints(v.heapSeries, MINI_W, MINI_H, 100));
|
|
3997
|
+
this.el.memMini?.setAttribute("class", `spark ${memT}`);
|
|
3998
|
+
if (this.networkEnabled) {
|
|
3999
|
+
this.text("netChip", String(this.net.total));
|
|
4000
|
+
this.cls("netChip", this.net.errors > 0 ? "crit" : "blue");
|
|
4001
|
+
this.text("netTab", String(this.net.total));
|
|
4002
|
+
this.cls("netTab", this.net.errors > 0 ? "tcount crit" : "tcount");
|
|
4003
|
+
}
|
|
4004
|
+
this.text("logs", String(v.logTotal));
|
|
4005
|
+
this.text("err", String(v.errorCount));
|
|
4006
|
+
this.text("logsTab", String(v.logTotal));
|
|
4007
|
+
this.cls("logsTab", v.errorCount > 0 ? "tcount crit" : "tcount");
|
|
4008
|
+
}
|
|
4009
|
+
renderRealtimePane(v) {
|
|
4010
|
+
const live = v.state === "connected";
|
|
4011
|
+
this.text("rtBig", String(this.rtRate));
|
|
4012
|
+
this.setPoints("rtLine", "rtArea", this.rtSeries, "rt", 38);
|
|
4013
|
+
this.text("rtTransport", "WebSocket");
|
|
4014
|
+
this.text("rtProto", "JSON-RPC 2.0");
|
|
4015
|
+
this.text("rtState", v.state);
|
|
4016
|
+
this.cls("rtState", `v ${live ? "ok" : "warn"}`);
|
|
4017
|
+
this.text("rtFrames", String(this.client.framesReceived));
|
|
4018
|
+
this.text("rtPeak", `${this.rtPeak}/s`);
|
|
4019
|
+
if (this.frontend) {
|
|
4020
|
+
this.text("hmrBig", String(this.hmrCount));
|
|
4021
|
+
this.setPoints("hmrLine", "hmrArea", this.hmrSeries, "fe", 34);
|
|
4022
|
+
this.text("feVite", this.viteConnected ? "Vite · connecté" : "Vite · …");
|
|
4023
|
+
this.cls("feVite", `v ${this.viteConnected ? "ok" : "warn"}`);
|
|
4024
|
+
this.text("hmrLast", this.hmrLast);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
renderPerf(v) {
|
|
4028
|
+
const cpuT = gauge(v.cpuPercent);
|
|
4029
|
+
const memT = gauge(v.heapPercent);
|
|
4030
|
+
const loopT = gauge(v.eventLoopMs, 50, 200);
|
|
4031
|
+
this.text("cpuVal", `${v.cpuPercent}%`);
|
|
4032
|
+
this.text("cpuPeak", v.cpuPeak ? `peak ${v.cpuPeak}%` : "");
|
|
4033
|
+
this.cls("cpuVal", `val ${cpuT}`);
|
|
4034
|
+
this.setPoints("cpuLine", "cpuArea", v.cpuSeries, cpuT, CHART_H, 100);
|
|
4035
|
+
this.text("heapVal", `${v.heapPercent}%`);
|
|
4036
|
+
this.text("heapPeak", v.heapPeak ? `peak ${v.heapPeak}%` : "");
|
|
4037
|
+
this.cls("heapVal", `val ${memT}`);
|
|
4038
|
+
this.setPoints("heapLine", "heapArea", v.heapSeries, memT, CHART_H, 100);
|
|
4039
|
+
this.text("loopVal", `${v.eventLoopMs}ms`);
|
|
4040
|
+
this.text("loopPeak", v.eventLoopPeak ? `peak ${v.eventLoopPeak}ms` : "");
|
|
4041
|
+
this.cls("loopVal", `val ${loopT}`);
|
|
4042
|
+
this.setPoints("loopLine", "loopArea", v.loopSeries, loopT, CHART_H);
|
|
4043
|
+
}
|
|
4044
|
+
renderRuntime(v) {
|
|
4045
|
+
this.text("appName", v.appName || "—");
|
|
4046
|
+
this.text("appVersion", v.appVersion || "—");
|
|
4047
|
+
this.text("envRow", v.debug ? `${v.env} · debug` : v.env || "—");
|
|
4048
|
+
this.text("branchRow", v.branch || "—");
|
|
4049
|
+
this.text("pid", String(v.pid));
|
|
4050
|
+
this.text("uptime", formatUptime(v.uptime));
|
|
4051
|
+
this.text("instance", v.instanceId);
|
|
4052
|
+
this.text("cores", String(v.cpuCount));
|
|
4053
|
+
this.text("load", v.loadavg.map((n) => n.toFixed(2)).join(" ") || "—");
|
|
4054
|
+
this.text("rss", formatBytes(v.rss));
|
|
4055
|
+
this.text("heapUsed", formatBytes(v.heapUsed));
|
|
4056
|
+
this.text("heapTotal", formatBytes(v.heapTotal));
|
|
4057
|
+
this.text("heapLimit", formatBytes(v.heapLimit));
|
|
4058
|
+
this.text("external", formatBytes(v.external));
|
|
4059
|
+
}
|
|
4060
|
+
renderLogsPane(v) {
|
|
4061
|
+
this.text("cTotal", String(v.logTotal));
|
|
4062
|
+
this.text("cErr", String(v.errorCount));
|
|
4063
|
+
this.text("cWarn", String(v.warnCount));
|
|
4064
|
+
this.text("cDrop", String(v.dropped));
|
|
4065
|
+
this.renderFeed(v.feed);
|
|
4066
|
+
}
|
|
4067
|
+
/** Classe de couleur d'une entrée, dérivée de sa sévérité RFC 5424. */
|
|
4068
|
+
static tierOf(severity) {
|
|
4069
|
+
if (severity <= 3) return "crit";
|
|
4070
|
+
if (severity === 4) return "warn";
|
|
4071
|
+
if (severity === 7) return "muted";
|
|
4072
|
+
return "info";
|
|
4073
|
+
}
|
|
4074
|
+
/** L'entrée passe-t-elle le filtre de sévérité et la recherche en cours ? */
|
|
4075
|
+
feedMatch(l) {
|
|
4076
|
+
if (this.feedSev === "err" && l.severity > 3) return false;
|
|
4077
|
+
if (this.feedSev === "warn" && l.severity !== 4) return false;
|
|
4078
|
+
const q = this.feedQuery;
|
|
4079
|
+
if (q === "") return true;
|
|
4080
|
+
return l.text.toLowerCase().includes(q) || l.module.toLowerCase().includes(q) || l.msgid.toLowerCase().includes(q) || (l.requestId ?? "").toLowerCase().includes(q);
|
|
4081
|
+
}
|
|
4082
|
+
/** Une ligne de journal — horodatage, sévérité, module, texte, requête. */
|
|
4083
|
+
feedRow(l) {
|
|
4084
|
+
const tier = DebugBar.tierOf(l.severity);
|
|
4085
|
+
const rid = l.requestId ? `<span class="rid" data-tip="requête ${escapeHtml(l.requestId)} — la même valeur relie ce journal à sa route, à ses requêtes de base et à sa réponse">${escapeHtml(l.requestId.slice(0, 8))}</span>` : "";
|
|
4086
|
+
return `<div class="log" data-uid="${l.uid}" tabindex="0" role="button" aria-expanded="false" aria-label="Entrée ${escapeHtml(l.name)} de ${escapeHtml(l.module)} — ouvrir le détail"><span class="ts">${fmtClock(l.ts)}</span><span class="sev ${tier}">${escapeHtml(l.name)}</span><span class="mod">${escapeHtml(l.module)}</span><span class="txt">${escapeHtml(l.text)}</span>${rid}</div>`;
|
|
4087
|
+
}
|
|
4088
|
+
/** Le détail d'une entrée — ce qu'on colle dans un ticket. */
|
|
4089
|
+
feedDetail(l) {
|
|
4090
|
+
const row = (k, v) => `<div class="drow"><span class="dk">${k}</span><span class="dv">${escapeHtml(v)}</span></div>`;
|
|
4091
|
+
return `<div class="logdetail">
|
|
4092
|
+
${row("horodatage", new Date(l.ts).toISOString())}
|
|
4093
|
+
${row("sévérité", `${l.name} (${l.severity})`)}
|
|
4094
|
+
${row("module", l.module)}
|
|
4095
|
+
${l.msgid ? row("catégorie", l.msgid) : ""}
|
|
4096
|
+
${l.pid ? row("worker (pid)", String(l.pid)) : ""}
|
|
4097
|
+
${row("séquence", String(l.uid))}
|
|
4098
|
+
${l.requestId ? row("requête", l.requestId) : ""}
|
|
4099
|
+
${row("message", l.text)}
|
|
4100
|
+
</div>`;
|
|
4101
|
+
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Rendu du journal — **incrémental**, et c'est le point.
|
|
4104
|
+
*
|
|
4105
|
+
* L'ancienne version réassignait `innerHTML` en entier à chaque arrivée. Deux
|
|
4106
|
+
* conséquences qu'on ne voit pas dans le code et qu'on subit à l'écran : la
|
|
4107
|
+
* SÉLECTION de texte s'efface — impossible de copier une ligne pendant que le
|
|
4108
|
+
* flux tourne — et la position de DÉFILEMENT saute. Ici, seules les entrées
|
|
4109
|
+
* nouvelles sont insérées en tête ; ce qui est affiché n'est jamais reconstruit.
|
|
4110
|
+
*
|
|
4111
|
+
* Un changement de filtre ou de recherche, lui, refait bien la liste : le jeu
|
|
4112
|
+
* affiché change entièrement, il n'y a rien à préserver.
|
|
4113
|
+
*/
|
|
4114
|
+
renderFeed(feed) {
|
|
4115
|
+
const node = this.el.feed;
|
|
4116
|
+
if (!node) return;
|
|
4117
|
+
if (this.feedPaused) return;
|
|
4118
|
+
const refilter = this.feedDirty;
|
|
4119
|
+
if (!refilter && feed.length === this.feedLen) return;
|
|
4120
|
+
const previous = this.feedLen;
|
|
4121
|
+
this.feedLen = feed.length;
|
|
4122
|
+
this.feedDirty = false;
|
|
4123
|
+
if (refilter) {
|
|
4124
|
+
const rows = [];
|
|
4125
|
+
for (let i = feed.length - 1; i >= 0; i--) {
|
|
4126
|
+
const l = feed[i];
|
|
4127
|
+
if (this.feedMatch(l)) rows.push(this.feedRow(l));
|
|
4128
|
+
}
|
|
4129
|
+
node.innerHTML = rows.length === 0 ? `<div class="empty">${feed.length === 0 ? "en attente de logs…" : "aucune entrée ne correspond au filtre."}</div>` : rows.join("");
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
const added = feed.length - previous;
|
|
4133
|
+
if (previous < 0 || added < 0 || added > feed.length) {
|
|
4134
|
+
this.feedDirty = true;
|
|
4135
|
+
this.renderFeed(feed);
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4138
|
+
if (added === 0) return;
|
|
4139
|
+
let html = "";
|
|
4140
|
+
for (let i = feed.length - 1; i >= feed.length - added; i--) {
|
|
4141
|
+
const l = feed[i];
|
|
4142
|
+
if (this.feedMatch(l)) html += this.feedRow(l);
|
|
4143
|
+
}
|
|
4144
|
+
if (html === "") return;
|
|
4145
|
+
if (node.querySelector(".empty")) node.innerHTML = "";
|
|
4146
|
+
node.insertAdjacentHTML("afterbegin", html);
|
|
4147
|
+
while (node.childElementCount > 40) node.lastElementChild?.remove();
|
|
4148
|
+
}
|
|
4149
|
+
renderNetwork() {
|
|
4150
|
+
this.text("netTotal", String(this.net.total));
|
|
4151
|
+
this.text("netErr", String(this.net.errors));
|
|
4152
|
+
this.text("netPend", String(this.net.pending));
|
|
4153
|
+
if (this.detailVersion !== this.detailRendered) {
|
|
4154
|
+
this.detailRendered = this.detailVersion;
|
|
4155
|
+
this.renderDetail();
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
renderDetail() {
|
|
4159
|
+
const node = this.el.netDetail;
|
|
4160
|
+
if (!node) return;
|
|
4161
|
+
const rid = this.selectedRid;
|
|
4162
|
+
const hasSel = !!rid || this.selNoRid;
|
|
4163
|
+
node.parentElement?.classList.toggle("sel", hasSel);
|
|
4164
|
+
if (!hasSel) {
|
|
4165
|
+
node.innerHTML = `<div class="empty">clique un appel → profil serveur (waterfall des phases, route, user).</div>`;
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
const closeBar = `<div class="det-bar"><span class="det-close" data-act="close" title="Fermer le détail">✕</span></div>`;
|
|
4169
|
+
let body;
|
|
4170
|
+
if (!rid) body = `<div class="det-err">cet appel n'a pas de requestId lisible — réponse sans header <b>X-Request-Id</b> (cross-origin sans Access-Control-Expose-Headers, ou appel hors Nodefony).</div>`;
|
|
4171
|
+
else {
|
|
4172
|
+
const st = this.net.profileState(rid);
|
|
4173
|
+
if (!st || st.status === "loading") body = `<div class="det-loading">profil <b>${escapeHtml(shortId(rid))}</b> — chargement…</div>`;
|
|
4174
|
+
else if (st.status === "missing") body = `<div class="det-err">profil introuvable (évincé du ring buffer, ou requête sans timing).</div>`;
|
|
4175
|
+
else if (st.status === "error") body = `<div class="det-err">erreur profiler : ${escapeHtml(st.message)}</div>`;
|
|
4176
|
+
else body = this.profileHtml(st.profile);
|
|
4177
|
+
}
|
|
4178
|
+
node.innerHTML = closeBar + body;
|
|
4179
|
+
}
|
|
4180
|
+
/** Désélectionne la requête → referme le détail (placeholder). */
|
|
4181
|
+
deselect() {
|
|
4182
|
+
if (this.selRowId !== null) this.netRows.get(this.selRowId)?.classList.remove("sel");
|
|
4183
|
+
this.selRowId = null;
|
|
4184
|
+
this.selectedRid = null;
|
|
4185
|
+
this.selNoRid = false;
|
|
4186
|
+
this.detailVersion++;
|
|
4187
|
+
this.scheduleRender();
|
|
4188
|
+
}
|
|
4189
|
+
/** Rend le détail d'un profil serveur (méta + waterfall des phases). */
|
|
4190
|
+
profileHtml(p) {
|
|
4191
|
+
const bars = computeWaterfall(p.phases);
|
|
4192
|
+
const total = p.durationMs === null ? "—" : `${p.durationMs}ms`;
|
|
4193
|
+
const kv = (k, val) => `<div class="kv"><span class="k">${k}</span><span class="v">${val}</span></div>`;
|
|
4194
|
+
const meta = `<div class="det-grid">` + kv("route", escapeHtml(p.route ?? "—")) + kv("controller", escapeHtml(p.controller ? `${p.controller}.${p.action ?? "?"}` : "—")) + kv("status", `${p.status ?? "—"}`) + kv("total serveur", total) + kv("user", escapeHtml(p.user ?? "anonyme")) + kv("kind", p.kind) + kv("requestId", escapeHtml(shortId(p.requestId))) + kv("traceparent", escapeHtml(traceId(p))) + (p.error ? kv("erreur", `<span class="crit">${escapeHtml(p.error)}</span>`) : "") + `</div>`;
|
|
4195
|
+
let wf = "";
|
|
4196
|
+
if (bars.length === 0) wf = `<div class="empty">aucune phase mesurée (timing désactivé ?).</div>`;
|
|
4197
|
+
else {
|
|
4198
|
+
wf = `<div class="wf-title">timeline des phases (serveur)</div><div class="wf">`;
|
|
4199
|
+
for (const b of bars) wf += `<div class="wf-row"><span class="wf-name">${escapeHtml(b.name)}</span><div class="wf-track"><div class="wf-bar ${b.tier}" style="left:${b.leftPct}%;width:${b.widthPct}%"></div></div><span class="wf-ms">${b.durationMs}ms</span></div>`;
|
|
4200
|
+
wf += `</div>`;
|
|
4201
|
+
}
|
|
4202
|
+
return meta + wf + this.queriesHtml(p.queries);
|
|
4203
|
+
}
|
|
4204
|
+
/** Rend les requêtes ORM (SEAM futur) — vide tant qu'aucun adapter ne pushe. */
|
|
4205
|
+
queriesHtml(queries) {
|
|
4206
|
+
if (!queries || queries.length === 0) return "";
|
|
4207
|
+
let out = `<div class="wf-title" style="margin-top:10px">requêtes ORM (${queries.length})</div><div class="net-list" style="border:0">`;
|
|
4208
|
+
for (const q of queries) out += `<div class="net-row" style="cursor:default"><span class="net-path" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>` + (q.connector ? `<span class="net-rid">${escapeHtml(q.connector)}</span>` : "") + (typeof q.rows === "number" ? `<span class="net-dur">${q.rows} rows</span>` : "") + `<span class="net-dur">${q.durationMs}ms</span></div>`;
|
|
4209
|
+
out += `</div>`;
|
|
4210
|
+
return out;
|
|
4211
|
+
}
|
|
4212
|
+
};
|
|
4213
|
+
/** Famille de status (2/3/4/5) → classe de couleur. `null` → 5 (inconnu). */
|
|
4214
|
+
function statusFamily(status) {
|
|
4215
|
+
if (status === null) return 5;
|
|
4216
|
+
return Math.floor(status / 100);
|
|
4217
|
+
}
|
|
4218
|
+
/** Méthode HTTP → classe de couleur du chip. */
|
|
4219
|
+
function methodClass(method) {
|
|
4220
|
+
const m = method.toLowerCase();
|
|
4221
|
+
if (m === "get" || m === "post" || m === "put" || m === "patch" || m === "delete") return m;
|
|
4222
|
+
return "ws";
|
|
4223
|
+
}
|
|
4224
|
+
/** Raccourci d'un id (1er bloc UUID, ou 8 chars). */
|
|
4225
|
+
function shortId(id) {
|
|
4226
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
4227
|
+
}
|
|
4228
|
+
/** Extrait le trace-id (2ᵉ segment) d'un `traceparent` W3C, raccourci. */
|
|
4229
|
+
function traceId(p) {
|
|
4230
|
+
if (!p.traceparent) return "—";
|
|
4231
|
+
const seg = p.traceparent.split("-");
|
|
4232
|
+
const tid = seg.length >= 2 ? seg[1] : p.traceparent;
|
|
4233
|
+
return tid && tid.length > 12 ? tid.slice(0, 12) + "…" : tid ?? "—";
|
|
4234
|
+
}
|
|
4235
|
+
/** Mappe un nom d'environnement vers une classe de couleur du badge. */
|
|
4236
|
+
function envClass(env) {
|
|
4237
|
+
const e = env.toLowerCase();
|
|
4238
|
+
if (e.startsWith("prod")) return "prod";
|
|
4239
|
+
if (e.startsWith("dev")) return "dev";
|
|
4240
|
+
if (e.startsWith("test")) return "test";
|
|
4241
|
+
if (e.startsWith("stag")) return "staging";
|
|
4242
|
+
return "";
|
|
4243
|
+
}
|
|
4244
|
+
function escapeHtml(s) {
|
|
4245
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4246
|
+
}
|
|
4247
|
+
//#endregion
|
|
4248
|
+
//#region src/client/debugbar/index.ts
|
|
4249
|
+
/**
|
|
4250
|
+
* Subpath `nodefony/debugbar` — point d'entrée de la debug bar Nodefony.
|
|
4251
|
+
*
|
|
4252
|
+
* Entry Rollup SÉPARÉE (tree-shakée, 0 octet si non importée) : ce fichier ne
|
|
4253
|
+
* doit JAMAIS être réexporté depuis `client/index.ts` (sinon le barrel `nodefony`
|
|
4254
|
+
* navigateur tire toute la barre). Cf décision `project_client_lib_subpaths_decision`.
|
|
4255
|
+
*
|
|
4256
|
+
* Usage :
|
|
4257
|
+
* ```ts
|
|
4258
|
+
* import { mountDebugBar } from "nodefony/debugbar";
|
|
4259
|
+
* mountDebugBar(); // dev only
|
|
4260
|
+
* ```
|
|
4261
|
+
*/
|
|
4262
|
+
/**
|
|
4263
|
+
* Monte la debug bar sur la page courante. Idempotent (un seul widget par page).
|
|
4264
|
+
* No-op hors navigateur (SSR). Branche un {@link RealtimeClient} sur le WS
|
|
4265
|
+
* realtime et rend l'overlay live.
|
|
4266
|
+
*
|
|
4267
|
+
* @param opts - URL du WS, client partagé, position, ouverture initiale
|
|
4268
|
+
* @returns l'instance {@link DebugBar} (appeler `.unmount()` pour retirer)
|
|
4269
|
+
*/
|
|
4270
|
+
function mountDebugBar(opts = {}) {
|
|
4271
|
+
return new DebugBar(opts).mount();
|
|
4272
|
+
}
|
|
4273
|
+
//#endregion
|
|
4274
|
+
export { DebugBar, NetworkModel, computeWaterfall, mountDebugBar as default, mountDebugBar, isError as isNetworkError, phaseTier };
|