@plitzi/sdk-server 0.37.2 → 0.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1170 -0
- package/README.md +100 -7
- package/dist/actions.d.ts +6 -2
- package/dist/actions.js +5 -2
- package/dist/adapters/cloudAdapters.js +4 -3
- package/dist/adapters/jsonAdapters.js +28 -6
- package/dist/adapters/jsonAdapters.test.d.ts +1 -0
- 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 +10 -0
- package/dist/core/server/baseServer.js +42 -11
- package/dist/core/server/closeOnSignals.js +4 -3
- 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 +34 -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 +6 -1
- package/dist/index.js +5 -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 +29 -10
- package/dist/modules/actions/index.js +43 -6
- 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 +2 -1
- package/dist/modules/actions/jobs/index.js +5 -3
- package/dist/modules/actions/jobs/memoryQueue.d.ts +3 -0
- package/dist/modules/actions/jobs/memoryQueue.js +17 -1
- package/dist/modules/actions/jobs/scheduler.d.ts +2 -0
- package/dist/modules/actions/jobs/scheduler.js +12 -7
- package/dist/modules/actions/jobs/worker.d.ts +3 -1
- package/dist/modules/actions/jobs/worker.js +9 -4
- package/dist/modules/actions/moduleFor.js +5 -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 +4 -8
- 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/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 +5 -2
- 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 +4 -3
- package/dist/modules/actions/types.d.ts +86 -3
- 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/kv.js +31 -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 +1 -1
- package/dist/modules/mysql/jobs/kv.js +30 -0
- package/dist/modules/mysql/jobs/schema.d.ts +9 -0
- package/dist/modules/mysql/jobs/schema.js +21 -4
- 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 +3 -1
- package/dist/modules/ssr/Component.js +2 -1
- package/dist/modules/ssr/handler.js +10 -2
- package/dist/modules/ssr/loadPluginComponents.js +2 -1
- package/dist/modules/ssr/prepareRender.js +39 -10
- 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/mysql.d.ts +1 -1
- package/dist/mysql.js +2 -2
- 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 +39 -8
- package/skills/plitzi-authoring/SKILL.md +98 -14
- 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 +84 -18
- package/skills/plitzi-authoring/reference/elements-and-styles.md +61 -7
- package/skills/plitzi-authoring/reference/flows.md +108 -17
- 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 +19 -2
- package/skills/plitzi-authoring/reference/templates.md +67 -27
- package/skills/plitzi-authoring/reference/testing.md +72 -5
- package/skills/plitzi-authoring/reference/validation.md +73 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import onAbort from "../../helpers/onAbort.js";
|
|
2
|
+
import { openEventStream } from "../../core/http/sse.js";
|
|
3
|
+
import { accessRefusal } from "@plitzi/sdk-shared/actions";
|
|
4
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import { PRESENCE_TYPE, channelLimits, matchChannel } from "@plitzi/sdk-shared/realtime";
|
|
6
|
+
//#region src/modules/realtime/handlers.ts
|
|
7
|
+
/** Topics one connection may hold. A page shows a handful; past this it is somebody enumerating. */
|
|
8
|
+
var MAX_TOPICS = 8;
|
|
9
|
+
/** A type a page may send: its own vocabulary, never one of the channel's `$` types but `$presence`. */
|
|
10
|
+
var TYPE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
11
|
+
/** How long a disconnected client waits before trying again — ours reconnects itself; this is for anyone else's. */
|
|
12
|
+
var RETRY_MS = 3e3;
|
|
13
|
+
var answer = (res, { status, ...payload }) => {
|
|
14
|
+
res.setStatus(status);
|
|
15
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
16
|
+
res.setHeader("Cache-Control", "no-store");
|
|
17
|
+
res.send(JSON.stringify(payload));
|
|
18
|
+
};
|
|
19
|
+
var listOf = (query, name) => [...new Set((Object.hasOwn(query, name) ? query[name] : "").split(",").map((item) => item.trim()).filter(Boolean))];
|
|
20
|
+
/** The one of the grants the page sent (`?grants=g1,g2`) that opens `topic` — or none. */
|
|
21
|
+
var grantFor = async (grants, space, topic, offered) => {
|
|
22
|
+
for (const grant of offered) if (await grants.opens(space, topic, grant)) return grant;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Which of the topics a page asked for (`?topics=a,b`) it may open: each checked against the channels the space
|
|
26
|
+
* declares and against the visitor's access. One that fails is reported, not a reason to refuse the rest; all failing
|
|
27
|
+
* is a `403`.
|
|
28
|
+
*/
|
|
29
|
+
var admit = async (req, resolveChannels, grants) => {
|
|
30
|
+
const { environment = "main", spaceId, revision } = req.ctx.spaceDeployment ?? {};
|
|
31
|
+
if (typeof spaceId !== "number") return {
|
|
32
|
+
ok: false,
|
|
33
|
+
answer: {
|
|
34
|
+
status: 404,
|
|
35
|
+
error: "No space is served here",
|
|
36
|
+
reason: "not_found"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const requested = listOf(req.query, "topics");
|
|
40
|
+
const offered = listOf(req.query, "grants").slice(0, MAX_TOPICS);
|
|
41
|
+
if (requested.length === 0 || requested.length > MAX_TOPICS) return {
|
|
42
|
+
ok: false,
|
|
43
|
+
answer: {
|
|
44
|
+
status: 422,
|
|
45
|
+
error: `Name between 1 and ${MAX_TOPICS} topics: ?topics=a,b`,
|
|
46
|
+
reason: "invalid_input"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const channels = await resolveChannels(spaceId, environment, revision);
|
|
50
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
51
|
+
const granted = /* @__PURE__ */ new Map();
|
|
52
|
+
const refused = [];
|
|
53
|
+
const space = {
|
|
54
|
+
spaceId,
|
|
55
|
+
environment
|
|
56
|
+
};
|
|
57
|
+
for (const topic of requested) {
|
|
58
|
+
const match = matchChannel(topic, channels);
|
|
59
|
+
const refusal = match ? accessRefusal(match.declaration.access, req.ctx.user) : "undeclared";
|
|
60
|
+
const grant = match && !refusal && match.declaration.grant === true ? await grantFor(grants, space, topic, offered) : void 0;
|
|
61
|
+
if (!match || refusal) refused.push({
|
|
62
|
+
topic,
|
|
63
|
+
reason: refusal ?? "undeclared"
|
|
64
|
+
});
|
|
65
|
+
else if (match.declaration.grant === true && grant === void 0) refused.push({
|
|
66
|
+
topic,
|
|
67
|
+
reason: "ungranted"
|
|
68
|
+
});
|
|
69
|
+
else {
|
|
70
|
+
accepted.set(topic, match.declaration);
|
|
71
|
+
if (grant !== void 0) granted.set(topic, grant);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (accepted.size === 0) return {
|
|
75
|
+
ok: false,
|
|
76
|
+
answer: {
|
|
77
|
+
status: 403,
|
|
78
|
+
error: "None of these topics may be opened here",
|
|
79
|
+
reason: "forbidden",
|
|
80
|
+
refused
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const { user } = req.ctx;
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
space,
|
|
87
|
+
...user ? { user: {
|
|
88
|
+
id: user.id,
|
|
89
|
+
name: user.username
|
|
90
|
+
} } : {},
|
|
91
|
+
accepted,
|
|
92
|
+
granted,
|
|
93
|
+
refused
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** A new connection for what was admitted, speaking through `send`. */
|
|
97
|
+
var connectionFor = (admission, send, end) => ({
|
|
98
|
+
id: randomUUID(),
|
|
99
|
+
token: randomBytes(24).toString("base64url"),
|
|
100
|
+
space: admission.space,
|
|
101
|
+
...admission.user ? { user: admission.user } : {},
|
|
102
|
+
topics: admission.accepted,
|
|
103
|
+
grants: admission.granted,
|
|
104
|
+
announced: /* @__PURE__ */ new Set(),
|
|
105
|
+
sent: /* @__PURE__ */ new Map(),
|
|
106
|
+
send,
|
|
107
|
+
end
|
|
108
|
+
});
|
|
109
|
+
/** Whether this connection may send one more message on `topic` this second. */
|
|
110
|
+
var withinRate = (connection, topic, perSecond) => {
|
|
111
|
+
const second = Math.floor(Date.now() / 1e3);
|
|
112
|
+
const sent = connection.sent.get(topic);
|
|
113
|
+
const count = sent?.second === second ? sent.count + 1 : 1;
|
|
114
|
+
connection.sent.set(topic, {
|
|
115
|
+
second,
|
|
116
|
+
count
|
|
117
|
+
});
|
|
118
|
+
return count <= perSecond;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* A page publishing, whichever way it arrived: only on a topic its connection subscribed to, only on a channel pages
|
|
122
|
+
* may send on, within its size and its rate — and stamped by the server with who sent it and when, whatever the
|
|
123
|
+
* message said.
|
|
124
|
+
*/
|
|
125
|
+
var publishFrom = async (hub, connection, { topic, type, data }) => {
|
|
126
|
+
const name = typeof topic === "string" ? topic : "";
|
|
127
|
+
const declaration = connection.topics.get(name);
|
|
128
|
+
if (!declaration) return {
|
|
129
|
+
status: 403,
|
|
130
|
+
error: `This connection did not subscribe to "${name}"`,
|
|
131
|
+
reason: "not_subscribed"
|
|
132
|
+
};
|
|
133
|
+
if (declaration.publish === "server") return {
|
|
134
|
+
status: 403,
|
|
135
|
+
error: `Only the server publishes on "${name}"`,
|
|
136
|
+
reason: "server_only"
|
|
137
|
+
};
|
|
138
|
+
const kind = typeof type === "string" ? type : "";
|
|
139
|
+
const presence = kind === PRESENCE_TYPE && declaration.presence === true;
|
|
140
|
+
if (!presence && !TYPE.test(kind)) return {
|
|
141
|
+
status: 422,
|
|
142
|
+
error: "A message type is 1-64 of A-Z a-z 0-9 _ . : -",
|
|
143
|
+
reason: "invalid_input"
|
|
144
|
+
};
|
|
145
|
+
const limits = channelLimits(declaration);
|
|
146
|
+
const value = data ?? null;
|
|
147
|
+
if (Buffer.byteLength(JSON.stringify(value)) > limits.maxMessageBytes) return {
|
|
148
|
+
status: 413,
|
|
149
|
+
error: `A message on "${name}" is at most ${limits.maxMessageBytes} bytes`,
|
|
150
|
+
reason: "too_large"
|
|
151
|
+
};
|
|
152
|
+
if (!withinRate(connection, name, limits.messagesPerSecond)) return {
|
|
153
|
+
status: 429,
|
|
154
|
+
error: `At most ${limits.messagesPerSecond} messages a second`,
|
|
155
|
+
reason: "rate_limited"
|
|
156
|
+
};
|
|
157
|
+
if (presence) connection.announced.add(name);
|
|
158
|
+
await hub.publish(connection.space, hub.from(connection, name, kind, value));
|
|
159
|
+
return { status: 204 };
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* `GET /_realtime?topics=a,b` (and `&grants=g1,g2` for the topics of a `grant: true` channel) — one Server-Sent Events connection for every topic a page listens to.
|
|
163
|
+
*
|
|
164
|
+
* The first event, `ready`, tells the page its public name (`from`) and the secret its publishes carry. Resolves when
|
|
165
|
+
* the page goes.
|
|
166
|
+
*/
|
|
167
|
+
var handleRealtimeSubscribe = async ({ req, res, raw, signal, hub, resolveChannels, grants }) => {
|
|
168
|
+
const admission = await admit(req, resolveChannels, grants);
|
|
169
|
+
if (!admission.ok) {
|
|
170
|
+
answer(res, admission.answer);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let finish = () => void 0;
|
|
174
|
+
const gone = new Promise((resolve) => {
|
|
175
|
+
finish = resolve;
|
|
176
|
+
});
|
|
177
|
+
const stream = openEventStream(raw, {
|
|
178
|
+
onAbort: () => finish(),
|
|
179
|
+
retryMs: RETRY_MS
|
|
180
|
+
});
|
|
181
|
+
const connection = connectionFor(admission, stream.send, () => finish());
|
|
182
|
+
const release = onAbort(signal, () => finish());
|
|
183
|
+
stream.send("ready", {
|
|
184
|
+
connection: connection.id,
|
|
185
|
+
token: connection.token,
|
|
186
|
+
topics: [...admission.accepted.keys()],
|
|
187
|
+
refused: admission.refused
|
|
188
|
+
});
|
|
189
|
+
await hub.connect(connection);
|
|
190
|
+
await gone;
|
|
191
|
+
release();
|
|
192
|
+
stream.close();
|
|
193
|
+
await hub.disconnect(connection);
|
|
194
|
+
};
|
|
195
|
+
var parseBody = (body) => {
|
|
196
|
+
try {
|
|
197
|
+
const value = body ? JSON.parse(body) : void 0;
|
|
198
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
199
|
+
} catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
/** `POST /_realtime` `{ token, topic, type, data }` — a page on a Server-Sent Events connection, publishing. */
|
|
204
|
+
var handleRealtimePublish = async ({ req, res, hub }) => {
|
|
205
|
+
const body = parseBody(req.body);
|
|
206
|
+
const connection = typeof body?.token === "string" ? hub.find(body.token) : void 0;
|
|
207
|
+
if (!body || !connection) {
|
|
208
|
+
answer(res, {
|
|
209
|
+
status: 401,
|
|
210
|
+
error: "Not connected: open /_realtime first and publish with its token",
|
|
211
|
+
reason: "not_connected"
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const result = await publishFrom(hub, connection, body);
|
|
216
|
+
if (result.status === 204) {
|
|
217
|
+
res.setStatus(204);
|
|
218
|
+
res.send("");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
answer(res, result);
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
224
|
+
export { admit, answer, connectionFor, handleRealtimePublish, handleRealtimeSubscribe, publishFrom };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ChannelDeclaration, PubSubAdapter, RealtimeMessage, RealtimeSender } from '@plitzi/sdk-shared';
|
|
2
|
+
/** The space a connection belongs to: its topics are that space's and no other's. */
|
|
3
|
+
export type RealtimeSpace = {
|
|
4
|
+
spaceId: number;
|
|
5
|
+
environment: string;
|
|
6
|
+
};
|
|
7
|
+
/** One page, connected. */
|
|
8
|
+
export type RealtimeConnection = {
|
|
9
|
+
/** Its public name — the `from` of everything it sends. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Its secret: what a publish proves it comes from this connection with. Never sent to anyone else. */
|
|
12
|
+
token: string;
|
|
13
|
+
space: RealtimeSpace;
|
|
14
|
+
user?: RealtimeSender['user'];
|
|
15
|
+
/** The topics it subscribed to, with the channel each falls under. */
|
|
16
|
+
topics: Map<string, ChannelDeclaration>;
|
|
17
|
+
/** The grant each topic of a `grant: true` channel was opened with — what a revoke names. */
|
|
18
|
+
grants: Map<string, string>;
|
|
19
|
+
/** The topics it announced itself on, which hear `$leave` when it goes. */
|
|
20
|
+
announced: Set<string>;
|
|
21
|
+
/** Messages sent this second, by topic — the rate a channel allows is per connection, and a connection is here. */
|
|
22
|
+
sent: Map<string, {
|
|
23
|
+
second: number;
|
|
24
|
+
count: number;
|
|
25
|
+
}>;
|
|
26
|
+
send: (event: string, data: unknown) => void;
|
|
27
|
+
/** Closes it from the server's side — the server is going away. Its own `close` then lets it go as any other. */
|
|
28
|
+
end: () => void;
|
|
29
|
+
};
|
|
30
|
+
/** A space's topic, as the adapter knows it: namespaced, so two spaces never hear each other. */
|
|
31
|
+
export declare const adapterTopic: ({ spaceId, environment }: RealtimeSpace, topic: string) => string;
|
|
32
|
+
export type RealtimeHub = ReturnType<typeof createRealtimeHub>;
|
|
33
|
+
/**
|
|
34
|
+
* This process's connections, and the ONE adapter subscription per topic they share.
|
|
35
|
+
*
|
|
36
|
+
* A hundred pages on one board are a hundred connections here and a single subscription to the adapter: a message
|
|
37
|
+
* arrives once and is written to each of them. The last one to leave a topic unsubscribes it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const createRealtimeHub: (pubsub: PubSubAdapter) => {
|
|
40
|
+
publish: (space: RealtimeSpace, message: RealtimeMessage) => Promise<void>;
|
|
41
|
+
from: (connection: RealtimeConnection, topic: string, type: string, data: unknown) => RealtimeMessage;
|
|
42
|
+
find: (token: string) => RealtimeConnection | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Lets go of `topic` every connection, on every replica, that opened it with `grant` — or with any grant, naming
|
|
45
|
+
* none. What stops a grant that is already in use; `grants.revoke` is what stops it being used again.
|
|
46
|
+
*/
|
|
47
|
+
revoke: (space: RealtimeSpace, topic: string, grant?: string) => Promise<void>;
|
|
48
|
+
/** Subscribes a new connection to its topics, and tells the members of each presence channel it arrived. */
|
|
49
|
+
connect: (connection: RealtimeConnection) => Promise<void>;
|
|
50
|
+
/** Unsubscribes a connection that went away, and tells the channels it announced itself on. */
|
|
51
|
+
disconnect: (connection: RealtimeConnection) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Every connection closed from here: the server is shutting down. An open socket or event stream is a request that
|
|
54
|
+
* never ends, so a server waiting for them to finish waited for ever — each is told to go, and goes the way it
|
|
55
|
+
* goes when its page leaves.
|
|
56
|
+
*/
|
|
57
|
+
closeAll: () => void;
|
|
58
|
+
/** How many connections this process holds — for tests and for a health endpoint. */
|
|
59
|
+
readonly connectionCount: number;
|
|
60
|
+
/** How many topics this process holds a subscription for — for tests and for a health endpoint. */
|
|
61
|
+
readonly topicCount: number;
|
|
62
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { JOIN_TYPE, LEAVE_TYPE, REVOKED_TYPE } from "@plitzi/sdk-shared/realtime";
|
|
2
|
+
//#region src/modules/realtime/hub.ts
|
|
3
|
+
/** A space's topic, as the adapter knows it: namespaced, so two spaces never hear each other. */
|
|
4
|
+
var adapterTopic = ({ spaceId, environment }, topic) => `${spaceId}/${environment}/${topic}`;
|
|
5
|
+
/**
|
|
6
|
+
* Said between the servers, never to a page: every connection opened with the grant it names — or, naming none, with
|
|
7
|
+
* any grant for the topic — is let go of it. On the topic's own adapter channel, so it reaches every replica holding
|
|
8
|
+
* the topic, and only them. A page cannot send it: the types a page or a flow may use have no `$` but `$presence`.
|
|
9
|
+
*/
|
|
10
|
+
var REVOKE_TYPE = "$revoke";
|
|
11
|
+
var revokedGrant = (data) => typeof data === "object" && data !== null && "grant" in data && typeof data.grant === "string" ? data.grant : void 0;
|
|
12
|
+
/** A message as the adapter carried it — written by this module, but read from a transport that is not. */
|
|
13
|
+
var isMessage = (value) => typeof value === "object" && value !== null && "topic" in value && typeof value.topic === "string" && "type" in value && typeof value.type === "string" && "from" in value && typeof value.from === "string" && "at" in value && typeof value.at === "number";
|
|
14
|
+
var parse = (raw) => {
|
|
15
|
+
try {
|
|
16
|
+
const value = JSON.parse(raw);
|
|
17
|
+
return isMessage(value) ? value : void 0;
|
|
18
|
+
} catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* This process's connections, and the ONE adapter subscription per topic they share.
|
|
24
|
+
*
|
|
25
|
+
* A hundred pages on one board are a hundred connections here and a single subscription to the adapter: a message
|
|
26
|
+
* arrives once and is written to each of them. The last one to leave a topic unsubscribes it.
|
|
27
|
+
*/
|
|
28
|
+
var createRealtimeHub = (pubsub) => {
|
|
29
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
30
|
+
const local = /* @__PURE__ */ new Map();
|
|
31
|
+
const publish = (space, message) => pubsub.publish(adapterTopic(space, message.topic), JSON.stringify(message));
|
|
32
|
+
const join = async (connection, topic) => {
|
|
33
|
+
const key = adapterTopic(connection.space, topic);
|
|
34
|
+
let entry = local.get(key);
|
|
35
|
+
if (!entry) {
|
|
36
|
+
const connections = /* @__PURE__ */ new Set();
|
|
37
|
+
entry = {
|
|
38
|
+
connections,
|
|
39
|
+
release: pubsub.subscribe(key, (raw) => {
|
|
40
|
+
const message = parse(raw);
|
|
41
|
+
if (message?.type === REVOKE_TYPE) {
|
|
42
|
+
const grant = revokedGrant(message.data);
|
|
43
|
+
[...connections].filter((member) => {
|
|
44
|
+
const opened = member.grants.get(message.topic);
|
|
45
|
+
return opened !== void 0 && (grant === void 0 || opened === grant);
|
|
46
|
+
}).forEach((member) => void releaseTopic(member, message.topic));
|
|
47
|
+
} else if (message) connections.forEach((member) => member.send("message", message));
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
local.set(key, entry);
|
|
51
|
+
}
|
|
52
|
+
entry.connections.add(connection);
|
|
53
|
+
await entry.release;
|
|
54
|
+
};
|
|
55
|
+
const leave = async (connection, topic) => {
|
|
56
|
+
const key = adapterTopic(connection.space, topic);
|
|
57
|
+
const entry = local.get(key);
|
|
58
|
+
if (!entry) return;
|
|
59
|
+
entry.connections.delete(connection);
|
|
60
|
+
if (entry.connections.size === 0) {
|
|
61
|
+
local.delete(key);
|
|
62
|
+
await (await entry.release)();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/** A stamped message from this connection: `from` and `user` are the server's, whatever was sent. */
|
|
66
|
+
const from = (connection, topic, type, data) => ({
|
|
67
|
+
topic,
|
|
68
|
+
type,
|
|
69
|
+
data,
|
|
70
|
+
from: connection.id,
|
|
71
|
+
...connection.user ? { user: connection.user } : {},
|
|
72
|
+
at: Date.now()
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Lets `connection` go of `topic`: it is told, the others on a presence channel hear it leave, and it no longer
|
|
76
|
+
* hears the topic or may send on it.
|
|
77
|
+
*/
|
|
78
|
+
const releaseTopic = async (connection, topic) => {
|
|
79
|
+
connection.send("message", {
|
|
80
|
+
topic,
|
|
81
|
+
type: REVOKED_TYPE,
|
|
82
|
+
data: null,
|
|
83
|
+
from: "server",
|
|
84
|
+
at: Date.now()
|
|
85
|
+
});
|
|
86
|
+
connection.topics.delete(topic);
|
|
87
|
+
connection.grants.delete(topic);
|
|
88
|
+
await leave(connection, topic);
|
|
89
|
+
if (connection.announced.delete(topic)) await publish(connection.space, from(connection, topic, LEAVE_TYPE, null));
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
publish,
|
|
93
|
+
from,
|
|
94
|
+
find: (token) => byToken.get(token),
|
|
95
|
+
/**
|
|
96
|
+
* Lets go of `topic` every connection, on every replica, that opened it with `grant` — or with any grant, naming
|
|
97
|
+
* none. What stops a grant that is already in use; `grants.revoke` is what stops it being used again.
|
|
98
|
+
*/
|
|
99
|
+
revoke: (space, topic, grant) => publish(space, {
|
|
100
|
+
topic,
|
|
101
|
+
type: REVOKE_TYPE,
|
|
102
|
+
data: grant === void 0 ? null : { grant },
|
|
103
|
+
from: "server",
|
|
104
|
+
at: Date.now()
|
|
105
|
+
}),
|
|
106
|
+
/** Subscribes a new connection to its topics, and tells the members of each presence channel it arrived. */
|
|
107
|
+
connect: async (connection) => {
|
|
108
|
+
byToken.set(connection.token, connection);
|
|
109
|
+
for (const [topic, declaration] of connection.topics) {
|
|
110
|
+
await join(connection, topic);
|
|
111
|
+
if (declaration.presence) await publish(connection.space, from(connection, topic, JOIN_TYPE, null));
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
/** Unsubscribes a connection that went away, and tells the channels it announced itself on. */
|
|
115
|
+
disconnect: async (connection) => {
|
|
116
|
+
byToken.delete(connection.token);
|
|
117
|
+
for (const topic of connection.topics.keys()) {
|
|
118
|
+
if (connection.announced.has(topic)) await publish(connection.space, from(connection, topic, LEAVE_TYPE, null));
|
|
119
|
+
await leave(connection, topic);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Every connection closed from here: the server is shutting down. An open socket or event stream is a request that
|
|
124
|
+
* never ends, so a server waiting for them to finish waited for ever — each is told to go, and goes the way it
|
|
125
|
+
* goes when its page leaves.
|
|
126
|
+
*/
|
|
127
|
+
closeAll: () => {
|
|
128
|
+
[...byToken.values()].forEach((connection) => connection.end());
|
|
129
|
+
},
|
|
130
|
+
/** How many connections this process holds — for tests and for a health endpoint. */
|
|
131
|
+
get connectionCount() {
|
|
132
|
+
return byToken.size;
|
|
133
|
+
},
|
|
134
|
+
/** How many topics this process holds a subscription for — for tests and for a health endpoint. */
|
|
135
|
+
get topicCount() {
|
|
136
|
+
return local.size;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
//#endregion
|
|
141
|
+
export { adapterTopic, createRealtimeHub };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ChannelResolver } from './declarations';
|
|
2
|
+
import { RealtimeGrants } from './grants';
|
|
3
|
+
import { RealtimeHub } from './hub';
|
|
4
|
+
import { RealtimeTransport, SSRServerConfig } from '@plitzi/sdk-shared';
|
|
5
|
+
export { createMemoryPubSub } from './memoryPubSub';
|
|
6
|
+
export { handleRealtimePublish, handleRealtimeSubscribe } from './handlers';
|
|
7
|
+
export { handleRealtimeSocket } from './socket';
|
|
8
|
+
export type RealtimeModule = {
|
|
9
|
+
hub: RealtimeHub;
|
|
10
|
+
path: string;
|
|
11
|
+
/** What pages are told to connect with. Both are served whatever this says. */
|
|
12
|
+
transport: RealtimeTransport;
|
|
13
|
+
/** Origins besides the server's own whose pages may open a socket. */
|
|
14
|
+
allowedOrigins: readonly string[];
|
|
15
|
+
resolveChannels: ChannelResolver;
|
|
16
|
+
/** The grants issued for topics of channels declared `grant: true`, and the check a subscription makes. */
|
|
17
|
+
grants: RealtimeGrants;
|
|
18
|
+
/** Closes every connection this process holds: the server is shutting down (`HttpServerParts.onClosing`). */
|
|
19
|
+
close: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Publishes from the server — what a flow's `realtime.publish` does. Checked against the space's channels like a
|
|
22
|
+
* page's publish, and sent `from: 'server'`: the one sender a page cannot be.
|
|
23
|
+
*/
|
|
24
|
+
publish: (space: {
|
|
25
|
+
spaceId: number;
|
|
26
|
+
environment: string;
|
|
27
|
+
}, topic: string, type: string, data: unknown) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* A grant for one topic of a channel declared `grant: true` — what a flow's `realtime.grant` hands a page, once it
|
|
30
|
+
* has decided the visitor may be there. Refused for a topic of any other channel: a grant it would never ask for
|
|
31
|
+
* is a mistake in the flow, not a key to hand out.
|
|
32
|
+
*/
|
|
33
|
+
grant: (space: {
|
|
34
|
+
spaceId: number;
|
|
35
|
+
environment: string;
|
|
36
|
+
}, topic: string, ttlSeconds?: number) => Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* `grant` no longer opens `topic` — or, naming none, no grant issued for it until now does: nobody who holds one gets
|
|
39
|
+
* in again, and whoever is on the topic with one, on any replica, is let go of it (`$revoked`). A page kept out asks
|
|
40
|
+
* the action that lets it in for a new grant, if it still may.
|
|
41
|
+
*/
|
|
42
|
+
revoke: (space: {
|
|
43
|
+
spaceId: number;
|
|
44
|
+
environment: string;
|
|
45
|
+
}, topic: string, grant?: string) => Promise<void>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The server's realtime channels, built once per config — or `undefined` when they are off or cannot be: without the
|
|
49
|
+
* space's documents (`getOfflineData`) there is nothing to say which channels a space declares, so none is opened.
|
|
50
|
+
*/
|
|
51
|
+
export declare const realtimeModuleFor: (config: SSRServerConfig) => RealtimeModule | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { fleetStore } from "../../core/server/fleet/link.js";
|
|
2
|
+
import { KV_METHODS, createMemoryKv } from "../actions/runtime/memoryKv.js";
|
|
3
|
+
import { createRealtimeGrants } from "./grants.js";
|
|
4
|
+
import { createChannelResolver } from "./declarations.js";
|
|
5
|
+
import { createRealtimeHub } from "./hub.js";
|
|
6
|
+
import { createMemoryPubSub } from "./memoryPubSub.js";
|
|
7
|
+
import { handleRealtimePublish, handleRealtimeSubscribe } from "./handlers.js";
|
|
8
|
+
import { handleRealtimeSocket } from "./socket.js";
|
|
9
|
+
import { matchChannel } from "@plitzi/sdk-shared/realtime";
|
|
10
|
+
//#region src/modules/realtime/index.ts
|
|
11
|
+
/** A type a flow publishes: its own vocabulary — the `$` types are the channel's. */
|
|
12
|
+
var SERVER_TYPE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
13
|
+
var modules = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* The server's realtime channels, built once per config — or `undefined` when they are off or cannot be: without the
|
|
16
|
+
* space's documents (`getOfflineData`) there is nothing to say which channels a space declares, so none is opened.
|
|
17
|
+
*/
|
|
18
|
+
var realtimeModuleFor = (config) => {
|
|
19
|
+
const getOfflineData = config.adapters.getOfflineData;
|
|
20
|
+
if (config.realtime === false || !getOfflineData) return;
|
|
21
|
+
const existing = modules.get(config);
|
|
22
|
+
if (existing) return existing;
|
|
23
|
+
const hub = createRealtimeHub(config.realtime?.pubsub ?? createMemoryPubSub());
|
|
24
|
+
const resolveChannels = createChannelResolver(getOfflineData);
|
|
25
|
+
const grants = createRealtimeGrants(config.action?.kv ?? fleetStore("realtime.grants", KV_METHODS) ?? createMemoryKv());
|
|
26
|
+
const module = {
|
|
27
|
+
hub,
|
|
28
|
+
path: config.realtime?.path ?? "/_realtime",
|
|
29
|
+
transport: config.realtime?.transport ?? "sse",
|
|
30
|
+
allowedOrigins: config.realtime?.allowedOrigins ?? [],
|
|
31
|
+
resolveChannels,
|
|
32
|
+
grants,
|
|
33
|
+
close: () => hub.closeAll(),
|
|
34
|
+
publish: async (space, topic, type, data) => {
|
|
35
|
+
const channels = await resolveChannels(space.spaceId, space.environment);
|
|
36
|
+
if (!matchChannel(topic, channels)) throw new Error(`No channel of this space matches "${topic}": declare it in the space's \`channels\``);
|
|
37
|
+
if (!SERVER_TYPE.test(type)) throw new Error("A message type is 1-64 of A-Z a-z 0-9 _ . : -");
|
|
38
|
+
await hub.publish(space, {
|
|
39
|
+
topic,
|
|
40
|
+
type,
|
|
41
|
+
data,
|
|
42
|
+
from: "server",
|
|
43
|
+
at: Date.now()
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
grant: async (space, topic, ttlSeconds) => {
|
|
47
|
+
const match = matchChannel(topic, await resolveChannels(space.spaceId, space.environment));
|
|
48
|
+
if (!match) throw new Error(`No channel of this space matches "${topic}": declare it in the space's \`channels\``);
|
|
49
|
+
if (match.declaration.grant !== true) throw new Error(`"${match.pattern}" is open to anyone its access lets in, so it takes no grant: declare it \`grant: true\` to make its topics private`);
|
|
50
|
+
return grants.issue(space, topic, ttlSeconds);
|
|
51
|
+
},
|
|
52
|
+
revoke: async (space, topic, grant) => {
|
|
53
|
+
if (matchChannel(topic, await resolveChannels(space.spaceId, space.environment))?.declaration.grant !== true) throw new Error(`"${topic}" is not a topic of a private (\`grant: true\`) channel of this space: it has no grants to revoke`);
|
|
54
|
+
await grants.revoke(space, topic, grant);
|
|
55
|
+
await hub.revoke(space, topic, grant);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
modules.set(config, module);
|
|
59
|
+
return module;
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { handleRealtimePublish, handleRealtimeSocket, handleRealtimeSubscribe, realtimeModuleFor };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PubSubAdapter } from '@plitzi/sdk-shared';
|
|
2
|
+
/**
|
|
3
|
+
* The default pub/sub: in this process, and across its workers.
|
|
4
|
+
*
|
|
5
|
+
* A page connected to one worker must hear what another worker published, so a publish is delivered here AND sent to
|
|
6
|
+
* the rest of the fleet as a broadcast, which each process delivers to its own listeners. Without workers the
|
|
7
|
+
* broadcast goes nowhere and this is a Map of listeners. A second REPLICA shares nothing with this one: a deployment
|
|
8
|
+
* that runs several hands the server an adapter over something shared (Redis, NATS…).
|
|
9
|
+
*/
|
|
10
|
+
export declare const createMemoryPubSub: () => PubSubAdapter;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { broadcast, channelName, onBroadcast } from "../../core/server/fleet/link.js";
|
|
2
|
+
//#region src/modules/realtime/memoryPubSub.ts
|
|
3
|
+
var isRelayed = (value) => typeof value === "object" && value !== null && "topic" in value && typeof value.topic === "string" && "message" in value && typeof value.message === "string";
|
|
4
|
+
/**
|
|
5
|
+
* The default pub/sub: in this process, and across its workers.
|
|
6
|
+
*
|
|
7
|
+
* A page connected to one worker must hear what another worker published, so a publish is delivered here AND sent to
|
|
8
|
+
* the rest of the fleet as a broadcast, which each process delivers to its own listeners. Without workers the
|
|
9
|
+
* broadcast goes nowhere and this is a Map of listeners. A second REPLICA shares nothing with this one: a deployment
|
|
10
|
+
* that runs several hands the server an adapter over something shared (Redis, NATS…).
|
|
11
|
+
*/
|
|
12
|
+
var createMemoryPubSub = () => {
|
|
13
|
+
const topics = /* @__PURE__ */ new Map();
|
|
14
|
+
const fleet = channelName("realtime.pubsub");
|
|
15
|
+
const deliver = (topic, message) => {
|
|
16
|
+
for (const listener of topics.get(topic) ?? []) listener(message);
|
|
17
|
+
};
|
|
18
|
+
onBroadcast(fleet, (payload) => {
|
|
19
|
+
if (isRelayed(payload)) deliver(payload.topic, payload.message);
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
publish: (topic, message) => {
|
|
23
|
+
deliver(topic, message);
|
|
24
|
+
broadcast(fleet, {
|
|
25
|
+
topic,
|
|
26
|
+
message
|
|
27
|
+
});
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
},
|
|
30
|
+
subscribe: (topic, listener) => {
|
|
31
|
+
const listeners = topics.get(topic) ?? /* @__PURE__ */ new Set();
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
topics.set(topic, listeners);
|
|
34
|
+
return Promise.resolve(() => {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
if (listeners.size === 0) topics.delete(topic);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createMemoryPubSub };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PubSubAdapter } from '@plitzi/sdk-shared';
|
|
2
|
+
/**
|
|
3
|
+
* The two Redis connections pub/sub needs, by shape — `ioredis` fits as it is, and so does anything answering the same
|
|
4
|
+
* calls. A connection in subscriber mode can do nothing else, which is why it is a second one.
|
|
5
|
+
*/
|
|
6
|
+
export type RedisPubSubClients = {
|
|
7
|
+
publisher: {
|
|
8
|
+
publish: (channel: string, message: string) => Promise<unknown>;
|
|
9
|
+
};
|
|
10
|
+
subscriber: {
|
|
11
|
+
subscribe: (channel: string) => Promise<unknown>;
|
|
12
|
+
unsubscribe: (channel: string) => Promise<unknown>;
|
|
13
|
+
on: (event: 'message', listener: (channel: string, message: string) => void) => unknown;
|
|
14
|
+
};
|
|
15
|
+
/** Prefixed onto every channel, so the realtime topics cannot collide with anything else on the server. */
|
|
16
|
+
prefix?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Pub/sub over Redis: what a deployment of several replicas hands `createServer({ realtime: { pubsub } })`.
|
|
20
|
+
*
|
|
21
|
+
* One Redis subscription per topic per process, however many pages of that process listen to it: the listeners are
|
|
22
|
+
* kept here, and the channel is subscribed on the first and unsubscribed with the last.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createRedisPubSub: ({ publisher, subscriber, prefix }: RedisPubSubClients) => PubSubAdapter;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/modules/realtime/redisPubSub.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pub/sub over Redis: what a deployment of several replicas hands `createServer({ realtime: { pubsub } })`.
|
|
4
|
+
*
|
|
5
|
+
* One Redis subscription per topic per process, however many pages of that process listen to it: the listeners are
|
|
6
|
+
* kept here, and the channel is subscribed on the first and unsubscribed with the last.
|
|
7
|
+
*/
|
|
8
|
+
var createRedisPubSub = ({ publisher, subscriber, prefix = "plitzi:rt:" }) => {
|
|
9
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
10
|
+
subscriber.on("message", (channel, message) => {
|
|
11
|
+
if (!channel.startsWith(prefix)) return;
|
|
12
|
+
listeners.get(channel.slice(prefix.length))?.forEach((listener) => listener(message));
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
publish: async (topic, message) => {
|
|
16
|
+
await publisher.publish(`${prefix}${topic}`, message);
|
|
17
|
+
},
|
|
18
|
+
subscribe: async (topic, listener) => {
|
|
19
|
+
const set = listeners.get(topic) ?? /* @__PURE__ */ new Set();
|
|
20
|
+
if (!set.size) {
|
|
21
|
+
listeners.set(topic, set);
|
|
22
|
+
await subscriber.subscribe(`${prefix}${topic}`);
|
|
23
|
+
}
|
|
24
|
+
set.add(listener);
|
|
25
|
+
return async () => {
|
|
26
|
+
set.delete(listener);
|
|
27
|
+
if (!set.size) {
|
|
28
|
+
listeners.delete(topic);
|
|
29
|
+
await subscriber.unsubscribe(`${prefix}${topic}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { createRedisPubSub };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|