bazis 0.96.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/cli/build.ts +147 -0
- package/cli/codegen.ts +40 -0
- package/cli/generateModule.ts +187 -0
- package/cli/generateProject.ts +183 -0
- package/cli/main.ts +125 -0
- package/cli/moduleRegistration.ts +127 -0
- package/cli/naming.ts +124 -0
- package/cli/parseCli.ts +151 -0
- package/cli/templates/module.ts +497 -0
- package/cli/templates/pack.ts +76 -0
- package/cli/templates/passport.ts +178 -0
- package/core/agent/AgentContextBuilder.ts +567 -0
- package/core/agent/AgentDataDefinition.ts +8 -0
- package/core/agent/AgentRegistry.ts +539 -0
- package/core/agent/AgentRuntime.ts +47 -0
- package/core/agent/AgentToolExecutor.ts +2130 -0
- package/core/agent/AgentToolHooks.ts +39 -0
- package/core/agent/ModelProvider.ts +21 -0
- package/core/agent/README.md +229 -0
- package/core/agent/Tool.contract.ts +20 -0
- package/core/agent/decorators/Agent.ts +11 -0
- package/core/agent/decorators/Prompt.ts +11 -0
- package/core/agent/decorators/Task.ts +29 -0
- package/core/agent/decorators/Tool.ts +12 -0
- package/core/agent/errors.ts +43 -0
- package/core/agent/generated/catalog.ts +26 -0
- package/core/agent/index.ts +170 -0
- package/core/agent/internal/AgentExecutionDriver.ts +1497 -0
- package/core/agent/internal/AgentExecutionState.ts +775 -0
- package/core/agent/internal/AgentJsonSchema.ts +150 -0
- package/core/agent/internal/AgentModelBinding.ts +57 -0
- package/core/agent/internal/Schema.contract.ts +36 -0
- package/core/agent/internal/ToolAudit.projector.ts +163 -0
- package/core/agent/internal/ToolContract.validator.ts +102 -0
- package/core/agent/metadata.ts +398 -0
- package/core/agent/moduleContributions-v1.ts +113 -0
- package/core/agent/semantic.ts +903 -0
- package/core/agent/session/codec.ts +33 -0
- package/core/agent/session/contracts.ts +96 -0
- package/core/agent/session/entities.ts +84 -0
- package/core/agent/session/protector.ts +92 -0
- package/core/app/index.ts +5 -0
- package/core/app/runApp.ts +293 -0
- package/core/app/ui/index.ts +29 -0
- package/core/app/ui/uiProfileResolver.ts +966 -0
- package/core/app/ui/uiSurfaceHosting.ts +38 -0
- package/core/app/ui/uiSurfaceHttp.ts +476 -0
- package/core/background/BackgroundService.ts +223 -0
- package/core/background/backgroundModule.ts +42 -0
- package/core/background/decorator.ts +43 -0
- package/core/background/delay.ts +27 -0
- package/core/background/index.ts +15 -0
- package/core/cache/COMPATIBILITY.md +13 -0
- package/core/cache/ICache.ts +71 -0
- package/core/cache/MemoryCache.ts +394 -0
- package/core/cache/README.md +83 -0
- package/core/cache/SPEC.md +1355 -0
- package/core/cache/cacheModule.ts +21 -0
- package/core/cache/decorators/Cacheable.ts +38 -0
- package/core/cache/decorators/CacheableRedis.ts +39 -0
- package/core/cache/decorators/OutputCache.ts +33 -0
- package/core/cache/decorators/OutputRedisCache.ts +33 -0
- package/core/cache/decorators/cacheableMetadata.ts +78 -0
- package/core/cache/decorators/cacheableRedisMetadata.ts +81 -0
- package/core/cache/decorators/outputCacheMetadata.ts +71 -0
- package/core/cache/decorators/outputRedisCacheMetadata.ts +77 -0
- package/core/cache/di/classProviderHook.ts +26 -0
- package/core/cache/distributed/CacheCodec.ts +5 -0
- package/core/cache/distributed/DistributedCache.ts +403 -0
- package/core/cache/distributed/DistributedCacheDriver.ts +83 -0
- package/core/cache/distributed/DistributedCacheStores.ts +28 -0
- package/core/cache/distributed/IDistributedCache.ts +40 -0
- package/core/cache/distributed/NamedCacheRegistry.ts +23 -0
- package/core/cache/distributed/codecs.ts +32 -0
- package/core/cache/distributed/index.ts +19 -0
- package/core/cache/errors/CacheError.ts +16 -0
- package/core/cache/http/CachedHttpPayload.ts +213 -0
- package/core/cache/http/applyClientCacheHeaders.ts +61 -0
- package/core/cache/http/buildOutputCacheKey.ts +154 -0
- package/core/cache/http/buildRedisOutputCacheKey.ts +16 -0
- package/core/cache/http/composeOutputCache.ts +130 -0
- package/core/cache/http/outputCacheMiddleware.ts +111 -0
- package/core/cache/http/outputCacheSecurityWarning.ts +58 -0
- package/core/cache/http/outputRedisCacheMiddleware.ts +118 -0
- package/core/cache/index.ts +97 -0
- package/core/cache/internal/AutomaticCacheKey.ts +106 -0
- package/core/cache/internal/CacheKeyGuard.ts +56 -0
- package/core/cache/internal/buildCacheProviders.ts +146 -0
- package/core/cache/internal/normalizeCacheKey.ts +9 -0
- package/core/cache/internal/resolveCachePolicy.ts +32 -0
- package/core/cache/internal/resolveCacheableRedisPolicy.ts +17 -0
- package/core/cache/internal/resolveOutputRedisCachePolicy.ts +17 -0
- package/core/cache/providers/cachedProviders.ts +68 -0
- package/core/cache/services/cacheProxy.ts +243 -0
- package/core/cache/tokens/CACHE_POLICIES.ts +5 -0
- package/core/cache/tokens/DISTRIBUTED_CACHE.ts +30 -0
- package/core/cache/types/CacheEntry.ts +7 -0
- package/core/cache/types/CacheOptions.ts +60 -0
- package/core/cache/types/CachePolicy.ts +74 -0
- package/core/cache/types/CacheableRedisOptions.ts +23 -0
- package/core/cache/types/OutputRedisCacheOptions.ts +25 -0
- package/core/di/README.md +104 -0
- package/core/di/ServiceCollection.ts +185 -0
- package/core/di/ServiceProvider.ts +689 -0
- package/core/di/ServiceScope.ts +61 -0
- package/core/di/container.ts +38 -0
- package/core/di/errors/AmbiguousNamedDependencyError.ts +10 -0
- package/core/di/errors/AsyncResolutionRequiredError.ts +12 -0
- package/core/di/errors/CircularDependencyError.ts +10 -0
- package/core/di/errors/ClassDependenciesMismatchError.ts +15 -0
- package/core/di/errors/DiError.ts +6 -0
- package/core/di/errors/InvalidProviderError.ts +9 -0
- package/core/di/errors/ModuleEncapsulationError.ts +7 -0
- package/core/di/errors/ModuleOwnedProviderConflictError.ts +8 -0
- package/core/di/errors/NamedDependencyNotFoundError.ts +7 -0
- package/core/di/errors/OptionsValidationError.ts +7 -0
- package/core/di/errors/ProviderNotFoundError.ts +10 -0
- package/core/di/errors/ScopeDisposedError.ts +7 -0
- package/core/di/errors/ScopedServiceFromRootError.ts +9 -0
- package/core/di/errors/ServiceValidationError.ts +7 -0
- package/core/di/errors/index.ts +14 -0
- package/core/di/extensions/activator.ts +39 -0
- package/core/di/extensions/application.ts +117 -0
- package/core/di/extensions/hosted-service.ts +124 -0
- package/core/di/extensions/index.ts +23 -0
- package/core/di/extensions/options-reloadable.ts +137 -0
- package/core/di/extensions/options.ts +91 -0
- package/core/di/extensions/service-provider-token.ts +21 -0
- package/core/di/index.ts +97 -0
- package/core/di/internal/GraphValidator.ts +119 -0
- package/core/di/internal/HostedServicePlan.ts +158 -0
- package/core/di/internal/NamedTokenIndex.ts +55 -0
- package/core/di/internal/OpenGenericRegistration.ts +14 -0
- package/core/di/internal/ResolutionPlan.ts +18 -0
- package/core/di/internal/ResolutionScopeState.ts +19 -0
- package/core/di/internal/ResolutionTracker.ts +153 -0
- package/core/di/internal/ScopeLifecycle.ts +159 -0
- package/core/di/internal/ServiceRegistration.ts +14 -0
- package/core/di/internal/ServiceRegistry.ts +252 -0
- package/core/di/internal/classDeps.ts +38 -0
- package/core/di/internal/disposal.ts +58 -0
- package/core/di/internal/providerDeps.ts +26 -0
- package/core/di/module/DI.ts +146 -0
- package/core/di/module/Global.ts +25 -0
- package/core/di/module/Module.ts +44 -0
- package/core/di/module/ModuleRegistrar.ts +69 -0
- package/core/di/module/autoDeps.ts +39 -0
- package/core/di/module/classProviderHooks.ts +66 -0
- package/core/di/module/collectModuleConfigs.ts +57 -0
- package/core/di/module/collectModuleControllers.ts +53 -0
- package/core/di/module/collectModuleUiProfiles.ts +53 -0
- package/core/di/module/createContainer.ts +261 -0
- package/core/di/module/encapsulation.ts +416 -0
- package/core/di/module/encapsulationHooks.ts +36 -0
- package/core/di/module/generatedProviderAttachments.ts +102 -0
- package/core/di/module/index.ts +41 -0
- package/core/di/module/moduleExtensions.ts +35 -0
- package/core/di/module/moduleNameBinding.ts +136 -0
- package/core/di/module/moduleOwnedProviderContributors.ts +409 -0
- package/core/di/module/providerSelection.ts +90 -0
- package/core/di/module/shortcuts.ts +111 -0
- package/core/di/module/types/BazisModule.ts +71 -0
- package/core/di/module/types/DiRegistrar.ts +24 -0
- package/core/di/module/types/index.ts +2 -0
- package/core/di/provider/index.ts +26 -0
- package/core/di/provider/providerGuards.ts +17 -0
- package/core/di/provider/types/AsyncFactoryProvider.ts +15 -0
- package/core/di/provider/types/ClassProvider.ts +18 -0
- package/core/di/provider/types/FactoryProvider.ts +20 -0
- package/core/di/provider/types/KeyedDependency.ts +20 -0
- package/core/di/provider/types/LazyDependency.ts +31 -0
- package/core/di/provider/types/NamedDependency.ts +16 -0
- package/core/di/provider/types/Provider.ts +10 -0
- package/core/di/provider/types/ProviderDefinition.ts +11 -0
- package/core/di/provider/types/ProviderDependencyList.ts +11 -0
- package/core/di/provider/types/ProviderLifetime.ts +1 -0
- package/core/di/provider/types/ProviderOwnership.ts +7 -0
- package/core/di/provider/types/ResolvedDeps.ts +18 -0
- package/core/di/provider/types/ValueProvider.ts +6 -0
- package/core/di/provider/types/index.ts +13 -0
- package/core/di/token.ts +84 -0
- package/core/di/types/BuildServiceProviderOptions.ts +4 -0
- package/core/di/types/ServiceKey.ts +1 -0
- package/core/di/types/ServiceResolver.ts +16 -0
- package/core/di/types/index.ts +3 -0
- package/core/generatedFingerprint.ts +60 -0
- package/core/generatedRuntime.ts +187 -0
- package/core/grpc/Grpc.module.ts +23 -0
- package/core/grpc/GrpcBinding.contract.ts +35 -0
- package/core/grpc/GrpcCall.ts +73 -0
- package/core/grpc/GrpcClient.ts +172 -0
- package/core/grpc/GrpcClientCall.ts +267 -0
- package/core/grpc/GrpcError.ts +19 -0
- package/core/grpc/GrpcRequest.binding.ts +45 -0
- package/core/grpc/GrpcServer.ts +204 -0
- package/core/grpc/GrpcStatus.ts +8 -0
- package/core/grpc/GrpcTransport.ts +174 -0
- package/core/grpc/Metadata.ts +73 -0
- package/core/grpc/ProtoCodec.ts +225 -0
- package/core/grpc/ProtoParser.ts +192 -0
- package/core/grpc/ProtoReader.ts +34 -0
- package/core/grpc/ProtoSchema.ts +52 -0
- package/core/grpc/ServerCredentials.ts +30 -0
- package/core/grpc/clientContracts.ts +39 -0
- package/core/grpc/contracts.ts +45 -0
- package/core/grpc/decorators.ts +82 -0
- package/core/grpc/grpcClientProvider.ts +9 -0
- package/core/grpc/index.ts +17 -0
- package/core/grpc/moduleControllers.ts +38 -0
- package/core/grpc/protobuf.ts +80 -0
- package/core/grpc/serviceDefinition.ts +20 -0
- package/core/http/Authorization/Authorize.ts +52 -0
- package/core/http/Authorization/authorizeComposer.ts +31 -0
- package/core/http/Authorization/metadata.ts +158 -0
- package/core/http/Binding/ParameterBinder.ts +109 -0
- package/core/http/Binding/RequestModel.ts +27 -0
- package/core/http/Binding/autoBindings.ts +125 -0
- package/core/http/Binding/bindings.ts +19 -0
- package/core/http/Binding/convert.ts +48 -0
- package/core/http/Binding/generatedSpec.ts +18 -0
- package/core/http/Binding/listModelRegistry.ts +47 -0
- package/core/http/Binding/modelBinder.ts +345 -0
- package/core/http/Binding/modelValidator.ts +30 -0
- package/core/http/Binding/requestModelRegistry.ts +117 -0
- package/core/http/Decorators/attributes.ts +92 -0
- package/core/http/Decorators/controller.ts +24 -0
- package/core/http/Decorators/metadata.ts +107 -0
- package/core/http/Decorators/routes.ts +131 -0
- package/core/http/Errors/HttpError.ts +92 -0
- package/core/http/HttpContext/HttpContext.ts +148 -0
- package/core/http/HttpContext/ResponseBuilder.ts +37 -0
- package/core/http/HttpContext/inspectableRedirects.ts +47 -0
- package/core/http/HttpContext/principal.ts +24 -0
- package/core/http/HttpContext/responseLifetime.ts +45 -0
- package/core/http/HttpServer.ts +627 -0
- package/core/http/Middleware/accessLog.ts +90 -0
- package/core/http/Middleware/cors.ts +160 -0
- package/core/http/Middleware/errorHandler.ts +100 -0
- package/core/http/Middleware/pipeline.ts +21 -0
- package/core/http/Middleware/rateLimit.ts +104 -0
- package/core/http/Middleware/securityHeaders.ts +118 -0
- package/core/http/Middleware/types.ts +19 -0
- package/core/http/OpenApi/generatedOpenApiRegistry.ts +102 -0
- package/core/http/OpenApi/openApiDocs.ts +75 -0
- package/core/http/OpenApi/openApiDocument.ts +356 -0
- package/core/http/README.md +139 -0
- package/core/http/Results/HttpResult.ts +19 -0
- package/core/http/Results/normalize.ts +93 -0
- package/core/http/Results/results.ts +70 -0
- package/core/http/Routing/Router.ts +201 -0
- package/core/http/Routing/RouterBuilder.ts +184 -0
- package/core/http/Routing/template.ts +112 -0
- package/core/http/SPEC.md +981 -0
- package/core/http/Versioning/VersionReader.ts +20 -0
- package/core/http/WebSocketUpgrade.ts +29 -0
- package/core/http/composeRouteMiddlewareComposers.ts +21 -0
- package/core/http/correlation/createCorrelationIdMiddleware.ts +79 -0
- package/core/http/httpModule.ts +48 -0
- package/core/http/index.ts +140 -0
- package/core/http/middlewareTokens.ts +48 -0
- package/core/http/options.ts +134 -0
- package/core/http-client/index.ts +105 -0
- package/core/infra/Infra.ts +158 -0
- package/core/infra/InfraConnector.ts +86 -0
- package/core/infra/InfraLifecycle.ts +86 -0
- package/core/infra/cache/RedisDistributedCacheBackend.ts +88 -0
- package/core/infra/cache/RedisDistributedCacheDriver.ts +186 -0
- package/core/infra/cache/index.ts +7 -0
- package/core/infra/connectorConfig.ts +29 -0
- package/core/infra/connectorIdentity.ts +39 -0
- package/core/infra/connectors/codex/CodexAppServer.ts +146 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +258 -0
- package/core/infra/connectors/codex/CodexPolicy.ts +114 -0
- package/core/infra/connectors/codex/CodexTurn.ts +133 -0
- package/core/infra/connectors/codex/contracts.ts +82 -0
- package/core/infra/connectors/codex.ts +26 -0
- package/core/infra/connectors/llm.ts +78 -0
- package/core/infra/connectors/llmRouter.ts +443 -0
- package/core/infra/connectors/openaiCompatible.ts +590 -0
- package/core/infra/connectors/openaiTextStream.ts +134 -0
- package/core/infra/connectors/opensearch.ts +307 -0
- package/core/infra/connectors/postgres.ts +105 -0
- package/core/infra/connectors/redis.ts +132 -0
- package/core/infra/index.ts +58 -0
- package/core/kernel/ApplicationLifetime.ts +91 -0
- package/core/kernel/Bazis.ts +73 -0
- package/core/kernel/Environment.ts +53 -0
- package/core/kernel/Kernel.ts +391 -0
- package/core/kernel/KernelBuilder.ts +172 -0
- package/core/kernel/LifecycleCoordinator.ts +204 -0
- package/core/kernel/README.md +66 -0
- package/core/kernel/SupervisedHostedService.ts +147 -0
- package/core/kernel/config/ConfigRegistry.ts +48 -0
- package/core/kernel/config/Configuration.ts +110 -0
- package/core/kernel/config/README.md +62 -0
- package/core/kernel/config/Secret.ts +29 -0
- package/core/kernel/config/addConfigOptions.ts +63 -0
- package/core/kernel/config/defineConfig.ts +211 -0
- package/core/kernel/config/sources.ts +123 -0
- package/core/kernel/correlation/requestContext.ts +57 -0
- package/core/kernel/errors/ConfigKeyMissingError.ts +7 -0
- package/core/kernel/errors/KernelError.ts +7 -0
- package/core/kernel/errors/ShutdownTimeoutError.ts +7 -0
- package/core/kernel/errors/StartupAbortedError.ts +7 -0
- package/core/kernel/errors/StartupTimeoutError.ts +7 -0
- package/core/kernel/errors/index.ts +5 -0
- package/core/kernel/events/EventBus.ts +140 -0
- package/core/kernel/events/EventToken.ts +17 -0
- package/core/kernel/events/eventSubscribers.ts +126 -0
- package/core/kernel/events/kernelEvents.ts +16 -0
- package/core/kernel/events/onEvent.ts +32 -0
- package/core/kernel/health/HealthCheckContracts.ts +42 -0
- package/core/kernel/health/HealthService.ts +93 -0
- package/core/kernel/index.ts +96 -0
- package/core/kernel/internal/awaitAbortable.ts +18 -0
- package/core/kernel/internal/awaitShutdown.ts +21 -0
- package/core/kernel/internal/reportDiagnosticFailure.ts +8 -0
- package/core/kernel/internal/validateTimeout.ts +8 -0
- package/core/kernel/lifecycleHooks.ts +18 -0
- package/core/kernel/logging/ConsoleLogger.ts +68 -0
- package/core/kernel/logging/Logger.ts +22 -0
- package/core/kernel/types/ConfigSource.ts +9 -0
- package/core/kernel/types/EnvironmentName.ts +1 -0
- package/core/kernel/types/KernelOptions.ts +23 -0
- package/core/kernel/types/LifecycleHook.ts +18 -0
- package/core/kernel/types/index.ts +4 -0
- package/core/orm/DATABASE_PROVIDER.ts +5 -0
- package/core/orm/OrmHostedPlan.validator.ts +55 -0
- package/core/orm/OrmLifecycle.ts +146 -0
- package/core/orm/OrmOwnedStoreLifecycle.ts +96 -0
- package/core/orm/buildOrmProviders.ts +152 -0
- package/core/orm/databaseConnector.ts +108 -0
- package/core/orm/encapsulationHook.ts +19 -0
- package/core/orm/index.ts +95 -0
- package/core/orm/listQuery.ts +198 -0
- package/core/orm/ormModule.ts +173 -0
- package/core/orm/ownedStoreContributions.ts +195 -0
- package/core/orm/registerRepositories.ts +27 -0
- package/core/orm/repository.ts +45 -0
- package/core/orm/strictSchemaHostedIdentity.ts +17 -0
- package/core/scripts/agent-codegen.ts +1166 -0
- package/core/scripts/di-generate-target.ts +90 -0
- package/core/scripts/di-generate.ts +1837 -0
- package/core/scripts/orm-predicate-codegen.ts +33 -0
- package/core/scripts/request-model-codegen.ts +326 -0
- package/core/websocket/DESIGN.md +297 -0
- package/core/websocket/WsRateLimiter.ts +101 -0
- package/core/websocket/adapter/adapter.interface.ts +45 -0
- package/core/websocket/adapter/in-memory.adapter.ts +190 -0
- package/core/websocket/adapter/redis-delivery-scripts.ts +135 -0
- package/core/websocket/adapter/redis-delivery-store.ts +86 -0
- package/core/websocket/adapter/redis-operations.ts +33 -0
- package/core/websocket/adapter/redis.adapter.ts +218 -0
- package/core/websocket/codec/binary.codec.ts +87 -0
- package/core/websocket/codec/json.codec.ts +34 -0
- package/core/websocket/codec/packet-codec.interface.ts +17 -0
- package/core/websocket/decorators.ts +165 -0
- package/core/websocket/explorer.ts +126 -0
- package/core/websocket/index.ts +78 -0
- package/core/websocket/middleware.ts +146 -0
- package/core/websocket/packet-codec.ts +75 -0
- package/core/websocket/reliable-delivery.ts +56 -0
- package/core/websocket/session-creation.ts +16 -0
- package/core/websocket/session-manager.ts +856 -0
- package/core/websocket/socket-wrapper.ts +314 -0
- package/core/websocket/topic-cache.ts +59 -0
- package/core/websocket/types.ts +122 -0
- package/core/websocket/websocketModule.ts +96 -0
- package/core/websocket/ws-dispatch.ts +123 -0
- package/core/websocket/ws-server.ts +1707 -0
- package/index.ts +281 -0
- package/library/boundary/diagnostics-v1.ts +136 -0
- package/library/boundary/index.ts +73 -0
- package/library/boundary/json/decodeBoundedJsonV1.ts +518 -0
- package/library/boundary/json/limits-v1.ts +46 -0
- package/library/boundary/json/normalizeBoundedJsonV1.ts +256 -0
- package/library/boundary/json/types-v1.ts +11 -0
- package/library/boundary/schema/boundarySchemaHashV1.ts +6 -0
- package/library/boundary/schema/collectBoundaryValueDiagnosticsV1.ts +325 -0
- package/library/boundary/schema/decodeBoundarySchemaV1.ts +646 -0
- package/library/boundary/schema/index.ts +8 -0
- package/library/boundary/schema/types-v1.ts +99 -0
- package/library/boundary/schema/validateBoundaryValueV1.ts +58 -0
- package/library/boundary/serialization/canonicalJsonV1.ts +66 -0
- package/library/http-client/HttpClient.ts +969 -0
- package/library/http-client/HttpClientFactory.ts +45 -0
- package/library/http-client/errors.ts +68 -0
- package/library/http-client/index.ts +43 -0
- package/library/http-client/interceptors.ts +40 -0
- package/library/http-client/progress.ts +62 -0
- package/library/http-client/redirectProtocol.ts +6 -0
- package/library/http-client/serialize.ts +133 -0
- package/library/http-client/types.ts +174 -0
- package/library/jsonapi/ListQueryError.ts +25 -0
- package/library/jsonapi/ListRequest.ts +37 -0
- package/library/jsonapi/decorators.ts +161 -0
- package/library/jsonapi/document.ts +124 -0
- package/library/jsonapi/index.ts +40 -0
- package/library/jsonapi/parseListQuery.ts +291 -0
- package/library/jsonapi/types.ts +125 -0
- package/library/jwt/JwtEncoder.ts +40 -0
- package/library/jwt/JwtKeyRing.ts +164 -0
- package/library/jwt/JwtValidator.ts +210 -0
- package/library/jwt/TokenIssuer.ts +149 -0
- package/library/jwt/TokenService.ts +51 -0
- package/library/jwt/base64url.ts +61 -0
- package/library/jwt/claims.ts +36 -0
- package/library/jwt/errors.ts +44 -0
- package/library/jwt/index.ts +59 -0
- package/library/jwt/limits.ts +8 -0
- package/library/jwt/signing/Hs256Algorithm.ts +62 -0
- package/library/jwt/signing/Rs256Algorithm.ts +133 -0
- package/library/jwt/signing/SigningAlgorithm.ts +18 -0
- package/library/jwt/signing/pem.ts +30 -0
- package/library/openapi/codegen.ts +875 -0
- package/library/openapi/document.ts +326 -0
- package/library/openapi/index.ts +13 -0
- package/library/openapi/types.ts +69 -0
- package/library/openapi/ui.ts +182 -0
- package/library/orm/DatabaseFacade.ts +209 -0
- package/library/orm/DbContext.ts +107 -0
- package/library/orm/DbContextFactory.ts +25 -0
- package/library/orm/DbContextOptions.ts +33 -0
- package/library/orm/Metadata/DynamicModelBuilder.ts +497 -0
- package/library/orm/Metadata/KeyTuple.ts +36 -0
- package/library/orm/Metadata/ModelBuilder.ts +299 -0
- package/library/orm/Metadata/OrmModel.ts +80 -0
- package/library/orm/Metadata/ValueConverter.ts +57 -0
- package/library/orm/Metadata/conventions.ts +54 -0
- package/library/orm/Metadata/decorators.ts +396 -0
- package/library/orm/Metadata/types.ts +140 -0
- package/library/orm/Providers/OwnedStoreCatalog.reader.ts +1802 -0
- package/library/orm/Providers/PostgresDialect.ts +234 -0
- package/library/orm/Providers/PostgresProvider.ts +1494 -0
- package/library/orm/Providers/PostgresSchema.decoder.ts +107 -0
- package/library/orm/Providers/limits.ts +29 -0
- package/library/orm/Providers/operationDeadline.ts +43 -0
- package/library/orm/Providers/ormOwnedStoreRuntime.ts +19 -0
- package/library/orm/Providers/ormTransactionRuntime.ts +117 -0
- package/library/orm/Providers/propertyConversion.ts +14 -0
- package/library/orm/Providers/resolvedForeignKey.ts +16 -0
- package/library/orm/Providers/transactionCallbacks.ts +45 -0
- package/library/orm/Providers/transactionOutcome.ts +82 -0
- package/library/orm/Providers/transactionScopes.ts +78 -0
- package/library/orm/Providers/types.ts +207 -0
- package/library/orm/Query/DbSet.ts +78 -0
- package/library/orm/Query/EntityQuery.ts +247 -0
- package/library/orm/Query/ImmediateMutations.ts +226 -0
- package/library/orm/Query/IncludeLoader.ts +200 -0
- package/library/orm/Query/QueryPlan.ts +55 -0
- package/library/orm/Query/SqlTranslator.ts +195 -0
- package/library/orm/Query/conditions.ts +127 -0
- package/library/orm/Query/materialize.ts +39 -0
- package/library/orm/README.md +256 -0
- package/library/orm/Repository/README.md +684 -0
- package/library/orm/Repository/Repository.ts +68 -0
- package/library/orm/Saving/CommandBuilder.ts +139 -0
- package/library/orm/Saving/ExecutionStrategy.ts +128 -0
- package/library/orm/Saving/SaveExecutor.ts +413 -0
- package/library/orm/Saving/applyConventions.ts +64 -0
- package/library/orm/Schema/CheckExpression.ts +327 -0
- package/library/orm/Schema/ExactSchemaVerifier.ts +168 -0
- package/library/orm/Schema/ExpectedSchema.ts +43 -0
- package/library/orm/Schema/MigrationRunner.ts +146 -0
- package/library/orm/Schema/Migrator.ts +73 -0
- package/library/orm/Schema/OrmOwnedStore.ts +123 -0
- package/library/orm/Schema/OwnedStoreAdmission.ts +752 -0
- package/library/orm/Schema/OwnedStoreCanonical.ts +273 -0
- package/library/orm/Schema/OwnedStoreCatalog.ts +1325 -0
- package/library/orm/Schema/SafeAdditiveSchema.ts +100 -0
- package/library/orm/Schema/SchemaAdmission.ts +62 -0
- package/library/orm/Schema/SchemaDiffer.ts +78 -0
- package/library/orm/Schema/formatSchemaDifferences.ts +62 -0
- package/library/orm/Schema/introspection.ts +52 -0
- package/library/orm/Schema/physicalColumnTypes.ts +37 -0
- package/library/orm/Schema/tableKey.ts +26 -0
- package/library/orm/Tracking/ChangeTracker.ts +448 -0
- package/library/orm/Tracking/EntityState.ts +13 -0
- package/library/orm/Transactions/OrmTransaction.ts +30 -0
- package/library/orm/Transactions/TransactionScopeCoordinator.ts +377 -0
- package/library/orm/errors.ts +190 -0
- package/library/orm/index.ts +162 -0
- package/library/orm/runtime.ts +14 -0
- package/library/redaction/index.ts +166 -0
- package/library/ui/UiProfile.ts +77 -0
- package/library/ui/compiled-v1.ts +131 -0
- package/library/ui/compiler/compileUiSurfaceV1.ts +717 -0
- package/library/ui/compiler/contracts-v1.ts +25 -0
- package/library/ui/compiler/index.ts +12 -0
- package/library/ui/compiler/openApiIndex.ts +246 -0
- package/library/ui/defineUiProfile.ts +340 -0
- package/library/ui/diagnostics-v1.ts +33 -0
- package/library/ui/index.ts +129 -0
- package/library/ui/profile-v1.ts +191 -0
- package/library/ui/serialization.ts +130 -0
- package/library/ui/uiProfileAuthoring.ts +289 -0
- package/library/ui/uiProfileAuthoringMetadata.ts +78 -0
- package/library/validation/MessageRegistry.ts +144 -0
- package/library/validation/README.md +62 -0
- package/library/validation/RuleCompiler.ts +183 -0
- package/library/validation/RuleEngine.ts +401 -0
- package/library/validation/SPEC.md +579 -0
- package/library/validation/ValidationError.ts +37 -0
- package/library/validation/ValidationResult.ts +63 -0
- package/library/validation/Validator.ts +101 -0
- package/library/validation/index.ts +16 -0
- package/library/validation/metadata.ts +69 -0
- package/library/validation/modelValidatorAdapter.ts +119 -0
- package/library/validation/types/ValidationCode.ts +69 -0
- package/library/validation/types/ValidatorOptions.ts +153 -0
- package/package.json +80 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import type { ReplayDelivery, ServerPacket, SocketContext } from "./types";
|
|
2
|
+
import type { WebSocketAdapter } from "./adapter/adapter.interface";
|
|
3
|
+
import { issueSessionCreationToken } from "./session-creation";
|
|
4
|
+
|
|
5
|
+
const ROOM_SEPARATOR = "\0";
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
|
|
8
|
+
export interface SessionState {
|
|
9
|
+
sid: string;
|
|
10
|
+
namespace: string;
|
|
11
|
+
context: SocketContext;
|
|
12
|
+
rooms: string[];
|
|
13
|
+
createdAt: number;
|
|
14
|
+
lastSeenAt: number;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
outboundQueue: ServerPacket[];
|
|
17
|
+
activeConnId?: string;
|
|
18
|
+
/** Monotonic state version. Optional for compatibility with older adapters. */
|
|
19
|
+
revision?: number;
|
|
20
|
+
/** Namespace-specific reconnect TTL. */
|
|
21
|
+
ttlMs?: number;
|
|
22
|
+
/** Approximate serialized bytes currently retained in `outboundQueue`. */
|
|
23
|
+
outboundQueueBytes?: number;
|
|
24
|
+
/** A live connection must renew this lease; stale active markers expire. */
|
|
25
|
+
activeLeaseExpiresAt?: number;
|
|
26
|
+
/** Adapter/node instance that currently owns `activeConnId`. */
|
|
27
|
+
ownerInstanceId?: string;
|
|
28
|
+
/** Immutable process-local creation order for bounded in-memory deletion history. */
|
|
29
|
+
creationToken?: string;
|
|
30
|
+
/** Client-ack replay cannot be downgraded by a later connection. */
|
|
31
|
+
replayDelivery?: ReplayDelivery;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SessionManagerOptions {
|
|
35
|
+
defaultTtlMs?: number;
|
|
36
|
+
maxOutboundQueue?: number;
|
|
37
|
+
maxOutboundQueueBytes?: number;
|
|
38
|
+
maxSessions?: number;
|
|
39
|
+
maxRoomsPerSession?: number;
|
|
40
|
+
maxRoomNameLength?: number;
|
|
41
|
+
maxOfflineBroadcastRecipients?: number;
|
|
42
|
+
maxOfflineBroadcastBytes?: number;
|
|
43
|
+
activeLeaseMs?: number;
|
|
44
|
+
adapter?: WebSocketAdapter | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OfflineBroadcastResult {
|
|
48
|
+
readonly queued: number;
|
|
49
|
+
readonly bytes: number;
|
|
50
|
+
readonly truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class SessionCapacityError extends Error {
|
|
54
|
+
public constructor(maxSessions: number) {
|
|
55
|
+
super(`WebSocket session capacity exceeded (${maxSessions}).`);
|
|
56
|
+
this.name = "SessionCapacityError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Local session index with ordered persistence. All mutations receive a
|
|
62
|
+
* monotonic revision and adapter writes for one SID are serialized. Critical
|
|
63
|
+
* server boundaries call `flushSession` before acknowledging reconnect/state.
|
|
64
|
+
*/
|
|
65
|
+
export class SessionManager {
|
|
66
|
+
private readonly sessions = new Map<string, SessionState>();
|
|
67
|
+
private readonly roomMembers = new Map<string, Set<string>>();
|
|
68
|
+
private readonly persistenceTails = new Map<string, Promise<void>>();
|
|
69
|
+
private readonly persistenceErrors = new Map<string, unknown>();
|
|
70
|
+
private readonly defaultTtlMs: number;
|
|
71
|
+
private readonly maxOutboundQueue: number;
|
|
72
|
+
private readonly maxOutboundQueueBytes: number;
|
|
73
|
+
private readonly maxSessions: number;
|
|
74
|
+
private readonly maxRoomsPerSession: number;
|
|
75
|
+
private readonly maxRoomNameLength: number;
|
|
76
|
+
private readonly maxOfflineBroadcastRecipients: number;
|
|
77
|
+
private readonly maxOfflineBroadcastBytes: number;
|
|
78
|
+
private readonly activeLeaseMs: number;
|
|
79
|
+
private readonly adapter: WebSocketAdapter | null;
|
|
80
|
+
|
|
81
|
+
constructor(options: SessionManagerOptions = {}) {
|
|
82
|
+
this.defaultTtlMs = positiveInteger(options.defaultTtlMs, 2 * 60 * 60 * 1000);
|
|
83
|
+
this.maxOutboundQueue = positiveInteger(options.maxOutboundQueue, 100);
|
|
84
|
+
this.maxOutboundQueueBytes = positiveInteger(options.maxOutboundQueueBytes, 1024 * 1024);
|
|
85
|
+
this.maxSessions = positiveInteger(options.maxSessions, 10_000);
|
|
86
|
+
this.maxRoomsPerSession = positiveInteger(options.maxRoomsPerSession, 256);
|
|
87
|
+
this.maxRoomNameLength = positiveInteger(options.maxRoomNameLength, 256);
|
|
88
|
+
this.maxOfflineBroadcastRecipients = positiveInteger(options.maxOfflineBroadcastRecipients, 1_000);
|
|
89
|
+
this.maxOfflineBroadcastBytes = positiveInteger(options.maxOfflineBroadcastBytes, 1024 * 1024);
|
|
90
|
+
this.activeLeaseMs = positiveInteger(options.activeLeaseMs, 30_000);
|
|
91
|
+
this.adapter = options.adapter ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
createSession(
|
|
95
|
+
sid: string,
|
|
96
|
+
namespace: string,
|
|
97
|
+
context: SocketContext,
|
|
98
|
+
ttlMs?: number,
|
|
99
|
+
replayDelivery: ReplayDelivery = "transport",
|
|
100
|
+
): SessionState {
|
|
101
|
+
this.purgeExpired();
|
|
102
|
+
this.ensureSessionCapacity(sid);
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const resolvedTtl = positiveInteger(ttlMs, this.defaultTtlMs);
|
|
105
|
+
const state: SessionState = {
|
|
106
|
+
sid,
|
|
107
|
+
namespace,
|
|
108
|
+
context: cloneSocketContext(context),
|
|
109
|
+
rooms: [],
|
|
110
|
+
createdAt: now,
|
|
111
|
+
creationToken: issueSessionCreationToken(),
|
|
112
|
+
lastSeenAt: now,
|
|
113
|
+
expiresAt: now + resolvedTtl,
|
|
114
|
+
outboundQueue: [],
|
|
115
|
+
revision: 1,
|
|
116
|
+
ttlMs: resolvedTtl,
|
|
117
|
+
outboundQueueBytes: 0,
|
|
118
|
+
...(replayDelivery === "client-ack" ? { replayDelivery } : {}),
|
|
119
|
+
};
|
|
120
|
+
const previous = this.sessions.get(sid);
|
|
121
|
+
if (previous) {
|
|
122
|
+
this.removeRoomIndex(previous);
|
|
123
|
+
}
|
|
124
|
+
this.sessions.set(sid, state);
|
|
125
|
+
this.addRoomIndex(state);
|
|
126
|
+
this.enqueueSave(state);
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
hasCapacity(): boolean {
|
|
131
|
+
this.purgeExpired();
|
|
132
|
+
return this.sessions.size < this.maxSessions;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getSession(sid: string): SessionState | undefined {
|
|
136
|
+
this.purgeExpired();
|
|
137
|
+
const state = this.sessions.get(sid);
|
|
138
|
+
if (!state) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
this.expireStaleLease(state);
|
|
142
|
+
if (Date.now() > state.expiresAt && !this.isActive(state)) {
|
|
143
|
+
this.removeLocalSession(state);
|
|
144
|
+
this.enqueueDelete(sid, (state.revision ?? 0) + 1);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return state;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async resolveSession(sid: string, signal?: AbortSignal): Promise<SessionState | undefined> {
|
|
151
|
+
signal?.throwIfAborted();
|
|
152
|
+
await this.flushSession(sid);
|
|
153
|
+
signal?.throwIfAborted();
|
|
154
|
+
let local = this.sessions.get(sid);
|
|
155
|
+
if (local) {
|
|
156
|
+
local = await this.refreshLocalFromAdapter(local, signal);
|
|
157
|
+
signal?.throwIfAborted();
|
|
158
|
+
this.expireStaleLease(local);
|
|
159
|
+
if (Date.now() > local.expiresAt && !this.isActive(local)) {
|
|
160
|
+
this.removeLocalSession(local);
|
|
161
|
+
this.enqueueDelete(sid, (local.revision ?? 0) + 1);
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
return local;
|
|
165
|
+
}
|
|
166
|
+
if (!this.adapter) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const loaded = await this.adapter.loadSession(sid);
|
|
170
|
+
signal?.throwIfAborted();
|
|
171
|
+
if (!loaded) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const remote = this.normalizeLoadedState(loaded);
|
|
175
|
+
this.expireStaleLease(remote);
|
|
176
|
+
if (Date.now() > remote.expiresAt && !this.isActive(remote)) {
|
|
177
|
+
await this.adapter.deleteSession(sid, (remote.revision ?? 0) + 1);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
this.ensureSessionCapacity(sid);
|
|
181
|
+
this.sessions.set(sid, remote);
|
|
182
|
+
this.addRoomIndex(remote);
|
|
183
|
+
return remote;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
touchSession(sid: string, connId?: string): boolean {
|
|
187
|
+
const state = this.sessions.get(sid);
|
|
188
|
+
if (!state) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (connId !== undefined && (state.activeConnId !== connId || !this.isActive(state, now))) {
|
|
193
|
+
// Once the local lease has elapsed, another node may already have claimed
|
|
194
|
+
// the SID. Never extend it; discard this socket's stale local owner state.
|
|
195
|
+
if (state.activeConnId === connId) {
|
|
196
|
+
this.removeLocalSession(state);
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (connId === undefined) {
|
|
201
|
+
this.purgeExpired();
|
|
202
|
+
if (this.sessions.get(sid) !== state) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (connId !== undefined) {
|
|
207
|
+
// Traffic is not a reason to fsync the entire session on every frame.
|
|
208
|
+
// Keep the persisted expiry/lease unchanged until renewal is due, so a
|
|
209
|
+
// local touch can never claim a longer lease than the store has accepted.
|
|
210
|
+
const ttl = this.ttlOf(state);
|
|
211
|
+
const refreshedAt = Math.min(state.expiresAt - ttl, state.activeLeaseExpiresAt! - this.activeLeaseMs);
|
|
212
|
+
if (now - refreshedAt < Math.max(1, Math.floor(Math.min(ttl, this.activeLeaseMs) / 3))) {
|
|
213
|
+
state.lastSeenAt = now;
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
state.lastSeenAt = now;
|
|
218
|
+
state.expiresAt = now + this.ttlOf(state);
|
|
219
|
+
if (state.activeConnId) {
|
|
220
|
+
state.activeLeaseExpiresAt = now + this.activeLeaseMs;
|
|
221
|
+
}
|
|
222
|
+
this.bumpAndSave(state);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Claims one active physical connection for a SID. A live lease owned by a
|
|
228
|
+
* different adapter instance is rejected; same-node replacement is allowed
|
|
229
|
+
* so the server can close the previous socket deterministically.
|
|
230
|
+
*/
|
|
231
|
+
async claimActiveConnection(sid: string, connId: string): Promise<boolean> {
|
|
232
|
+
await this.flushSession(sid);
|
|
233
|
+
let state = this.sessions.get(sid);
|
|
234
|
+
if (!state) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
state = await this.refreshLocalFromAdapter(state);
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
this.expireStaleLease(state, now);
|
|
240
|
+
if (now > state.expiresAt && !this.isActive(state, now)) {
|
|
241
|
+
this.removeLocalSession(state);
|
|
242
|
+
this.enqueueDelete(sid, (state.revision ?? 0) + 1);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
this.isActive(state, now)
|
|
247
|
+
&& state.activeConnId !== connId
|
|
248
|
+
&& state.ownerInstanceId !== this.adapter?.instanceId
|
|
249
|
+
) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const expectedRevision = state.revision ?? 0;
|
|
254
|
+
const previous = {
|
|
255
|
+
activeConnId: state.activeConnId,
|
|
256
|
+
ownerInstanceId: state.ownerInstanceId,
|
|
257
|
+
activeLeaseExpiresAt: state.activeLeaseExpiresAt,
|
|
258
|
+
lastSeenAt: state.lastSeenAt,
|
|
259
|
+
expiresAt: state.expiresAt,
|
|
260
|
+
revision: state.revision,
|
|
261
|
+
};
|
|
262
|
+
state.activeConnId = connId;
|
|
263
|
+
state.ownerInstanceId = this.adapter?.instanceId ?? "local";
|
|
264
|
+
state.activeLeaseExpiresAt = now + this.activeLeaseMs;
|
|
265
|
+
state.lastSeenAt = now;
|
|
266
|
+
state.expiresAt = now + this.ttlOf(state);
|
|
267
|
+
state.revision = expectedRevision + 1;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const claimed = await this.enqueueSave(state, expectedRevision);
|
|
271
|
+
if (claimed) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
Object.assign(state, previous);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Object.assign(state, previous);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Atomically snapshots rooms and releases the active connection lease. */
|
|
284
|
+
async releaseActiveConnection(
|
|
285
|
+
sid: string,
|
|
286
|
+
expectedConnId: string,
|
|
287
|
+
rooms?: Iterable<string>,
|
|
288
|
+
): Promise<boolean> {
|
|
289
|
+
await this.flushSession(sid);
|
|
290
|
+
let state = this.sessions.get(sid);
|
|
291
|
+
if (!state) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
state = await this.refreshLocalFromAdapter(state);
|
|
295
|
+
if (state.activeConnId !== expectedConnId) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const expectedRevision = state.revision ?? 0;
|
|
300
|
+
if (rooms !== undefined) {
|
|
301
|
+
const previousRooms = state.rooms;
|
|
302
|
+
state.rooms = this.normalizeRooms(rooms);
|
|
303
|
+
this.replaceRoomIndex(state, previousRooms);
|
|
304
|
+
}
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
state.activeConnId = undefined;
|
|
307
|
+
state.ownerInstanceId = undefined;
|
|
308
|
+
state.activeLeaseExpiresAt = undefined;
|
|
309
|
+
state.lastSeenAt = now;
|
|
310
|
+
state.expiresAt = now + this.ttlOf(state);
|
|
311
|
+
state.revision = expectedRevision + 1;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const released = await this.enqueueSave(state, expectedRevision);
|
|
315
|
+
if (!released && this.sessions.get(sid) === state) {
|
|
316
|
+
this.removeLocalSession(state);
|
|
317
|
+
}
|
|
318
|
+
return released;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (this.sessions.get(sid) === state) {
|
|
321
|
+
this.removeLocalSession(state);
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
updateRooms(sid: string, rooms: Iterable<string>): void {
|
|
328
|
+
this.purgeExpired();
|
|
329
|
+
const state = this.sessions.get(sid);
|
|
330
|
+
if (!state) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const previousRooms = state.rooms;
|
|
334
|
+
state.rooms = this.normalizeRooms(rooms);
|
|
335
|
+
state.lastSeenAt = Date.now();
|
|
336
|
+
this.replaceRoomIndex(state, previousRooms);
|
|
337
|
+
this.bumpAndSave(state);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
updateContext(sid: string, context: SocketContext): void {
|
|
341
|
+
this.purgeExpired();
|
|
342
|
+
const state = this.sessions.get(sid);
|
|
343
|
+
if (!state) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
state.context = cloneSocketContext(context);
|
|
347
|
+
state.lastSeenAt = Date.now();
|
|
348
|
+
this.bumpAndSave(state);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
enqueueOutbound(sid: string, packet: ServerPacket, _packetBytes?: number): boolean {
|
|
352
|
+
this.purgeExpired();
|
|
353
|
+
const state = this.sessions.get(sid);
|
|
354
|
+
if (!state) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return this.enqueueOutboundState(state, packet);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Queues a broadcast by indexed room membership, never by scanning all sessions. */
|
|
361
|
+
enqueueToOfflineRoomMembers(
|
|
362
|
+
namespace: string,
|
|
363
|
+
room: string,
|
|
364
|
+
packet: ServerPacket,
|
|
365
|
+
excludeSid?: string,
|
|
366
|
+
_packetBytes?: number,
|
|
367
|
+
allOrNothing = false,
|
|
368
|
+
): OfflineBroadcastResult {
|
|
369
|
+
this.purgeExpired();
|
|
370
|
+
// Queue memory/persistence is the serialized packet object, independent of
|
|
371
|
+
// the live wire codec used for the immediate broadcast.
|
|
372
|
+
const packetBytes = serializedBytes(packet);
|
|
373
|
+
const members = this.roomMembers.get(roomKey(namespace, room));
|
|
374
|
+
if (!members || packetBytes > this.maxOfflineBroadcastBytes) {
|
|
375
|
+
return { queued: 0, bytes: 0, truncated: packetBytes > this.maxOfflineBroadcastBytes };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let queued = 0;
|
|
379
|
+
let bytes = 0;
|
|
380
|
+
let truncated = false;
|
|
381
|
+
if (allOrNothing) {
|
|
382
|
+
const targets: SessionState[] = [];
|
|
383
|
+
let total = 0;
|
|
384
|
+
for (const sid of members) {
|
|
385
|
+
const state = this.sessions.get(sid);
|
|
386
|
+
if (!state || this.isActive(state) || state.namespace !== namespace || sid === excludeSid) continue;
|
|
387
|
+
const added = serializedBytes(state.replayDelivery === "client-ack"
|
|
388
|
+
? { ...packet, deliveryId: "00000000-0000-4000-8000-000000000000" } : packet);
|
|
389
|
+
if (targets.length >= this.maxOfflineBroadcastRecipients || total + added > this.maxOfflineBroadcastBytes
|
|
390
|
+
|| state.outboundQueue.length >= this.maxOutboundQueue || (state.outboundQueueBytes ?? 0) + added > this.maxOutboundQueueBytes) {
|
|
391
|
+
return { queued: 0, bytes: 0, truncated: true };
|
|
392
|
+
}
|
|
393
|
+
targets.push(state); total += added;
|
|
394
|
+
}
|
|
395
|
+
for (const state of targets) this.enqueueOutboundState(state, packet);
|
|
396
|
+
return { queued: targets.length, bytes: total, truncated: false };
|
|
397
|
+
}
|
|
398
|
+
for (const sid of members) {
|
|
399
|
+
const state = this.sessions.get(sid);
|
|
400
|
+
if (!state || this.isActive(state) || state.namespace !== namespace || sid === excludeSid) continue;
|
|
401
|
+
if (queued >= this.maxOfflineBroadcastRecipients || bytes + packetBytes > this.maxOfflineBroadcastBytes) {
|
|
402
|
+
truncated = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
if (this.enqueueOutboundState(state, packet)) {
|
|
406
|
+
queued += 1;
|
|
407
|
+
bytes += packetBytes;
|
|
408
|
+
} else {
|
|
409
|
+
truncated = true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { queued, bytes, truncated };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Snapshot for transport encoding; reading never consumes replay. */
|
|
416
|
+
peekOutbound(sid: string): readonly ServerPacket[] {
|
|
417
|
+
return this.getSession(sid)?.outboundQueue.slice() ?? [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Switches the session to client-acknowledged replay: assigns a delivery id to
|
|
422
|
+
* every queued packet (within the queue byte limit) so the client can ack them.
|
|
423
|
+
*/
|
|
424
|
+
enableReplayAcknowledgements(sid: string): void {
|
|
425
|
+
const state = this.sessions.get(sid);
|
|
426
|
+
if (!state || state.replayDelivery === "client-ack") return;
|
|
427
|
+
const queue = state.outboundQueue.map((packet) => ({ ...packet, deliveryId: crypto.randomUUID() }));
|
|
428
|
+
const bytes = queue.reduce((total, packet) => total + serializedBytes(packet), 0);
|
|
429
|
+
if (bytes > this.maxOutboundQueueBytes) throw new Error("WebSocket replay identities exceed the queue byte limit.");
|
|
430
|
+
state.replayDelivery = "client-ack";
|
|
431
|
+
state.outboundQueue = queue;
|
|
432
|
+
state.outboundQueueBytes = bytes;
|
|
433
|
+
this.bumpAndSave(state);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The server must first validate IDs against replay issued to this connection. */
|
|
437
|
+
acknowledgeDeliveries(sid: string, deliveryIds: readonly string[]): number {
|
|
438
|
+
const state = this.sessions.get(sid);
|
|
439
|
+
if (!state || state.replayDelivery !== "client-ack") return 0;
|
|
440
|
+
const ids = new Set(deliveryIds);
|
|
441
|
+
const remaining = state.outboundQueue.filter((packet) => !packet.deliveryId || !ids.has(packet.deliveryId));
|
|
442
|
+
const removed = state.outboundQueue.length - remaining.length;
|
|
443
|
+
if (removed > 0) {
|
|
444
|
+
state.outboundQueue = remaining;
|
|
445
|
+
state.outboundQueueBytes = remaining.reduce((bytes, packet) => bytes + serializedBytes(packet), 0);
|
|
446
|
+
this.bumpAndSave(state);
|
|
447
|
+
}
|
|
448
|
+
return removed;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Remove only a prefix that the transport has accepted, without an intervening await. */
|
|
452
|
+
acknowledgeOutbound(sid: string, count: number): void {
|
|
453
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
454
|
+
throw new Error("WebSocket replay acknowledgement count must be a non-negative integer.");
|
|
455
|
+
}
|
|
456
|
+
const state = this.sessions.get(sid);
|
|
457
|
+
if (state?.replayDelivery === "client-ack" && count > 0) {
|
|
458
|
+
throw new Error("Client-ack replay requires delivery identities, not a transport prefix.");
|
|
459
|
+
}
|
|
460
|
+
if (!state || count === 0) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (count > state.outboundQueue.length) {
|
|
464
|
+
throw new Error("WebSocket replay acknowledgement exceeds the queued prefix.");
|
|
465
|
+
}
|
|
466
|
+
const removed = state.outboundQueue.splice(0, count);
|
|
467
|
+
state.outboundQueueBytes = Math.max(0,
|
|
468
|
+
(state.outboundQueueBytes ?? 0) - removed.reduce((bytes, packet) => bytes + serializedBytes(packet), 0));
|
|
469
|
+
this.bumpAndSave(state);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
drainOutbound(sid: string, maxBytes = Number.POSITIVE_INFINITY): ServerPacket[] {
|
|
473
|
+
this.purgeExpired();
|
|
474
|
+
const state = this.sessions.get(sid);
|
|
475
|
+
if (!state) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
const missed: ServerPacket[] = [];
|
|
479
|
+
let drainedBytes = 0;
|
|
480
|
+
for (const next of state.outboundQueue) {
|
|
481
|
+
const bytes = serializedBytes(next);
|
|
482
|
+
if (drainedBytes + bytes > maxBytes) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
missed.push(next);
|
|
486
|
+
drainedBytes += bytes;
|
|
487
|
+
}
|
|
488
|
+
this.acknowledgeOutbound(sid, missed.length);
|
|
489
|
+
return missed;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
deleteSession(sid: string): void {
|
|
493
|
+
const state = this.sessions.get(sid);
|
|
494
|
+
if (state) {
|
|
495
|
+
this.removeLocalSession(state);
|
|
496
|
+
}
|
|
497
|
+
this.enqueueDelete(sid, state === undefined ? undefined : (state.revision ?? 0) + 1);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
getStats(): { sessions: number; queuedMessages: number } {
|
|
501
|
+
this.purgeExpired();
|
|
502
|
+
let queuedMessages = 0;
|
|
503
|
+
for (const state of this.sessions.values()) {
|
|
504
|
+
queuedMessages += state.outboundQueue.length;
|
|
505
|
+
}
|
|
506
|
+
return { sessions: this.sessions.size, queuedMessages };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
purgeExpired(): number {
|
|
510
|
+
const now = Date.now();
|
|
511
|
+
let purged = 0;
|
|
512
|
+
for (const state of this.sessions.values()) {
|
|
513
|
+
this.expireStaleLease(state, now);
|
|
514
|
+
if (now > state.expiresAt && !this.isActive(state, now)) {
|
|
515
|
+
this.removeLocalSession(state);
|
|
516
|
+
this.enqueueDelete(state.sid, (state.revision ?? 0) + 1);
|
|
517
|
+
purged += 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return purged;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Renews leases owned by this adapter/node; called by the server heartbeat.
|
|
525
|
+
* Returns SIDs whose optimistic renewal lost to a newer distributed owner.
|
|
526
|
+
*/
|
|
527
|
+
async renewOwnedLeases(schedule?: { afterMs: number; spreadMs: number }): Promise<string[]> {
|
|
528
|
+
if (schedule && (!Number.isSafeInteger(schedule.afterMs) || schedule.afterMs < 0
|
|
529
|
+
|| !Number.isSafeInteger(schedule.spreadMs) || schedule.spreadMs < 0)) throw new Error("Invalid lease renewal schedule.");
|
|
530
|
+
const owner = this.adapter?.instanceId ?? "local";
|
|
531
|
+
const states = [...this.sessions.values()];
|
|
532
|
+
const lostOwnership: string[] = [];
|
|
533
|
+
let cursor = 0;
|
|
534
|
+
// Bounded native I/O, while each SID still enters its ordered persistence
|
|
535
|
+
// chain immediately after mutation. Delaying only the save would reorder it
|
|
536
|
+
// behind concurrent message updates.
|
|
537
|
+
const worker = async (): Promise<void> => {
|
|
538
|
+
while (cursor < states.length) {
|
|
539
|
+
const state = states[cursor++]!;
|
|
540
|
+
if (this.sessions.get(state.sid) !== state || !state.activeConnId || state.ownerInstanceId !== owner) continue;
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
if (!this.isActive(state, now)) {
|
|
543
|
+
this.removeLocalSession(state);
|
|
544
|
+
lostOwnership.push(state.sid);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (schedule) {
|
|
548
|
+
const lifetime = Math.min(this.activeLeaseMs, this.ttlOf(state));
|
|
549
|
+
const after = Math.min(schedule.afterMs, Math.floor(lifetime / 3));
|
|
550
|
+
const spread = Math.min(schedule.spreadMs, Math.floor((lifetime - after) / 2));
|
|
551
|
+
let hash = 2166136261;
|
|
552
|
+
for (let i = 0; i < state.sid.length; i++) hash = Math.imul(hash ^ state.sid.charCodeAt(i), 16777619) >>> 0;
|
|
553
|
+
const refreshedAt = Math.min(state.activeLeaseExpiresAt! - this.activeLeaseMs, state.expiresAt - this.ttlOf(state));
|
|
554
|
+
if (now < refreshedAt + after + hash % (spread + 1)) continue;
|
|
555
|
+
}
|
|
556
|
+
const expectedRevision = state.revision ?? 0;
|
|
557
|
+
state.activeLeaseExpiresAt = now + this.activeLeaseMs;
|
|
558
|
+
state.lastSeenAt = now;
|
|
559
|
+
state.expiresAt = now + this.ttlOf(state);
|
|
560
|
+
state.revision = expectedRevision + 1;
|
|
561
|
+
try {
|
|
562
|
+
if (!await this.enqueueSave(state, expectedRevision) && this.sessions.get(state.sid) === state) {
|
|
563
|
+
this.removeLocalSession(state);
|
|
564
|
+
lostOwnership.push(state.sid);
|
|
565
|
+
}
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (this.sessions.get(state.sid) === state) this.removeLocalSession(state);
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(16, states.length) }, worker));
|
|
573
|
+
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
574
|
+
return lostOwnership;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async flushSession(sid: string): Promise<void> {
|
|
578
|
+
const tail = this.persistenceTails.get(sid);
|
|
579
|
+
if (tail) {
|
|
580
|
+
await tail.catch(() => {});
|
|
581
|
+
}
|
|
582
|
+
if (this.persistenceErrors.has(sid)) {
|
|
583
|
+
const error = this.persistenceErrors.get(sid);
|
|
584
|
+
this.persistenceErrors.delete(sid);
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async flushAll(): Promise<void> {
|
|
590
|
+
const sids = new Set([...this.persistenceTails.keys(), ...this.persistenceErrors.keys()]);
|
|
591
|
+
const errors: unknown[] = [];
|
|
592
|
+
for (const sid of sids) {
|
|
593
|
+
try {
|
|
594
|
+
await this.flushSession(sid);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
errors.push(error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (errors.length > 0) {
|
|
600
|
+
throw errors[0];
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private normalizeLoadedState(state: SessionState): SessionState {
|
|
605
|
+
const ttlMs = positiveInteger(state.ttlMs, this.defaultTtlMs);
|
|
606
|
+
if (state.replayDelivery === "client-ack" && (
|
|
607
|
+
!Array.isArray(state.outboundQueue)
|
|
608
|
+
|| state.outboundQueue.length > this.maxOutboundQueue
|
|
609
|
+
|| state.outboundQueue.some((packet) => typeof packet.deliveryId !== "string")
|
|
610
|
+
|| new Set(state.outboundQueue.map((packet) => packet.deliveryId)).size !== state.outboundQueue.length
|
|
611
|
+
|| state.outboundQueue.reduce((bytes, packet) => bytes + serializedBytes(packet), 0) > this.maxOutboundQueueBytes
|
|
612
|
+
)) throw new Error("Stored client-ack replay violates queue limits or identities.");
|
|
613
|
+
const outboundQueue = Array.isArray(state.outboundQueue)
|
|
614
|
+
? [...state.outboundQueue].slice(-this.maxOutboundQueue)
|
|
615
|
+
: [];
|
|
616
|
+
let outboundQueueBytes = outboundQueue.reduce(
|
|
617
|
+
(total, packet) => total + serializedBytes(packet),
|
|
618
|
+
0,
|
|
619
|
+
);
|
|
620
|
+
if (!Number.isFinite(outboundQueueBytes)) {
|
|
621
|
+
outboundQueue.length = 0;
|
|
622
|
+
outboundQueueBytes = 0;
|
|
623
|
+
}
|
|
624
|
+
while (outboundQueue.length > 0 && outboundQueueBytes > this.maxOutboundQueueBytes) {
|
|
625
|
+
const removed = outboundQueue.shift();
|
|
626
|
+
if (removed !== undefined) {
|
|
627
|
+
outboundQueueBytes -= serializedBytes(removed);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
...state,
|
|
632
|
+
context: cloneSocketContext(state.context ?? {}),
|
|
633
|
+
rooms: Array.isArray(state.rooms) ? this.normalizeRooms(state.rooms) : [],
|
|
634
|
+
outboundQueue,
|
|
635
|
+
revision: nonNegativeInteger(state.revision, 0),
|
|
636
|
+
ttlMs,
|
|
637
|
+
outboundQueueBytes: Math.max(0, outboundQueueBytes),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private async refreshLocalFromAdapter(state: SessionState, signal?: AbortSignal): Promise<SessionState> {
|
|
642
|
+
if (!this.adapter) {
|
|
643
|
+
return state;
|
|
644
|
+
}
|
|
645
|
+
const loaded = await this.adapter.loadSession(state.sid);
|
|
646
|
+
signal?.throwIfAborted();
|
|
647
|
+
if (!loaded) {
|
|
648
|
+
return state;
|
|
649
|
+
}
|
|
650
|
+
if (loaded.sid !== state.sid) {
|
|
651
|
+
throw new Error("WebSocket adapter returned a mismatched session id.");
|
|
652
|
+
}
|
|
653
|
+
const remote = this.normalizeLoadedState(loaded);
|
|
654
|
+
if ((remote.revision ?? 0) < (state.revision ?? 0)) {
|
|
655
|
+
return state;
|
|
656
|
+
}
|
|
657
|
+
const previousRooms = state.rooms;
|
|
658
|
+
Object.assign(state, remote);
|
|
659
|
+
this.replaceRoomIndex(state, previousRooms);
|
|
660
|
+
return state;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private normalizeRooms(rooms: Iterable<unknown>): string[] {
|
|
664
|
+
const normalized: string[] = [];
|
|
665
|
+
const seen = new Set<string>();
|
|
666
|
+
for (const room of rooms) {
|
|
667
|
+
if (
|
|
668
|
+
typeof room !== "string"
|
|
669
|
+
|| room.length === 0
|
|
670
|
+
|| room.length > this.maxRoomNameLength
|
|
671
|
+
|| room.includes(ROOM_SEPARATOR)
|
|
672
|
+
|| seen.has(room)
|
|
673
|
+
) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
seen.add(room);
|
|
677
|
+
normalized.push(room);
|
|
678
|
+
if (normalized.length >= this.maxRoomsPerSession) {
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return normalized;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private enqueueOutboundState(state: SessionState, original: ServerPacket): boolean {
|
|
686
|
+
const packet = state.replayDelivery === "client-ack"
|
|
687
|
+
? { ...original, deliveryId: crypto.randomUUID() } : original;
|
|
688
|
+
const packetBytes = serializedBytes(packet);
|
|
689
|
+
if (packetBytes > this.maxOutboundQueueBytes) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
if (state.replayDelivery === "client-ack" && (
|
|
693
|
+
state.outboundQueue.length >= this.maxOutboundQueue
|
|
694
|
+
|| (state.outboundQueueBytes ?? 0) + packetBytes > this.maxOutboundQueueBytes
|
|
695
|
+
)) return false;
|
|
696
|
+
state.outboundQueue.push(packet);
|
|
697
|
+
state.outboundQueueBytes = (state.outboundQueueBytes ?? 0) + packetBytes;
|
|
698
|
+
while (
|
|
699
|
+
state.outboundQueue.length > this.maxOutboundQueue
|
|
700
|
+
|| (state.outboundQueueBytes ?? 0) > this.maxOutboundQueueBytes
|
|
701
|
+
) {
|
|
702
|
+
const removed = state.outboundQueue.shift();
|
|
703
|
+
if (removed !== undefined) {
|
|
704
|
+
state.outboundQueueBytes = Math.max(0, (state.outboundQueueBytes ?? 0) - serializedBytes(removed));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
state.lastSeenAt = Date.now();
|
|
708
|
+
this.bumpAndSave(state);
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private bumpAndSave(state: SessionState): void {
|
|
713
|
+
const expectedRevision = state.revision ?? 0;
|
|
714
|
+
state.revision = expectedRevision + 1;
|
|
715
|
+
this.enqueueSave(state, expectedRevision, true);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private enqueueSave(state: SessionState, expectedRevision?: number, requireMatch = false): Promise<boolean> {
|
|
719
|
+
if (!this.adapter) {
|
|
720
|
+
return Promise.resolve(true);
|
|
721
|
+
}
|
|
722
|
+
const snapshot = structuredClone(state);
|
|
723
|
+
return this.enqueuePersistence(state.sid, async () => {
|
|
724
|
+
if (expectedRevision !== undefined && this.adapter?.compareAndSwapSession) {
|
|
725
|
+
const saved = await this.adapter.compareAndSwapSession(snapshot, expectedRevision);
|
|
726
|
+
if (!saved && requireMatch) {
|
|
727
|
+
if (this.sessions.get(state.sid) === state) this.removeLocalSession(state);
|
|
728
|
+
throw new Error("WebSocket session revision conflict.");
|
|
729
|
+
}
|
|
730
|
+
return saved;
|
|
731
|
+
}
|
|
732
|
+
await this.adapter?.saveSession(snapshot);
|
|
733
|
+
return true;
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private enqueueDelete(sid: string, revision?: number): void {
|
|
738
|
+
if (!this.adapter) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
void this.enqueuePersistence(sid, async () => {
|
|
742
|
+
await this.adapter?.deleteSession(sid, revision);
|
|
743
|
+
return true;
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private enqueuePersistence<T>(sid: string, operation: () => Promise<T>): Promise<T> {
|
|
748
|
+
const previous = this.persistenceTails.get(sid) ?? Promise.resolve();
|
|
749
|
+
const next = previous.catch(() => {}).then(operation);
|
|
750
|
+
const tail = next.then(() => undefined);
|
|
751
|
+
this.persistenceTails.set(sid, tail);
|
|
752
|
+
void tail.catch((error: unknown) => {
|
|
753
|
+
this.persistenceErrors.set(sid, error);
|
|
754
|
+
});
|
|
755
|
+
void tail.finally(() => {
|
|
756
|
+
if (this.persistenceTails.get(sid) === tail) {
|
|
757
|
+
this.persistenceTails.delete(sid);
|
|
758
|
+
}
|
|
759
|
+
}).catch(() => {});
|
|
760
|
+
return next;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private expireStaleLease(state: SessionState, now = Date.now()): void {
|
|
764
|
+
if (!state.activeConnId) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const leaseExpiresAt = state.activeLeaseExpiresAt ?? state.lastSeenAt + this.activeLeaseMs;
|
|
768
|
+
if (now <= leaseExpiresAt) {
|
|
769
|
+
state.activeLeaseExpiresAt = leaseExpiresAt;
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
state.activeConnId = undefined;
|
|
773
|
+
state.ownerInstanceId = undefined;
|
|
774
|
+
state.activeLeaseExpiresAt = undefined;
|
|
775
|
+
this.bumpAndSave(state);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private isActive(state: SessionState, now = Date.now()): boolean {
|
|
779
|
+
return state.activeConnId !== undefined && (state.activeLeaseExpiresAt ?? 0) > now;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private ttlOf(state: SessionState): number {
|
|
783
|
+
return positiveInteger(state.ttlMs, this.defaultTtlMs);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private ensureSessionCapacity(sid?: string): void {
|
|
787
|
+
if ((sid !== undefined && this.sessions.has(sid)) || this.sessions.size < this.maxSessions) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
throw new SessionCapacityError(this.maxSessions);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private replaceRoomIndex(state: SessionState, previousRooms: readonly string[]): void {
|
|
794
|
+
this.removeRoomsFromIndex(state.sid, state.namespace, previousRooms);
|
|
795
|
+
this.addRoomIndex(state);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private addRoomIndex(state: SessionState): void {
|
|
799
|
+
for (const room of state.rooms) {
|
|
800
|
+
const key = roomKey(state.namespace, room);
|
|
801
|
+
let members = this.roomMembers.get(key);
|
|
802
|
+
if (!members) {
|
|
803
|
+
members = new Set<string>();
|
|
804
|
+
this.roomMembers.set(key, members);
|
|
805
|
+
}
|
|
806
|
+
members.add(state.sid);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private removeRoomIndex(state: SessionState): void {
|
|
811
|
+
this.removeRoomsFromIndex(state.sid, state.namespace, state.rooms);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private removeRoomsFromIndex(sid: string, namespace: string, rooms: readonly string[]): void {
|
|
815
|
+
for (const room of rooms) {
|
|
816
|
+
const key = roomKey(namespace, room);
|
|
817
|
+
const members = this.roomMembers.get(key);
|
|
818
|
+
members?.delete(sid);
|
|
819
|
+
if (members?.size === 0) {
|
|
820
|
+
this.roomMembers.delete(key);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private removeLocalSession(state: SessionState): void {
|
|
826
|
+
this.sessions.delete(state.sid);
|
|
827
|
+
this.removeRoomIndex(state);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function roomKey(namespace: string, room: string): string {
|
|
832
|
+
return `${namespace}${ROOM_SEPARATOR}${room}`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function serializedBytes(value: unknown): number {
|
|
836
|
+
try {
|
|
837
|
+
return encoder.encode(JSON.stringify(value)).byteLength;
|
|
838
|
+
} catch {
|
|
839
|
+
return Number.POSITIVE_INFINITY;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function cloneSocketContext(context: SocketContext): SocketContext {
|
|
844
|
+
return {
|
|
845
|
+
...context,
|
|
846
|
+
...(context.user ? { user: { ...context.user } } : {}),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
851
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function nonNegativeInteger(value: number | undefined, fallback: number): number {
|
|
855
|
+
return value !== undefined && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
856
|
+
}
|