@plitzi/sdk-server 0.37.1 → 0.37.3
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/CHANGELOG.md +1296 -0
- package/README.md +126 -7
- package/dist/actions.d.ts +20 -8
- package/dist/actions.js +11 -4
- package/dist/adapters/cloudAdapters.js +4 -3
- package/dist/adapters/jsonAdapters.js +28 -6
- package/dist/auth.d.ts +7 -4
- package/dist/auth.js +8 -6
- package/dist/core/auth/api.d.ts +29 -1
- package/dist/core/auth/api.js +47 -13
- package/dist/core/auth/createAuth.d.ts +4 -0
- package/dist/core/auth/createAuth.js +3 -3
- package/dist/core/auth/credentials.d.ts +3 -0
- package/dist/core/auth/credentials.js +1 -1
- package/dist/core/auth/csrf.d.ts +25 -1
- package/dist/core/auth/csrf.js +29 -4
- package/dist/core/auth/renewal.d.ts +26 -0
- package/dist/core/auth/renewal.js +24 -0
- package/dist/core/auth/routes.js +8 -6
- package/dist/core/auth/session.d.ts +45 -0
- package/dist/core/auth/session.js +47 -1
- package/dist/core/auth/sessionActivity.d.ts +11 -0
- package/dist/core/auth/sessionActivity.js +14 -0
- package/dist/core/auth/throttle.d.ts +15 -0
- package/dist/core/auth/throttle.js +29 -1
- package/dist/core/auth/totp.d.ts +9 -1
- package/dist/core/auth/totp.js +11 -6
- package/dist/core/createServer.actions.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +14 -1
- package/dist/core/createServer.functions.test.d.ts +1 -0
- package/dist/core/createServer.js +22 -12
- package/dist/core/fontStore.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/authRouteHandlers.js +57 -21
- package/dist/core/handlers/csrfMiddleware.d.ts +23 -0
- package/dist/core/handlers/csrfMiddleware.js +31 -7
- package/dist/core/handlers/csrfMiddleware.test.d.ts +1 -0
- package/dist/core/handlers/oauthRouteHandlers.js +5 -3
- package/dist/core/handlers/socialRouteHandlers.d.ts +1 -1
- package/dist/core/handlers/socialRouteHandlers.js +2 -1
- package/dist/core/http/dispatcher.d.ts +9 -1
- package/dist/core/http/dispatcher.js +38 -11
- package/dist/core/http/navigation.d.ts +5 -2
- package/dist/core/http/navigation.js +7 -4
- package/dist/core/http/socketResponse.d.ts +18 -0
- package/dist/core/http/socketResponse.js +59 -0
- package/dist/core/http/sse.d.ts +26 -0
- package/dist/core/http/sse.js +78 -0
- package/dist/core/http/stages/authRoutes.js +3 -1
- package/dist/core/http/stages/exchangeRoute.test.d.ts +1 -0
- package/dist/core/http/stages/middlewares.d.ts +1 -1
- package/dist/core/http/stages/middlewares.js +16 -10
- package/dist/core/http/stages/middlewares.test.d.ts +1 -0
- package/dist/core/http/stages/oauth.js +2 -2
- package/dist/core/http/stages/sessionRenewal.d.ts +16 -0
- package/dist/core/http/stages/sessionRenewal.js +81 -0
- package/dist/core/http/stages/sessionRenewal.test.d.ts +1 -0
- package/dist/core/http/stages/signInRoute.d.ts +2 -0
- package/dist/core/http/stages/signInRoute.js +139 -0
- package/dist/core/http/stages/signInRoute.test.d.ts +1 -0
- package/dist/core/http/types.d.ts +9 -0
- package/dist/core/http/webExchange.d.ts +18 -0
- package/dist/core/http/webExchange.js +53 -0
- package/dist/core/oauth/authorize.js +37 -13
- package/dist/core/oauth/consentPage.js +11 -0
- package/dist/core/oauth/issueContext.d.ts +14 -0
- package/dist/core/oauth/issueContext.js +32 -0
- package/dist/core/oauth/records.d.ts +7 -0
- package/dist/core/oauth/register.d.ts +2 -0
- package/dist/core/oauth/register.js +32 -11
- package/dist/core/oauth/register.test.d.ts +1 -0
- package/dist/core/oauth/token.js +21 -6
- package/dist/core/oauth/token.test.d.ts +1 -0
- package/dist/core/requestParser.d.ts +4 -0
- package/dist/core/requestParser.js +14 -7
- package/dist/core/sdkAssets.js +1 -1
- package/dist/core/server/baseServer.d.ts +14 -1
- package/dist/core/server/baseServer.js +43 -11
- package/dist/core/server/closeOnSignals.d.ts +28 -0
- package/dist/core/server/closeOnSignals.js +47 -0
- package/dist/core/server/closeOnSignals.test.d.ts +1 -0
- package/dist/core/server/drain.d.ts +18 -0
- package/dist/core/server/drain.js +62 -0
- package/dist/core/server/drain.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.d.ts +103 -0
- package/dist/core/server/fleet/channel.js +191 -0
- package/dist/core/server/fleet/channel.test.d.ts +1 -0
- package/dist/core/server/fleet/fixtures/fleetServer.d.ts +1 -0
- package/dist/core/server/fleet/fleet.integration.test.d.ts +1 -0
- package/dist/core/server/fleet/link.d.ts +23 -0
- package/dist/core/server/fleet/link.js +70 -0
- package/dist/core/server/fleet/relay.d.ts +11 -0
- package/dist/core/server/fleet/relay.js +104 -0
- package/dist/core/server/fleet/relay.test.d.ts +1 -0
- package/dist/core/server/fleet/role.d.ts +46 -0
- package/dist/core/server/fleet/role.js +59 -0
- package/dist/core/server/fleet/role.test.d.ts +1 -0
- package/dist/core/server/fleet/stores.d.ts +7 -0
- package/dist/core/server/fleet/stores.js +19 -0
- package/dist/core/server/fleet/supervisor.d.ts +26 -0
- package/dist/core/server/fleet/supervisor.js +108 -0
- package/dist/core/server/fleet/supervisor.test.d.ts +1 -0
- package/dist/core/server/pageServer.js +36 -5
- package/dist/core/services/action.js +10 -2
- package/dist/core/services/functionRoutes.d.ts +9 -0
- package/dist/core/services/functionRoutes.js +47 -0
- package/dist/core/services/realtime.d.ts +8 -0
- package/dist/core/services/realtime.js +57 -0
- package/dist/core/services/registry.js +6 -2
- package/dist/core/staticFiles.js +12 -4
- package/dist/core/staticFiles.test.d.ts +1 -0
- package/dist/core/transports.d.ts +13 -3
- package/dist/core/transports.js +14 -5
- package/dist/functions-api.d.ts +281 -0
- package/dist/functions.d.ts +19 -0
- package/dist/functions.js +5 -0
- package/dist/functionsRunner.d.ts +15 -0
- package/dist/functionsRunner.js +6 -0
- package/dist/handlers.d.ts +2 -2
- package/dist/handlers.js +2 -2
- package/dist/helpers/atomicFile.d.ts +10 -0
- package/dist/helpers/atomicFile.js +33 -0
- package/dist/helpers/atomicFile.test.d.ts +1 -0
- package/dist/helpers/buildResponseHelpers.js +35 -11
- package/dist/helpers/buildServerInfo.js +4 -2
- package/dist/helpers/buildServerInfo.test.d.ts +1 -0
- package/dist/helpers/cache/CompressedFileCache.d.ts +18 -0
- package/dist/helpers/cache/CompressedFileCache.js +44 -0
- package/dist/helpers/cache/CompressedFileCache.test.d.ts +1 -0
- package/dist/helpers/cache/cacheManager.d.ts +1 -1
- package/dist/helpers/cache/cacheManager.js +2 -1
- package/dist/helpers/cache/cacheManager.test.d.ts +1 -0
- package/dist/helpers/cache/index.d.ts +1 -1
- package/dist/helpers/cache/keys.d.ts +15 -1
- package/dist/helpers/cache/keys.js +47 -5
- package/dist/helpers/cache/serverCaches.d.ts +7 -1
- package/dist/helpers/compress.d.ts +3 -1
- package/dist/helpers/compress.js +11 -3
- package/dist/helpers/hydrationPayload.d.ts +11 -0
- package/dist/helpers/hydrationPayload.js +51 -0
- package/dist/helpers/hydrationPayload.test.d.ts +1 -0
- package/dist/helpers/metrics.d.ts +1 -1
- package/dist/helpers/metrics.js +3 -2
- package/dist/helpers/outboundGuard.d.ts +14 -6
- package/dist/helpers/outboundGuard.js +132 -8
- package/dist/helpers/paintedCookie.d.ts +8 -0
- package/dist/helpers/paintedCookie.js +13 -0
- package/dist/helpers/reportReactBuild.d.ts +9 -0
- package/dist/helpers/reportReactBuild.js +18 -0
- package/dist/helpers/reportReactBuild.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +25 -1
- package/dist/helpers/serverLog.js +68 -3
- package/dist/helpers/serverLog.test.d.ts +1 -0
- package/dist/helpers/wsFrame.d.ts +3 -0
- package/dist/helpers/wsFrame.js +5 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +6 -2
- package/dist/kernel.d.ts +2 -1
- package/dist/kernel.js +3 -2
- package/dist/modules/actions/connectorWrite.js +2 -1
- package/dist/modules/actions/index.d.ts +41 -13
- package/dist/modules/actions/index.js +52 -9
- package/dist/modules/actions/jobs/failureStreak.d.ts +17 -0
- package/dist/modules/actions/jobs/failureStreak.js +47 -0
- package/dist/modules/actions/jobs/failureStreak.test.d.ts +1 -0
- package/dist/modules/actions/jobs/index.d.ts +33 -0
- package/dist/modules/actions/jobs/index.js +59 -0
- package/dist/modules/actions/jobs/memoryQueue.d.ts +33 -0
- package/dist/modules/actions/jobs/memoryQueue.js +194 -0
- package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
- package/dist/modules/actions/jobs/scheduler.d.ts +64 -0
- package/dist/modules/actions/jobs/scheduler.js +130 -0
- package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
- package/dist/modules/actions/jobs/schedules.d.ts +23 -0
- package/dist/modules/actions/jobs/schedules.js +47 -0
- package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
- package/dist/modules/actions/jobs/storeClock.js +32 -0
- package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
- package/dist/modules/actions/jobs/worker.d.ts +64 -0
- package/dist/modules/actions/jobs/worker.js +267 -0
- package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
- package/dist/modules/actions/moduleFor.js +6 -2
- package/dist/modules/actions/runtime/errors.d.ts +12 -0
- package/dist/modules/actions/runtime/errors.js +16 -1
- package/dist/modules/actions/runtime/kvChange.d.ts +27 -0
- package/dist/modules/actions/runtime/kvChange.js +23 -0
- package/dist/modules/actions/runtime/kvList.d.ts +51 -0
- package/dist/modules/actions/runtime/kvList.js +52 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +8 -0
- package/dist/modules/actions/runtime/kvStore.js +46 -20
- package/dist/modules/actions/runtime/memoryKv.d.ts +3 -0
- package/dist/modules/actions/runtime/memoryKv.js +19 -1
- package/dist/modules/actions/runtime/namespaceKv.d.ts +4 -6
- package/dist/modules/actions/runtime/namespaceKv.js +15 -3
- package/dist/modules/actions/runtime/namespaceKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/precheck.js +5 -9
- package/dist/modules/actions/runtime/rateLimit.d.ts +19 -0
- package/dist/modules/actions/runtime/rateLimit.js +19 -0
- package/dist/modules/actions/runtime/redisKv.d.ts +27 -0
- package/dist/modules/actions/runtime/redisKv.integration.test.d.ts +1 -0
- package/dist/modules/actions/runtime/redisKv.js +46 -0
- package/dist/modules/actions/runtime/renderResolver.js +2 -2
- package/dist/modules/actions/runtime/report.js +2 -1
- package/dist/modules/actions/runtime/runAction.d.ts +10 -2
- package/dist/modules/actions/runtime/runAction.js +52 -116
- package/dist/modules/actions/runtime/runLogger.d.ts +5 -0
- package/dist/modules/actions/runtime/runLogger.js +8 -2
- package/dist/modules/actions/runtime/signing.d.ts +20 -0
- package/dist/modules/actions/runtime/signing.js +25 -0
- package/dist/modules/actions/runtime/signing.test.d.ts +1 -0
- package/dist/modules/actions/runtime/taskContext.d.ts +33 -0
- package/dist/modules/actions/runtime/taskContext.js +124 -0
- package/dist/modules/actions/tasks/builtins.js +5 -1
- package/dist/modules/actions/tasks/flow.js +81 -10
- package/dist/modules/actions/tasks/helpers.js +2 -2
- package/dist/modules/actions/tasks/http.js +2 -3
- package/dist/modules/actions/tasks/kv.js +39 -0
- package/dist/modules/actions/tasks/list.d.ts +2 -0
- package/dist/modules/actions/tasks/list.js +145 -0
- package/dist/modules/actions/tasks/realtime.d.ts +2 -0
- package/dist/modules/actions/tasks/realtime.js +104 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -1
- package/dist/modules/actions/tasks/registry.js +17 -6
- package/dist/modules/actions/transport/callHandler.js +6 -3
- package/dist/modules/actions/transport/catalogHandler.d.ts +5 -2
- package/dist/modules/actions/transport/catalogHandler.js +3 -2
- package/dist/modules/actions/transport/stream.d.ts +4 -5
- package/dist/modules/actions/transport/stream.js +12 -76
- package/dist/modules/actions/transport/webhookHandler.js +5 -4
- package/dist/modules/actions/types.d.ts +162 -4
- package/dist/modules/connectors/engine.js +3 -5
- package/dist/modules/functions/build.d.ts +22 -0
- package/dist/modules/functions/build.js +122 -0
- package/dist/modules/functions/build.test.d.ts +1 -0
- package/dist/modules/functions/capabilities.d.ts +22 -0
- package/dist/modules/functions/capabilities.js +228 -0
- package/dist/modules/functions/config.d.ts +34 -0
- package/dist/modules/functions/config.js +24 -0
- package/dist/modules/functions/context.d.ts +17 -0
- package/dist/modules/functions/context.js +88 -0
- package/dist/modules/functions/contract.d.ts +121 -0
- package/dist/modules/functions/contract.js +9 -0
- package/dist/modules/functions/driver.d.ts +36 -0
- package/dist/modules/functions/driver.js +150 -0
- package/dist/modules/functions/load.d.ts +15 -0
- package/dist/modules/functions/load.js +45 -0
- package/dist/modules/functions/load.test.d.ts +1 -0
- package/dist/modules/functions/local.d.ts +29 -0
- package/dist/modules/functions/local.js +61 -0
- package/dist/modules/functions/local.test.d.ts +1 -0
- package/dist/modules/functions/manifest.d.ts +24 -0
- package/dist/modules/functions/manifest.js +163 -0
- package/dist/modules/functions/manifest.test.d.ts +1 -0
- package/dist/modules/functions/native.d.ts +8 -0
- package/dist/modules/functions/native.js +21 -0
- package/dist/modules/functions/native.test.d.ts +1 -0
- package/dist/modules/functions/protocol.d.ts +216 -0
- package/dist/modules/functions/protocol.js +18 -0
- package/dist/modules/functions/routes.d.ts +36 -0
- package/dist/modules/functions/routes.js +181 -0
- package/dist/modules/functions/routes.test.d.ts +1 -0
- package/dist/modules/functions/runner/messages.d.ts +5 -0
- package/dist/modules/functions/runner/messages.js +188 -0
- package/dist/modules/functions/runner/remote.d.ts +19 -0
- package/dist/modules/functions/runner/remote.js +116 -0
- package/dist/modules/functions/runner/remote.test.d.ts +1 -0
- package/dist/modules/functions/runner/service.d.ts +32 -0
- package/dist/modules/functions/runner/service.js +209 -0
- package/dist/modules/functions/sandbox/guest.d.ts +45 -0
- package/dist/modules/functions/sandbox/guest.js +660 -0
- package/dist/modules/functions/sandbox/isolate.d.ts +37 -0
- package/dist/modules/functions/sandbox/isolate.js +347 -0
- package/dist/modules/functions/sandbox/isolate.test.d.ts +1 -0
- package/dist/modules/functions/sandbox/prelude.d.ts +2 -0
- package/dist/modules/functions/sandbox/prelude.js +49 -0
- package/dist/modules/functions/space.d.ts +40 -0
- package/dist/modules/functions/space.js +162 -0
- package/dist/modules/functions/testing/tasksOf.d.ts +4 -0
- package/dist/modules/functions/tryEntry.d.ts +9 -0
- package/dist/modules/functions/tryEntry.js +49 -0
- package/dist/modules/mongo/index.d.ts +4 -0
- package/dist/modules/mongo/jobQueue.d.ts +38 -0
- package/dist/modules/mongo/jobQueue.js +335 -0
- package/dist/modules/mongo/kv.d.ts +15 -0
- package/dist/modules/mongo/kv.js +129 -0
- package/dist/modules/mongo/stores.integration.test.d.ts +1 -0
- package/dist/modules/mysql/accounts.js +19 -9
- package/dist/modules/mysql/index.d.ts +2 -1
- package/dist/modules/mysql/jobs/index.d.ts +5 -0
- package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
- package/dist/modules/mysql/jobs/jobQueue.js +375 -0
- package/dist/modules/mysql/jobs/kv.d.ts +18 -0
- package/dist/modules/mysql/jobs/kv.js +108 -0
- package/dist/modules/mysql/jobs/schema.d.ts +33 -0
- package/dist/modules/mysql/jobs/schema.js +113 -0
- package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
- package/dist/modules/mysql/schema.js +25 -0
- package/dist/modules/mysql/sessions.d.ts +1 -11
- package/dist/modules/mysql/sessions.js +6 -1
- package/dist/modules/realtime/declarations.d.ts +11 -0
- package/dist/modules/realtime/declarations.js +26 -0
- package/dist/modules/realtime/grants.d.ts +23 -0
- package/dist/modules/realtime/grants.js +61 -0
- package/dist/modules/realtime/handlers.d.ts +74 -0
- package/dist/modules/realtime/handlers.js +224 -0
- package/dist/modules/realtime/hub.d.ts +62 -0
- package/dist/modules/realtime/hub.js +141 -0
- package/dist/modules/realtime/index.d.ts +51 -0
- package/dist/modules/realtime/index.js +62 -0
- package/dist/modules/realtime/memoryPubSub.d.ts +10 -0
- package/dist/modules/realtime/memoryPubSub.js +43 -0
- package/dist/modules/realtime/realtime.test.d.ts +1 -0
- package/dist/modules/realtime/redisPubSub.d.ts +24 -0
- package/dist/modules/realtime/redisPubSub.js +36 -0
- package/dist/modules/realtime/redisPubSub.test.d.ts +1 -0
- package/dist/modules/realtime/socket.d.ts +25 -0
- package/dist/modules/realtime/socket.js +122 -0
- package/dist/modules/realtime/socket.test.d.ts +1 -0
- package/dist/modules/rsc/handler.d.ts +16 -0
- package/dist/modules/rsc/handler.js +17 -10
- package/dist/modules/rsc/handler.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.js +2 -1
- package/dist/modules/runtime/bundle.d.ts +24 -0
- package/dist/modules/runtime/bundle.js +102 -0
- package/dist/modules/runtime/contract.d.ts +37 -0
- package/dist/modules/runtime/contract.js +21 -0
- package/dist/modules/runtime/entry.test.d.ts +1 -0
- package/dist/modules/runtime/host.d.ts +38 -0
- package/dist/modules/runtime/host.js +68 -0
- package/dist/modules/runtime/inside.d.ts +15 -0
- package/dist/modules/runtime/inside.js +47 -0
- package/dist/modules/runtime/inside.test.d.ts +1 -0
- package/dist/modules/runtime/runner.d.ts +11 -0
- package/dist/modules/runtime/runner.js +38 -0
- package/dist/modules/runtime/runtime.test.d.ts +1 -0
- package/dist/modules/runtime/stages.d.ts +50 -0
- package/dist/modules/runtime/stages.js +117 -0
- package/dist/modules/ssr/Component.d.ts +4 -5
- package/dist/modules/ssr/Component.js +2 -2
- package/dist/modules/ssr/handler.js +10 -2
- package/dist/modules/ssr/loadPluginComponents.js +2 -1
- package/dist/modules/ssr/prepareRender.js +39 -13
- package/dist/modules/ssr/preview.d.ts +3 -0
- package/dist/modules/ssr/preview.js +17 -7
- package/dist/modules/ssr/redirect.test.d.ts +1 -0
- package/dist/modules/ssr/registerExternalPlugins.d.ts +6 -1
- package/dist/modules/ssr/registerExternalPlugins.js +19 -16
- package/dist/modules/ssr/registerExternalPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/reportMissingPlugins.d.ts +9 -0
- package/dist/modules/ssr/reportMissingPlugins.js +24 -0
- package/dist/modules/ssr/reportMissingPlugins.test.d.ts +1 -0
- package/dist/modules/ssr/streamBody.d.ts +2 -2
- package/dist/modules/ssr/streamBody.js +4 -1
- package/dist/modules/ssr/template.js +1 -1
- package/dist/modules/ssr/views/template.ejs +8 -5
- package/dist/mongo.d.ts +25 -0
- package/dist/mongo.js +3 -0
- package/dist/mysql.d.ts +11 -0
- package/dist/mysql.js +4 -1
- package/dist/oauth.js +2 -2
- package/dist/plugins/compile.js +9 -4
- package/dist/plugins/copy.js +3 -3
- package/dist/plugins/manager.d.ts +35 -3
- package/dist/plugins/manager.js +109 -38
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +7 -0
- package/package.json +52 -11
- package/skills/plitzi-authoring/SKILL.md +153 -263
- package/skills/plitzi-authoring/reference/accessibility.md +111 -0
- package/skills/plitzi-authoring/reference/authoring-errors.md +70 -0
- package/skills/plitzi-authoring/reference/data-and-visibility.md +180 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +161 -0
- package/skills/plitzi-authoring/reference/flows.md +171 -0
- package/skills/plitzi-authoring/reference/layouts.md +140 -0
- package/skills/plitzi-authoring/reference/lists.md +74 -0
- package/skills/plitzi-authoring/reference/performance.md +67 -0
- package/skills/plitzi-authoring/reference/plugins.md +167 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +67 -0
- package/skills/plitzi-authoring/reference/structure.md +83 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
- package/skills/plitzi-authoring/reference/templates.md +109 -0
- package/skills/plitzi-authoring/reference/testing.md +119 -0
- package/skills/plitzi-authoring/reference/validation.md +73 -0
- package/dist/modules/actions/runtime/schedule.d.ts +0 -35
- package/dist/modules/actions/runtime/schedule.js +0 -76
- /package/dist/{modules/actions/runtime/schedule.test.d.ts → adapters/jsonAdapters.test.d.ts} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { createFailureStreak } from "./failureStreak.js";
|
|
3
|
+
import { ActionRunError } from "../runtime/errors.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
//#region src/modules/actions/jobs/worker.ts
|
|
6
|
+
/**
|
|
7
|
+
* Refusals that mean "not now" rather than "not ever".
|
|
8
|
+
*
|
|
9
|
+
* Both are the cluster protecting itself — another run holds the single-flight key, or this replica is already
|
|
10
|
+
* carrying as much as it will — and neither says anything about the job. So the job goes back to the queue with
|
|
11
|
+
* its attempt REFUNDED: burning one would let a busy afternoon retire a nightly digest as dead.
|
|
12
|
+
*/
|
|
13
|
+
var TRY_AGAIN = /* @__PURE__ */ new Set(["duplicate", "over_capacity"]);
|
|
14
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
15
|
+
/**
|
|
16
|
+
* Why a run that ended badly did, in the words an operator can act on: the step that failed and what it said.
|
|
17
|
+
*
|
|
18
|
+
* Read from `steps` — the outline, whose errors are already redacted of every credential the run resolved — so
|
|
19
|
+
* nothing lands on the job that the run history would not show. A run that failed without any step failing (a
|
|
20
|
+
* timeout between steps, a budget spent) has no step to name, and says how it ended instead.
|
|
21
|
+
*/
|
|
22
|
+
var failureOf = (result) => {
|
|
23
|
+
const failed = result.steps.find((step) => step.phase === "flow" && step.status === "failed");
|
|
24
|
+
return failed ? `step "${failed.id}" failed: ${failed.error ?? "no reason given"}` : `the flow ended ${result.status}`;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Refusals no number of attempts will change: the action was deleted, its schedule was switched off, or the
|
|
28
|
+
* document refuses this trigger. Retrying is noise, and the job stops where an operator can see why.
|
|
29
|
+
*/
|
|
30
|
+
var GIVE_UP = /* @__PURE__ */ new Set([
|
|
31
|
+
"not_found",
|
|
32
|
+
"disabled",
|
|
33
|
+
"forbidden",
|
|
34
|
+
"recursion"
|
|
35
|
+
]);
|
|
36
|
+
var createJobWorker = ({ queue, lookups, module, workers = 4, pollMs = 1e3, leaseMs = 3e4, backoff = {}, workerId = `${process.pid}-${randomUUID().slice(0, 8)}`, onError: customOnError }) => {
|
|
37
|
+
const onError = customOnError ?? ((error) => serverLog.error("Actions", "job worker failed", error));
|
|
38
|
+
const claimFailures = createFailureStreak("Actions", "job claim");
|
|
39
|
+
const heartbeatFailures = createFailureStreak("Actions", "job heartbeat");
|
|
40
|
+
const { baseMs = 3e4, maxMs = 9e5 } = backoff;
|
|
41
|
+
const active = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Every job this worker started, until it has settled — what `stop()` waits for. */
|
|
43
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
44
|
+
let timer;
|
|
45
|
+
let heart;
|
|
46
|
+
let stopping = false;
|
|
47
|
+
let pass;
|
|
48
|
+
const isStopping = () => stopping;
|
|
49
|
+
/** Doubles per attempt, with a jitter so a batch that failed together does not retry together. */
|
|
50
|
+
const retryDelay = (attempts) => {
|
|
51
|
+
const step = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempts - 1));
|
|
52
|
+
return step + Math.floor(Math.random() * Math.min(step, 3e4));
|
|
53
|
+
};
|
|
54
|
+
const settle = async (settlement) => {
|
|
55
|
+
try {
|
|
56
|
+
await queue.settle(settlement);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
onError(error);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Back to the queue with nothing held against it — see {@link TRY_AGAIN}. */
|
|
62
|
+
const release = (job, delayMs, error) => settle({
|
|
63
|
+
jobId: job.id,
|
|
64
|
+
workerId,
|
|
65
|
+
status: "pending",
|
|
66
|
+
delayMs,
|
|
67
|
+
error
|
|
68
|
+
});
|
|
69
|
+
const fail = (job, runId, error) => {
|
|
70
|
+
const exhausted = job.attempts >= job.maxAttempts;
|
|
71
|
+
return settle({
|
|
72
|
+
jobId: job.id,
|
|
73
|
+
workerId,
|
|
74
|
+
status: exhausted ? "dead" : "pending",
|
|
75
|
+
...exhausted ? {} : { delayMs: retryDelay(job.attempts) },
|
|
76
|
+
...runId ? { runId } : {},
|
|
77
|
+
error,
|
|
78
|
+
attempt: {
|
|
79
|
+
status: "failed",
|
|
80
|
+
workerId,
|
|
81
|
+
...runId ? { runId } : {},
|
|
82
|
+
error
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const execute = async (job) => {
|
|
87
|
+
const entry = await lookups.getAction(job.spaceId, job.actionId);
|
|
88
|
+
if (!entry) {
|
|
89
|
+
await settle({
|
|
90
|
+
jobId: job.id,
|
|
91
|
+
workerId,
|
|
92
|
+
status: "dead",
|
|
93
|
+
error: "the action this job belongs to no longer exists",
|
|
94
|
+
attempt: {
|
|
95
|
+
status: "failed",
|
|
96
|
+
workerId,
|
|
97
|
+
error: "action not found"
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let run;
|
|
103
|
+
try {
|
|
104
|
+
run = await module.guards.begin({
|
|
105
|
+
spaceId: job.spaceId,
|
|
106
|
+
actionId: job.actionId,
|
|
107
|
+
callerId: "schedule",
|
|
108
|
+
input: job.input,
|
|
109
|
+
idempotencyKey: job.id,
|
|
110
|
+
sharedKey: true,
|
|
111
|
+
ttlMs: module.limitsFor(entry.document).timeoutMs
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const refusal = error instanceof ActionRunError ? error.reason : void 0;
|
|
115
|
+
/**
|
|
116
|
+
* Two different things, and telling them apart is what decides whether the job survives.
|
|
117
|
+
*
|
|
118
|
+
* A REFUSAL is the cluster answering: another run holds the key, this replica is full. A throw that is not a
|
|
119
|
+
* refusal means the guards could not be consulted at all — the shared store is unreachable — and that is
|
|
120
|
+
* infrastructure rather than anything about this job. Both go back with the attempt REFUNDED.
|
|
121
|
+
*
|
|
122
|
+
* Counting an outage as attempts is how a queue loses work: with three attempts and a widening backoff, a
|
|
123
|
+
* Redis blip of two minutes retires every scheduled job in the cluster as `dead`, permanently, and the digest
|
|
124
|
+
* nobody got is indistinguishable from one nobody asked for. The job comes back instead, carrying why, and an
|
|
125
|
+
* operator sees a queue that is waiting rather than a queue that has been emptied.
|
|
126
|
+
*/
|
|
127
|
+
if (!refusal || TRY_AGAIN.has(refusal)) {
|
|
128
|
+
await release(job, retryDelay(1), `not started: ${refusal ?? messageOf(error)}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await fail(job, void 0, `could not start: ${refusal}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
active.set(job.id, run.controller);
|
|
135
|
+
try {
|
|
136
|
+
const result = await module.runAction({
|
|
137
|
+
entry,
|
|
138
|
+
input: job.input,
|
|
139
|
+
spaceId: job.spaceId,
|
|
140
|
+
environment: job.environment,
|
|
141
|
+
trigger: job.trigger,
|
|
142
|
+
callerId: "schedule",
|
|
143
|
+
runId: run.runId,
|
|
144
|
+
signal: run.controller.signal
|
|
145
|
+
});
|
|
146
|
+
if (result.status === "completed") {
|
|
147
|
+
await settle({
|
|
148
|
+
jobId: job.id,
|
|
149
|
+
workerId,
|
|
150
|
+
status: "succeeded",
|
|
151
|
+
runId: run.runId,
|
|
152
|
+
attempt: {
|
|
153
|
+
status: "succeeded",
|
|
154
|
+
workerId,
|
|
155
|
+
runId: run.runId
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (result.status === "aborted" && run.controller.signal.aborted) {
|
|
161
|
+
await settle({
|
|
162
|
+
jobId: job.id,
|
|
163
|
+
workerId,
|
|
164
|
+
status: "cancelled",
|
|
165
|
+
runId: run.runId,
|
|
166
|
+
attempt: {
|
|
167
|
+
status: "cancelled",
|
|
168
|
+
workerId,
|
|
169
|
+
runId: run.runId
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await fail(job, run.runId, failureOf(result));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const reason = error instanceof ActionRunError ? error.reason : void 0;
|
|
177
|
+
if (reason && GIVE_UP.has(reason)) {
|
|
178
|
+
await settle({
|
|
179
|
+
jobId: job.id,
|
|
180
|
+
workerId,
|
|
181
|
+
status: "dead",
|
|
182
|
+
runId: run.runId,
|
|
183
|
+
error: reason,
|
|
184
|
+
attempt: {
|
|
185
|
+
status: "failed",
|
|
186
|
+
workerId,
|
|
187
|
+
runId: run.runId,
|
|
188
|
+
error: reason
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await fail(job, run.runId, messageOf(error));
|
|
194
|
+
} finally {
|
|
195
|
+
active.delete(job.id);
|
|
196
|
+
await module.guards.end(run);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const poll = async () => {
|
|
200
|
+
const free = workers - active.size;
|
|
201
|
+
if (stopping || free <= 0) return 0;
|
|
202
|
+
const claimed = await queue.claim({
|
|
203
|
+
workerId,
|
|
204
|
+
leaseMs,
|
|
205
|
+
limit: free
|
|
206
|
+
});
|
|
207
|
+
if (isStopping()) {
|
|
208
|
+
await Promise.all(claimed.map((job) => release(job, 0, "not started: the replica was shutting down")));
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
for (const job of claimed) {
|
|
212
|
+
const running = execute(job).catch(onError);
|
|
213
|
+
inFlight.add(running);
|
|
214
|
+
running.finally(() => inFlight.delete(running));
|
|
215
|
+
}
|
|
216
|
+
return claimed.length;
|
|
217
|
+
};
|
|
218
|
+
/** Renews every claim this worker holds, and stops the ones an operator asked to cancel. */
|
|
219
|
+
const beat = async () => {
|
|
220
|
+
const held = [...active.keys()];
|
|
221
|
+
if (held.length === 0) return;
|
|
222
|
+
for (const jobId of await queue.heartbeat({
|
|
223
|
+
jobIds: held,
|
|
224
|
+
workerId,
|
|
225
|
+
leaseMs
|
|
226
|
+
})) active.get(jobId)?.abort();
|
|
227
|
+
};
|
|
228
|
+
/** One pass at a time, and the one under way is kept so `stop()` can wait for the claim it may be making. */
|
|
229
|
+
const guarded = async () => {
|
|
230
|
+
if (pass) return;
|
|
231
|
+
pass = poll().then(() => claimFailures.succeeded()).catch(customOnError ?? claimFailures.failed).finally(() => {
|
|
232
|
+
pass = void 0;
|
|
233
|
+
});
|
|
234
|
+
await pass;
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
poll,
|
|
238
|
+
beat,
|
|
239
|
+
inFlight: () => active.size,
|
|
240
|
+
start: () => {
|
|
241
|
+
if (timer || workers <= 0) return;
|
|
242
|
+
timer = setInterval(() => void guarded(), pollMs);
|
|
243
|
+
timer.unref();
|
|
244
|
+
heart = setInterval(() => void beat().then(() => heartbeatFailures.succeeded()).catch(customOnError ?? heartbeatFailures.failed), Math.max(1e3, Math.floor(leaseMs / 3)));
|
|
245
|
+
heart.unref();
|
|
246
|
+
},
|
|
247
|
+
stop: async () => {
|
|
248
|
+
stopping = true;
|
|
249
|
+
clearInterval(timer);
|
|
250
|
+
timer = void 0;
|
|
251
|
+
await pass;
|
|
252
|
+
if (inFlight.size > 0) serverLog.info("Actions", `waiting for ${inFlight.size} running job(s) to finish before stopping`);
|
|
253
|
+
/**
|
|
254
|
+
* Drained rather than abandoned: a deploy finishes the jobs it started instead of leaving them to be retried.
|
|
255
|
+
*
|
|
256
|
+
* The heartbeat is stopped only AFTER, and that is the half that makes waiting safe. A job longer than its
|
|
257
|
+
* lease used to lose its claim the moment the drain began — another replica took it over and ran it again
|
|
258
|
+
* while this one was still finishing it, and the drain gave up at the lease regardless.
|
|
259
|
+
*/
|
|
260
|
+
await Promise.allSettled([...inFlight]);
|
|
261
|
+
clearInterval(heart);
|
|
262
|
+
heart = void 0;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
export { createJobWorker };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createActionsModule } from "./index.js";
|
|
2
|
+
import { realtimeModuleFor } from "../realtime/index.js";
|
|
2
3
|
//#region src/modules/actions/moduleFor.ts
|
|
3
4
|
/**
|
|
4
5
|
* The one actions module for a server, whoever asks first.
|
|
@@ -19,15 +20,18 @@ var actionsModuleFor = (config) => {
|
|
|
19
20
|
if (existing) return existing;
|
|
20
21
|
const module = createActionsModule({
|
|
21
22
|
lookups,
|
|
22
|
-
|
|
23
|
+
functions: config.functions,
|
|
23
24
|
limits: config.action?.limits,
|
|
24
25
|
concurrency: config.action?.concurrency,
|
|
25
26
|
kv: config.action?.kv,
|
|
27
|
+
jobs: config.action?.jobs,
|
|
26
28
|
rateLimit: config.action?.rateLimit,
|
|
27
29
|
idempotency: config.action?.idempotency,
|
|
30
|
+
signingSecret: config.action?.signingSecret,
|
|
28
31
|
dbDrivers: config.action?.dbDrivers,
|
|
29
32
|
email: config.action?.email,
|
|
30
|
-
onRun: config.action?.onRun
|
|
33
|
+
onRun: config.action?.onRun,
|
|
34
|
+
realtime: realtimeModuleFor(config)
|
|
31
35
|
});
|
|
32
36
|
modules.set(config, module);
|
|
33
37
|
return module;
|
|
@@ -14,3 +14,15 @@ export declare class ActionRunError extends Error {
|
|
|
14
14
|
steps?: ActionRunStep[];
|
|
15
15
|
constructor(reason: ActionErrorReason, message: string);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* A step refusing to go on, with a reason written for whoever called — a password too easy to guess, a board that is
|
|
19
|
+
* read-only, a quota reached.
|
|
20
|
+
*
|
|
21
|
+
* What any other step throws stays in the run's record and never reaches the caller: an error can carry a query, a
|
|
22
|
+
* URL, a credential's name, and a page is shown to anybody. This one says, by its type, that its message was written
|
|
23
|
+
* to be read. A run that ends on it answers `status: 'failed'` with the message as `error`, which a page's flow reads
|
|
24
|
+
* as `{{ step.error }}` — the same place a refusal before the run began puts its own.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ActionRefusal extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
@@ -18,5 +18,20 @@ var ActionRunError = class extends Error {
|
|
|
18
18
|
this.reason = reason;
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* A step refusing to go on, with a reason written for whoever called — a password too easy to guess, a board that is
|
|
23
|
+
* read-only, a quota reached.
|
|
24
|
+
*
|
|
25
|
+
* What any other step throws stays in the run's record and never reaches the caller: an error can carry a query, a
|
|
26
|
+
* URL, a credential's name, and a page is shown to anybody. This one says, by its type, that its message was written
|
|
27
|
+
* to be read. A run that ends on it answers `status: 'failed'` with the message as `error`, which a page's flow reads
|
|
28
|
+
* as `{{ step.error }}` — the same place a refusal before the run began puts its own.
|
|
29
|
+
*/
|
|
30
|
+
var ActionRefusal = class extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ActionRefusal";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
21
36
|
//#endregion
|
|
22
|
-
export { ActionRunError };
|
|
37
|
+
export { ActionRefusal, ActionRunError };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** What a change needs of a store: read a key, and write it only if it still holds what was read. */
|
|
2
|
+
export type KvChangeStore<V> = {
|
|
3
|
+
get: (key: string) => Promise<V | undefined>;
|
|
4
|
+
swap: (key: string, expected: V | undefined, next: V, ttlSeconds?: number) => Promise<boolean>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The lifetime a change writes with: a number of seconds, one worked out from the value written, or none.
|
|
8
|
+
*/
|
|
9
|
+
export type KvChangeLifetime<T> = number | ((next: T) => number | undefined);
|
|
10
|
+
export type KvChangeOptions<T> = {
|
|
11
|
+
lifetime?: KvChangeLifetime<T> | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The error a change that kept losing ends with — an `ActionRefusal` where there is one to make: being too busy is
|
|
14
|
+
* something to tell whoever asked, not a fault. Handed in because this function may import nothing.
|
|
15
|
+
*/
|
|
16
|
+
refusal: (message: string) => Error;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Reads `key`, hands its value to `change`, and writes back what that answers — unless somebody wrote in between, and
|
|
20
|
+
* then it reads again and asks again. `change` answering `undefined` writes nothing. Answers what was written, or
|
|
21
|
+
* `undefined` when nothing was. Losing every time ends in `refusal`, whose message names no key: it is for a visitor.
|
|
22
|
+
*
|
|
23
|
+
* The one read-change-write there is: `ctx.kv.change`, a list's writes, and the sandbox's `ctx.kv.change` too — its
|
|
24
|
+
* guest is handed THIS function's source (`isolate.ts`), so it must stay self-contained: no import, nothing of this
|
|
25
|
+
* module's, only what the guest's globals also have (`setTimeout`, `Math`).
|
|
26
|
+
*/
|
|
27
|
+
export declare const changeKv: <V, T extends V>(store: KvChangeStore<V>, key: string, change: (current: V | undefined) => T | undefined | Promise<T | undefined>, { lifetime, refusal }: KvChangeOptions<T>) => Promise<T | undefined>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/kvChange.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reads `key`, hands its value to `change`, and writes back what that answers — unless somebody wrote in between, and
|
|
4
|
+
* then it reads again and asks again. `change` answering `undefined` writes nothing. Answers what was written, or
|
|
5
|
+
* `undefined` when nothing was. Losing every time ends in `refusal`, whose message names no key: it is for a visitor.
|
|
6
|
+
*
|
|
7
|
+
* The one read-change-write there is: `ctx.kv.change`, a list's writes, and the sandbox's `ctx.kv.change` too — its
|
|
8
|
+
* guest is handed THIS function's source (`isolate.ts`), so it must stay self-contained: no import, nothing of this
|
|
9
|
+
* module's, only what the guest's globals also have (`setTimeout`, `Math`).
|
|
10
|
+
*/
|
|
11
|
+
var changeKv = async (store, key, change, { lifetime, refusal }) => {
|
|
12
|
+
const attempts = 25;
|
|
13
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
14
|
+
const current = await store.get(key);
|
|
15
|
+
const next = await change(current);
|
|
16
|
+
if (next === void 0) return;
|
|
17
|
+
if (await store.swap(key, current, next, typeof lifetime === "function" ? lifetime(next) : lifetime)) return next;
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, Math.random() * Math.min(5 + attempt * 5, 50)));
|
|
19
|
+
}
|
|
20
|
+
throw refusal("Many people are changing this at once — try again in a moment");
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { changeKv };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** One entry of a list: what it is called, where it sorts, and what it carries. */
|
|
2
|
+
export type KvListEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
score: number;
|
|
5
|
+
value: unknown;
|
|
6
|
+
};
|
|
7
|
+
export type KvListRange = {
|
|
8
|
+
/** How many entries to skip first. 0 when absent. */
|
|
9
|
+
offset?: number;
|
|
10
|
+
/** How many to answer. All of them when absent. */
|
|
11
|
+
limit?: number;
|
|
12
|
+
/** `desc` — the highest score first — when absent. */
|
|
13
|
+
order?: 'asc' | 'desc';
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The most a list holds. A list is one value of the store, read and written whole, so it is for what a page shows at
|
|
17
|
+
* once — the latest twenty, a leaderboard, a gallery — not for a table.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_LIST_ENTRIES = 500;
|
|
20
|
+
/**
|
|
21
|
+
* Its largest size, as JSON. Every store holds far more; this is what a list is read and written whole for — a page's
|
|
22
|
+
* worth of entries, each an id and what the page shows — and past it, a list is being used as a table.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_LIST_BYTES = 128000;
|
|
25
|
+
export declare const listIdProblem: (id: unknown) => string | undefined;
|
|
26
|
+
/** The entries a stored list holds — none for a key that holds nothing, or something that is not a list. */
|
|
27
|
+
export declare const parseList: (raw: string | undefined) => KvListEntry[];
|
|
28
|
+
export type KvListPutOptions = {
|
|
29
|
+
/** How many to keep, the highest scores; what falls past it is dropped. {@link MAX_LIST_ENTRIES} at most. */
|
|
30
|
+
keep?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Replace an entry already there only with a score at least as high — a player's best, or the latest of two writes
|
|
33
|
+
* that arrived out of order. Otherwise the one there stays and nothing is written.
|
|
34
|
+
*/
|
|
35
|
+
higherOnly?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** What a put did: whether the entry went in, and what `keep` dropped to make room. */
|
|
38
|
+
export type KvListPut = {
|
|
39
|
+
stored: boolean;
|
|
40
|
+
dropped: KvListEntry[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The list with `entry` in it — replacing the one with its id — sorted and cut to the highest `keep`, and what the cut
|
|
44
|
+
* dropped; `undefined` when `higherOnly` keeps the one there. Refused, with what to do instead, when the result would
|
|
45
|
+
* be larger than a list may be.
|
|
46
|
+
*/
|
|
47
|
+
export declare const withEntry: (entries: KvListEntry[], entry: KvListEntry, { keep, higherOnly }?: KvListPutOptions) => {
|
|
48
|
+
kept: KvListEntry[];
|
|
49
|
+
dropped: KvListEntry[];
|
|
50
|
+
} | undefined;
|
|
51
|
+
export declare const rangeOf: (entries: KvListEntry[], { offset, limit, order }?: KvListRange) => KvListEntry[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/modules/actions/runtime/kvList.ts
|
|
2
|
+
/**
|
|
3
|
+
* The most a list holds. A list is one value of the store, read and written whole, so it is for what a page shows at
|
|
4
|
+
* once — the latest twenty, a leaderboard, a gallery — not for a table.
|
|
5
|
+
*/
|
|
6
|
+
var MAX_LIST_ENTRIES = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Its largest size, as JSON. Every store holds far more; this is what a list is read and written whole for — a page's
|
|
9
|
+
* worth of entries, each an id and what the page shows — and past it, a list is being used as a table.
|
|
10
|
+
*/
|
|
11
|
+
var MAX_LIST_BYTES = 128e3;
|
|
12
|
+
var ID = /^[A-Za-z0-9:_.@-]{1,128}$/;
|
|
13
|
+
var listIdProblem = (id) => typeof id === "string" && ID.test(id) ? void 0 : "A list entry’s id is 1-128 of A-Z a-z 0-9 : _ . @ -";
|
|
14
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
15
|
+
var isEntry = (value) => isRecord(value) && typeof value.id === "string" && typeof value.score === "number";
|
|
16
|
+
/** The entries a stored list holds — none for a key that holds nothing, or something that is not a list. */
|
|
17
|
+
var parseList = (raw) => {
|
|
18
|
+
if (raw === void 0) return [];
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(raw);
|
|
21
|
+
return Array.isArray(value) ? value.filter(isEntry) : [];
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Highest score first; entries that tie keep a stable order, by id. */
|
|
27
|
+
var byScore = (a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
28
|
+
/**
|
|
29
|
+
* The list with `entry` in it — replacing the one with its id — sorted and cut to the highest `keep`, and what the cut
|
|
30
|
+
* dropped; `undefined` when `higherOnly` keeps the one there. Refused, with what to do instead, when the result would
|
|
31
|
+
* be larger than a list may be.
|
|
32
|
+
*/
|
|
33
|
+
var withEntry = (entries, entry, { keep = 500, higherOnly = false } = {}) => {
|
|
34
|
+
const there = entries.find((existing) => existing.id === entry.id);
|
|
35
|
+
if (higherOnly && there && there.score > entry.score) return;
|
|
36
|
+
const sorted = [...entries.filter((existing) => existing.id !== entry.id), entry].sort(byScore);
|
|
37
|
+
const cut = Math.min(Math.max(0, Math.floor(keep)), 500);
|
|
38
|
+
const kept = sorted.slice(0, cut);
|
|
39
|
+
const bytes = Buffer.byteLength(JSON.stringify(kept));
|
|
40
|
+
if (bytes > 128e3) throw new Error(`A list is at most ${MAX_LIST_BYTES} bytes and this one would be ${bytes}: keep fewer entries (\`keep\`), or store less in each one — an id and what a page shows, with the rest under its own key`);
|
|
41
|
+
return {
|
|
42
|
+
kept,
|
|
43
|
+
dropped: sorted.slice(cut)
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
var rangeOf = (entries, { offset = 0, limit, order = "desc" } = {}) => {
|
|
47
|
+
const ordered = order === "asc" ? entries.toReversed() : entries;
|
|
48
|
+
const start = Math.max(0, Math.floor(offset));
|
|
49
|
+
return ordered.slice(start, limit === void 0 ? void 0 : start + Math.max(0, Math.floor(limit)));
|
|
50
|
+
};
|
|
51
|
+
//#endregion
|
|
52
|
+
export { MAX_LIST_BYTES, MAX_LIST_ENTRIES, listIdProblem, parseList, rangeOf, withEntry };
|
|
@@ -23,5 +23,13 @@ export type KvStoreConfig = {
|
|
|
23
23
|
* table.
|
|
24
24
|
* 3. **Nothing is caught.** This is not a cache: a miss means the rate limit did not count and the idempotency key
|
|
25
25
|
* was not seen, so an adapter that cannot answer fails the run rather than being read as "no value".
|
|
26
|
+
*
|
|
27
|
+
* A list is one value, changed only by `swap`: read, change, write back if nobody wrote in between, and read again if
|
|
28
|
+
* somebody did. Every adapter can do that much, so lists work on all of them without a command of their own. They
|
|
29
|
+
* live under `list:` rather than beside the values, so no key a flow writes can be one.
|
|
26
30
|
*/
|
|
31
|
+
/** What every key is prefixed with unless the store was given another. */
|
|
32
|
+
export declare const DEFAULT_KV_PREFIX = "kv:";
|
|
33
|
+
/** Where a list lives, after the prefix: its own segment, so a list and a value of the same name never meet. */
|
|
34
|
+
export declare const LIST_SEGMENT = "list:";
|
|
27
35
|
export declare const createKvStore: (adapter: ActionKvAdapter, { prefix }?: KvStoreConfig) => ActionKvStore;
|
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* 1. **Values round-trip through JSON**, so a flow reads back what it wrote instead of its own `toString`. A value
|
|
9
|
-
* written by something else is answered as the string it is rather than failing.
|
|
10
|
-
* 2. **A counter's TTL is set once, by whoever created it, and never extended.** A window refreshed on every hit
|
|
11
|
-
* never closes while traffic keeps arriving — which is a rate limit that stops limiting exactly when it is
|
|
12
|
-
* being leant on. The counter is the one that created it precisely when the increment returns its own amount,
|
|
13
|
-
* so this needs no conditional-expire support from the adapter and works the same on Redis, Memcached or a
|
|
14
|
-
* table.
|
|
15
|
-
* 3. **Nothing is caught.** This is not a cache: a miss means the rate limit did not count and the idempotency key
|
|
16
|
-
* was not seen, so an adapter that cannot answer fails the run rather than being read as "no value".
|
|
17
|
-
*/
|
|
1
|
+
import { ActionRefusal } from "./errors.js";
|
|
2
|
+
import { changeKv } from "./kvChange.js";
|
|
3
|
+
import { listIdProblem, parseList, rangeOf, withEntry } from "./kvList.js";
|
|
4
|
+
/** Where a list lives, after the prefix: its own segment, so a list and a value of the same name never meet. */
|
|
5
|
+
var LIST_SEGMENT = "list:";
|
|
6
|
+
var refusal = (message) => new ActionRefusal(message);
|
|
18
7
|
var createKvStore = (adapter, { prefix = "kv:" } = {}) => {
|
|
19
8
|
const prefixed = (key) => `${prefix}${key}`;
|
|
20
|
-
|
|
9
|
+
const listKey = (list) => `${prefix}${LIST_SEGMENT}${list}`;
|
|
10
|
+
/** Applies `change` to the list and writes it back unless somebody wrote first — then reads again. */
|
|
11
|
+
const changeList = async (list, change) => {
|
|
12
|
+
return await changeKv(adapter, listKey(list), (raw) => {
|
|
13
|
+
const next = change(parseList(raw));
|
|
14
|
+
return next ? JSON.stringify(next) : void 0;
|
|
15
|
+
}, { refusal }) !== void 0;
|
|
16
|
+
};
|
|
17
|
+
const store = {
|
|
21
18
|
get: async (key) => {
|
|
22
19
|
const raw = await adapter.get(prefixed(key));
|
|
23
20
|
if (raw === void 0) return;
|
|
@@ -34,8 +31,37 @@ var createKvStore = (adapter, { prefix = "kv:" } = {}) => {
|
|
|
34
31
|
const value = await adapter.increment(full, amount);
|
|
35
32
|
if (ttlSeconds !== void 0 && value === amount) await adapter.expire(full, ttlSeconds);
|
|
36
33
|
return value;
|
|
37
|
-
}
|
|
34
|
+
},
|
|
35
|
+
swap: (key, expected, next, ttlSeconds) => adapter.swap(prefixed(key), expected === void 0 ? void 0 : JSON.stringify(expected), JSON.stringify(next), ttlSeconds),
|
|
36
|
+
listPut: async (list, entry, options = {}) => {
|
|
37
|
+
const problem = listIdProblem(entry.id);
|
|
38
|
+
if (problem) throw new Error(problem);
|
|
39
|
+
if (!Number.isFinite(entry.score)) throw new Error("A list entry’s score is a number");
|
|
40
|
+
let put = {
|
|
41
|
+
stored: false,
|
|
42
|
+
dropped: []
|
|
43
|
+
};
|
|
44
|
+
await changeList(list, (entries) => {
|
|
45
|
+
const next = withEntry(entries, entry, options);
|
|
46
|
+
put = next ? {
|
|
47
|
+
stored: true,
|
|
48
|
+
dropped: next.dropped
|
|
49
|
+
} : {
|
|
50
|
+
stored: false,
|
|
51
|
+
dropped: []
|
|
52
|
+
};
|
|
53
|
+
return next?.kept;
|
|
54
|
+
});
|
|
55
|
+
return put;
|
|
56
|
+
},
|
|
57
|
+
listRange: async (list, range) => rangeOf(parseList(await adapter.get(listKey(list))), range),
|
|
58
|
+
listRemove: (list, id) => changeList(list, (entries) => entries.some((entry) => entry.id === id) ? entries.filter((entry) => entry.id !== id) : void 0),
|
|
59
|
+
change: (key, change, lifetime) => changeKv(store, key, change, {
|
|
60
|
+
lifetime,
|
|
61
|
+
refusal
|
|
62
|
+
})
|
|
38
63
|
};
|
|
64
|
+
return store;
|
|
39
65
|
};
|
|
40
66
|
//#endregion
|
|
41
|
-
export { createKvStore };
|
|
67
|
+
export { LIST_SEGMENT, createKvStore };
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { StoreMethods } from '../../../core/server/fleet/channel';
|
|
1
2
|
import { ActionKvAdapter } from '../types';
|
|
3
|
+
/** Every method of the store, for the copy a server's workers share (see `fleet/stores.ts`). */
|
|
4
|
+
export declare const KV_METHODS: StoreMethods<ActionKvAdapter>;
|
|
2
5
|
export declare const createMemoryKv: () => ActionKvAdapter;
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** How many writes go by before the map is swept. Cheap enough to be unnoticeable, often enough to bound it. */
|
|
13
13
|
var SWEEP_EVERY = 256;
|
|
14
|
+
/** Every method of the store, for the copy a server's workers share (see `fleet/stores.ts`). */
|
|
15
|
+
var KV_METHODS = {
|
|
16
|
+
get: true,
|
|
17
|
+
set: true,
|
|
18
|
+
delete: true,
|
|
19
|
+
increment: true,
|
|
20
|
+
expire: true,
|
|
21
|
+
swap: true
|
|
22
|
+
};
|
|
14
23
|
var createMemoryKv = () => {
|
|
15
24
|
const entries = /* @__PURE__ */ new Map();
|
|
16
25
|
let writes = 0;
|
|
@@ -70,8 +79,17 @@ var createMemoryKv = () => {
|
|
|
70
79
|
expiresAt: expiry(ttlSeconds)
|
|
71
80
|
});
|
|
72
81
|
return Promise.resolve();
|
|
82
|
+
},
|
|
83
|
+
swap: (key, expected, next, ttlSeconds) => {
|
|
84
|
+
if (read(key)?.value !== expected) return Promise.resolve(false);
|
|
85
|
+
entries.set(key, {
|
|
86
|
+
value: next,
|
|
87
|
+
expiresAt: expiry(ttlSeconds)
|
|
88
|
+
});
|
|
89
|
+
sweep();
|
|
90
|
+
return Promise.resolve(true);
|
|
73
91
|
}
|
|
74
92
|
};
|
|
75
93
|
};
|
|
76
94
|
//#endregion
|
|
77
|
-
export { createMemoryKv };
|
|
95
|
+
export { KV_METHODS, createMemoryKv };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { ActionKvStore } from '../types';
|
|
2
|
+
export declare const namespaceKv: (store: ActionKvStore, spaceId: number) => ActionKvStore;
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* A shared store with unprefixed keys is one space reading — or overwriting — another's counters, which is the
|
|
6
|
-
* cross-tenant leak this whole design exists to avoid. Done at the runner rather than in each task so a
|
|
7
|
-
* deployment's own tasks inherit it too.
|
|
4
|
+
* Every key a space's store holds, as patterns over the adapter's keys (`createKvStore`'s `prefix`, `kv:` unless it was
|
|
5
|
+
* given another) — for whoever has to let all of them go at once: a space that is deleted, or reset to how it started.
|
|
8
6
|
*/
|
|
9
|
-
export declare const
|
|
7
|
+
export declare const spaceKvPatterns: (spaceId: number, prefix?: string) => string[];
|