@webiny/api-event-handler-standalone 0.0.0-unstable.5387e5f7c8 → 0.0.0-unstable.d0f34a8194

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.
@@ -0,0 +1,26 @@
1
+ import type { PublicUrl } from "@webiny/api-core/features/publicUrls/index.js";
2
+ export interface IStandaloneConfig {
3
+ /** `WEBINY_API_URL`. Required. */
4
+ readonly apiUrl: PublicUrl;
5
+ /** `WEBINY_ADMIN_URL`. Optional: unknown when not set. */
6
+ readonly adminUrl: PublicUrl;
7
+ readonly fileStorage: {
8
+ /** `WEBINY_LOCAL_STORAGE_PATH`, resolved to an absolute path. Required. */
9
+ readonly storagePath: string;
10
+ /** `WEBINY_UPLOAD_SECRET`. Required. */
11
+ readonly uploadSecret: string;
12
+ };
13
+ /**
14
+ * `WEBINY_REDIS_URL`. Optional. When set, websocket messages travel between API processes
15
+ * through this Redis or Valkey server; required when more than one API process runs.
16
+ */
17
+ readonly redisUrl: string | undefined;
18
+ }
19
+ /**
20
+ * Deployment-specific settings of a standalone API, read from env once when the server starts.
21
+ * Nothing here is baked into the build, so one build runs in every environment.
22
+ */
23
+ export declare const StandaloneConfig: import("@webiny/di").Abstraction<IStandaloneConfig>;
24
+ export declare namespace StandaloneConfig {
25
+ type Interface = IStandaloneConfig;
26
+ }
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const StandaloneConfig = createAbstraction("Standalone/Config");
3
+ export { StandaloneConfig };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config/abstractions.js","sources":["../../src/config/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { PublicUrl } from \"@webiny/api-core/features/publicUrls/index.js\";\n\nexport interface IStandaloneConfig {\n /** `WEBINY_API_URL`. Required. */\n readonly apiUrl: PublicUrl;\n /** `WEBINY_ADMIN_URL`. Optional: unknown when not set. */\n readonly adminUrl: PublicUrl;\n readonly fileStorage: {\n /** `WEBINY_LOCAL_STORAGE_PATH`, resolved to an absolute path. Required. */\n readonly storagePath: string;\n /** `WEBINY_UPLOAD_SECRET`. Required. */\n readonly uploadSecret: string;\n };\n /**\n * `WEBINY_REDIS_URL`. Optional. When set, websocket messages travel between API processes\n * through this Redis or Valkey server; required when more than one API process runs.\n */\n readonly redisUrl: string | undefined;\n}\n\n/**\n * Deployment-specific settings of a standalone API, read from env once when the server starts.\n * Nothing here is baked into the build, so one build runs in every environment.\n */\nexport const StandaloneConfig = createAbstraction<IStandaloneConfig>(\"Standalone/Config\");\n\nexport namespace StandaloneConfig {\n export type Interface = IStandaloneConfig;\n}\n"],"names":["StandaloneConfig","createAbstraction"],"mappings":";AAyBO,MAAMA,mBAAmBC,kBAAqC"}
@@ -0,0 +1,12 @@
1
+ import type { StandaloneConfig } from "./abstractions.js";
2
+ type Env = Record<string, string | undefined>;
3
+ /**
4
+ * Reads the standalone API's settings from env. Throws one error naming every missing or invalid
5
+ * variable, so a misconfigured container fails at boot instead of on the first upload or login.
6
+ *
7
+ * Nothing falls back to a default: `webiny watch` and `webiny serve` get their values from
8
+ * `<Infra.ApiUrl>`, `<Infra.AdminUrl>` and `<Infra.FileStorage>` in webiny.config, and a deployed
9
+ * container must set them explicitly.
10
+ */
11
+ export declare function readStandaloneConfig(env: Env): StandaloneConfig.Interface;
12
+ export {};
@@ -0,0 +1,38 @@
1
+ import node_path from "node:path";
2
+ import { PublicUrl } from "@webiny/api-core/features/publicUrls/index.js";
3
+ function read(env, name) {
4
+ return env[name]?.trim() ?? "";
5
+ }
6
+ function readStandaloneConfig(env) {
7
+ const problems = [];
8
+ const apiUrlValue = read(env, "WEBINY_API_URL");
9
+ const apiUrl = PublicUrl.from(apiUrlValue);
10
+ if (apiUrlValue) {
11
+ if (!apiUrl.isKnown()) problems.push(`WEBINY_API_URL is not an absolute http(s) URL: "${apiUrlValue}".`);
12
+ } else problems.push("WEBINY_API_URL is not set.");
13
+ const adminUrlValue = read(env, "WEBINY_ADMIN_URL");
14
+ const adminUrl = PublicUrl.from(adminUrlValue);
15
+ if (adminUrlValue && !adminUrl.isKnown()) problems.push(`WEBINY_ADMIN_URL is not an absolute http(s) URL: "${adminUrlValue}".`);
16
+ const storagePath = read(env, "WEBINY_LOCAL_STORAGE_PATH");
17
+ if (!storagePath) problems.push("WEBINY_LOCAL_STORAGE_PATH is not set.");
18
+ const uploadSecret = read(env, "WEBINY_UPLOAD_SECRET");
19
+ if (!uploadSecret) problems.push("WEBINY_UPLOAD_SECRET is not set.");
20
+ const redisUrl = read(env, "WEBINY_REDIS_URL");
21
+ if (redisUrl && !/^rediss?:\/\//.test(redisUrl)) problems.push("WEBINY_REDIS_URL must start with redis:// or rediss://.");
22
+ if (problems.length > 0) throw new Error([
23
+ "The standalone API is missing required configuration:",
24
+ ...problems
25
+ ].join("\n "));
26
+ return {
27
+ apiUrl,
28
+ adminUrl,
29
+ fileStorage: {
30
+ storagePath: node_path.resolve(storagePath),
31
+ uploadSecret
32
+ },
33
+ redisUrl: redisUrl || void 0
34
+ };
35
+ }
36
+ export { readStandaloneConfig };
37
+
38
+ //# sourceMappingURL=readStandaloneConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config/readStandaloneConfig.js","sources":["../../src/config/readStandaloneConfig.ts"],"sourcesContent":["import nodePath from \"node:path\";\nimport { PublicUrl } from \"@webiny/api-core/features/publicUrls/index.js\";\nimport type { StandaloneConfig } from \"./abstractions.js\";\n\ntype Env = Record<string, string | undefined>;\n\nfunction read(env: Env, name: string): string {\n return env[name]?.trim() ?? \"\";\n}\n\n/**\n * Reads the standalone API's settings from env. Throws one error naming every missing or invalid\n * variable, so a misconfigured container fails at boot instead of on the first upload or login.\n *\n * Nothing falls back to a default: `webiny watch` and `webiny serve` get their values from\n * `<Infra.ApiUrl>`, `<Infra.AdminUrl>` and `<Infra.FileStorage>` in webiny.config, and a deployed\n * container must set them explicitly.\n */\nexport function readStandaloneConfig(env: Env): StandaloneConfig.Interface {\n const problems: string[] = [];\n\n const apiUrlValue = read(env, \"WEBINY_API_URL\");\n const apiUrl = PublicUrl.from(apiUrlValue);\n if (!apiUrlValue) {\n problems.push(\"WEBINY_API_URL is not set.\");\n } else if (!apiUrl.isKnown()) {\n problems.push(`WEBINY_API_URL is not an absolute http(s) URL: \"${apiUrlValue}\".`);\n }\n\n const adminUrlValue = read(env, \"WEBINY_ADMIN_URL\");\n const adminUrl = PublicUrl.from(adminUrlValue);\n if (adminUrlValue && !adminUrl.isKnown()) {\n problems.push(`WEBINY_ADMIN_URL is not an absolute http(s) URL: \"${adminUrlValue}\".`);\n }\n\n const storagePath = read(env, \"WEBINY_LOCAL_STORAGE_PATH\");\n if (!storagePath) {\n problems.push(\"WEBINY_LOCAL_STORAGE_PATH is not set.\");\n }\n\n const uploadSecret = read(env, \"WEBINY_UPLOAD_SECRET\");\n if (!uploadSecret) {\n problems.push(\"WEBINY_UPLOAD_SECRET is not set.\");\n }\n\n const redisUrl = read(env, \"WEBINY_REDIS_URL\");\n if (redisUrl && !/^rediss?:\\/\\//.test(redisUrl)) {\n problems.push(\"WEBINY_REDIS_URL must start with redis:// or rediss://.\");\n }\n\n if (problems.length > 0) {\n throw new Error(\n [\"The standalone API is missing required configuration:\", ...problems].join(\"\\n \")\n );\n }\n\n return {\n apiUrl,\n adminUrl,\n fileStorage: {\n storagePath: nodePath.resolve(storagePath),\n uploadSecret\n },\n redisUrl: redisUrl || undefined\n };\n}\n"],"names":["read","env","name","readStandaloneConfig","problems","apiUrlValue","apiUrl","PublicUrl","adminUrlValue","adminUrl","storagePath","uploadSecret","redisUrl","Error","nodePath","undefined"],"mappings":";;AAMA,SAASA,KAAKC,GAAQ,EAAEC,IAAY;IAChC,OAAOD,GAAG,CAACC,KAAK,EAAE,UAAU;AAChC;AAUO,SAASC,qBAAqBF,GAAQ;IACzC,MAAMG,WAAqB,EAAE;IAE7B,MAAMC,cAAcL,KAAKC,KAAK;IAC9B,MAAMK,SAASC,UAAU,IAAI,CAACF;IAC9B,IAAKA,aAEE;QAAA,IAAI,CAACC,OAAO,OAAO,IACtBF,SAAS,IAAI,CAAC,CAAC,gDAAgD,EAAEC,YAAY,EAAE,CAAC;IACpF,OAHID,SAAS,IAAI,CAAC;IAKlB,MAAMI,gBAAgBR,KAAKC,KAAK;IAChC,MAAMQ,WAAWF,UAAU,IAAI,CAACC;IAChC,IAAIA,iBAAiB,CAACC,SAAS,OAAO,IAClCL,SAAS,IAAI,CAAC,CAAC,kDAAkD,EAAEI,cAAc,EAAE,CAAC;IAGxF,MAAME,cAAcV,KAAKC,KAAK;IAC9B,IAAI,CAACS,aACDN,SAAS,IAAI,CAAC;IAGlB,MAAMO,eAAeX,KAAKC,KAAK;IAC/B,IAAI,CAACU,cACDP,SAAS,IAAI,CAAC;IAGlB,MAAMQ,WAAWZ,KAAKC,KAAK;IAC3B,IAAIW,YAAY,CAAC,gBAAgB,IAAI,CAACA,WAClCR,SAAS,IAAI,CAAC;IAGlB,IAAIA,SAAS,MAAM,GAAG,GAClB,MAAM,IAAIS,MACN;QAAC;WAA4DT;KAAS,CAAC,IAAI,CAAC;IAIpF,OAAO;QACHE;QACAG;QACA,aAAa;YACT,aAAaK,UAAAA,OAAgB,CAACJ;YAC9BC;QACJ;QACA,UAAUC,YAAYG;IAC1B;AACJ"}
@@ -1,10 +1,11 @@
1
1
  import { GraphQLSchemaCacheFeature } from "@webiny/api-graphql";
2
2
  import { EncryptionKeyCacheFeature } from "@webiny/api-core/features/encryption/index.js";
3
3
  import { JwksStoreFeature } from "@webiny/api-core/idp/index.js";
4
- import { NodeHttpFeature, createServerHandler } from "@webiny/event-handler-standalone";
4
+ import { NodeHttpFeature, ProcessOwner, RandomProcessOwner, createServerHandler } from "@webiny/event-handler-standalone";
5
5
  import { registerApiRequestStack } from "@webiny/api-event-handler-core";
6
- import { NodeWsAdapter, ServerConnectionManager, WebsocketsConnectionManager, WebsocketsStandaloneFeature, attachWebsocketsServer } from "@webiny/api-websockets-standalone";
7
- import { BackgroundTasksStandaloneFeature } from "@webiny/background-tasks-standalone";
6
+ import { InProcessMessageBus, NodeWsAdapter, ServerConnectionManager, WebsocketsConnectionManager, WebsocketsMessageBus, WebsocketsServerAdapter, WebsocketsStandaloneFeature, attachWebsocketsServer, createBusMessageHandler } from "@webiny/api-websockets-standalone";
7
+ import { WebsocketsRedisFeature } from "@webiny/api-websockets-standalone-redis";
8
+ import { BackgroundTasksStandaloneFeature, startTaskRecovery, stopBackgroundTasks } from "@webiny/background-tasks-standalone";
8
9
  import { FileManagerStandaloneFeature } from "@webiny/api-file-manager-standalone";
9
10
  import { registerSchedulerServer, startSchedulerServer } from "./scheduler/schedulerServer.js";
10
11
  import { startBulkActionsServer } from "./bulkActions/bulkActionsServer.js";
@@ -13,9 +14,25 @@ import { NodeHttpTenantLoaderDecorator } from "./handlers/NodeHttpTenantLoaderDe
13
14
  import { NodeHttpAssumePermissionsDecorator } from "./handlers/NodeHttpAssumePermissionsDecorator.js";
14
15
  import { createWebsocketsAuthenticator } from "./websockets/createWebsocketsAuthenticator.js";
15
16
  import { EmptyTrashBinsFeature } from "@webiny/api-headless-cms-bulk-actions-standalone";
17
+ import { mkdirSync } from "node:fs";
18
+ import { ConfigPublicUrlsFeature } from "./publicUrls/feature.js";
19
+ import { StandaloneConfig } from "./config/abstractions.js";
20
+ import { readStandaloneConfig } from "./config/readStandaloneConfig.js";
21
+ import { consoleLogger } from "./logger/consoleLogger.js";
22
+ const SHUTDOWN_WAIT_MS = 6000;
16
23
  function createWebinyApiHandler(config) {
24
+ let stopTaskRecovery;
25
+ let stopScheduler;
26
+ let stopWebsockets;
27
+ let closeMessageBus;
28
+ const standaloneConfig = readStandaloneConfig(process.env);
17
29
  return createServerHandler({
18
30
  root: async (rootContainer)=>{
31
+ rootContainer.registerInstance(StandaloneConfig, standaloneConfig);
32
+ rootContainer.register(RandomProcessOwner).inSingletonScope();
33
+ mkdirSync(standaloneConfig.fileStorage.storagePath, {
34
+ recursive: true
35
+ });
19
36
  NodeHttpFeature.register(rootContainer);
20
37
  rootContainer.registerDecorator(NodeHttpIdentityLoaderDecorator);
21
38
  rootContainer.registerDecorator(NodeHttpTenantLoaderDecorator);
@@ -23,6 +40,11 @@ function createWebinyApiHandler(config) {
23
40
  await config.registerRootStorage(rootContainer);
24
41
  rootContainer.register(ServerConnectionManager).inSingletonScope();
25
42
  rootContainer.register(NodeWsAdapter).inSingletonScope();
43
+ if (standaloneConfig.redisUrl) WebsocketsRedisFeature.register(rootContainer, {
44
+ url: standaloneConfig.redisUrl,
45
+ logger: consoleLogger
46
+ });
47
+ else rootContainer.register(InProcessMessageBus).inSingletonScope();
26
48
  GraphQLSchemaCacheFeature.register(rootContainer);
27
49
  EncryptionKeyCacheFeature.register(rootContainer);
28
50
  JwksStoreFeature.register(rootContainer);
@@ -34,24 +56,62 @@ function createWebinyApiHandler(config) {
34
56
  await registerApiRequestStack(container, {
35
57
  extensions: config.extensions,
36
58
  transports: {
59
+ publicUrls: (requestContainer)=>{
60
+ ConfigPublicUrlsFeature.register(requestContainer);
61
+ },
37
62
  realtime: (requestContainer)=>{
38
63
  WebsocketsStandaloneFeature.register(requestContainer);
39
64
  },
40
65
  fileManager: (requestContainer)=>{
41
- FileManagerStandaloneFeature.register(requestContainer);
66
+ FileManagerStandaloneFeature.register(requestContainer, standaloneConfig.fileStorage);
42
67
  }
43
68
  }
44
69
  });
45
70
  },
46
71
  onServer: async (server, rootContainer)=>{
72
+ const connectionManager = rootContainer.resolve(WebsocketsConnectionManager);
47
73
  const websockets = attachWebsocketsServer({
48
74
  server,
49
- connectionManager: rootContainer.resolve(WebsocketsConnectionManager),
75
+ connectionManager,
50
76
  authenticate: createWebsocketsAuthenticator(rootContainer, config.extensions)
51
77
  });
52
78
  await websockets.start();
53
- await startSchedulerServer(rootContainer);
79
+ stopWebsockets = ()=>websockets.stop();
80
+ const messageBus = rootContainer.resolve(WebsocketsMessageBus);
81
+ const processOwner = rootContainer.resolve(ProcessOwner);
82
+ const adapter = rootContainer.resolve(WebsocketsServerAdapter);
83
+ const handleBusMessage = createBusMessageHandler({
84
+ connectionManager,
85
+ adapter,
86
+ logger: consoleLogger
87
+ });
88
+ await messageBus.subscribe(processOwner.id, handleBusMessage);
89
+ closeMessageBus = ()=>messageBus.close();
90
+ consoleLogger.info({
91
+ messageBus: standaloneConfig.redisUrl ? "redis" : "in-process"
92
+ }, "[websockets] using message bus");
93
+ stopScheduler = await startSchedulerServer(rootContainer);
54
94
  startBulkActionsServer(rootContainer);
95
+ stopTaskRecovery = startTaskRecovery(rootContainer);
96
+ },
97
+ onShutdown: async (rootContainer)=>{
98
+ stopTaskRecovery?.();
99
+ const tasks = stopBackgroundTasks(rootContainer, SHUTDOWN_WAIT_MS);
100
+ const scheduledActions = stopScheduler?.(SHUTDOWN_WAIT_MS);
101
+ const websockets = stopWebsockets?.();
102
+ const workDone = Promise.allSettled([
103
+ tasks,
104
+ scheduledActions
105
+ ]);
106
+ const messageBus = workDone.then(()=>closeMessageBus?.());
107
+ const results = await Promise.allSettled([
108
+ tasks,
109
+ scheduledActions,
110
+ websockets,
111
+ messageBus
112
+ ]);
113
+ const failed = results.find((result)=>"rejected" === result.status);
114
+ if (failed) throw failed.reason;
55
115
  }
56
116
  });
57
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"createWebinyApiHandler.js","sources":["../src/createWebinyApiHandler.ts"],"sourcesContent":["/**\n * Webiny API handler for the standalone Node HTTP server transport — storage-agnostic BASE.\n *\n * The ROOT container wires the Node HTTP transport (NodeHttpFeature = event type + router + HttpFeature)\n * plus the auth/tenant loader decorators (extract token / x-tenant from the IncomingMessage → shared\n * RequestIdentityLoader / RequestTenantLoader). The per-request feature stack is the transport-agnostic\n * `registerApiRequestStack` from `@webiny/api-event-handler-core` — the SAME stack the AWS handler uses.\n * The realtime transport hook installs the server WebSockets transport (vs AWS's API Gateway Management\n * API) with a SINGLE-PROCESS, in-process implementation. Background tasks and the Bree scheduler are\n * ROOT-container singletons (below) — mirroring how the AWS handler registers background tasks at root,\n * and the WebSockets connection manager — rather than per-request transports; the scheduler fires timers\n * in-process (vs AWS's EventBridge). The storage variant (and identity provider) is injected via\n * `registerRootStorage`.\n *\n * The identity provider (e.g. `@webiny/self-hosted-auth`'s JWT IdP) must be registered by the variant\n * in `registerRootStorage`, so the RequestIdentityLoader driven by the identity decorator can resolve it.\n */\nimport { GraphQLSchemaCacheFeature } from \"@webiny/api-graphql\";\nimport { EncryptionKeyCacheFeature } from \"@webiny/api-core/features/encryption/index.js\";\nimport { JwksStoreFeature } from \"@webiny/api-core/idp/index.js\";\nimport type { Container } from \"@webiny/di\";\nimport { createServerHandler, NodeHttpFeature } from \"@webiny/event-handler-standalone\";\nimport { registerExtensions } from \"@webiny/handler\";\nimport { registerApiRequestStack } from \"@webiny/api-event-handler-core\";\nimport {\n ServerConnectionManager,\n NodeWsAdapter,\n WebsocketsStandaloneFeature,\n WebsocketsConnectionManager,\n attachWebsocketsServer\n} from \"@webiny/api-websockets-standalone\";\nimport { BackgroundTasksStandaloneFeature } from \"@webiny/background-tasks-standalone\";\nimport { FileManagerStandaloneFeature } from \"@webiny/api-file-manager-standalone\";\nimport { registerSchedulerServer, startSchedulerServer } from \"~/scheduler/schedulerServer.js\";\nimport { startBulkActionsServer } from \"~/bulkActions/bulkActionsServer.js\";\nimport { NodeHttpIdentityLoaderDecorator } from \"~/handlers/NodeHttpIdentityLoaderDecorator.js\";\nimport { NodeHttpTenantLoaderDecorator } from \"~/handlers/NodeHttpTenantLoaderDecorator.js\";\nimport { NodeHttpAssumePermissionsDecorator } from \"~/handlers/NodeHttpAssumePermissionsDecorator.js\";\nimport { createWebsocketsAuthenticator } from \"~/websockets/createWebsocketsAuthenticator.js\";\nimport { EmptyTrashBinsFeature } from \"@webiny/api-headless-cms-bulk-actions-standalone\";\n\nexport interface CreateWebinyApiHandlerConfig {\n /**\n * Project-defined extensions, applied at register() time.\n */\n extensions: () => Parameters<typeof registerExtensions>[1];\n /**\n * Register ALL root-container storage for this deployment (the DB feature + CMS storage ops +\n * registries) AND the identity provider. Unlike the AWS base, the server base bakes in no DB or\n * IdP assumption — the variant supplies the complete storage + auth wiring.\n */\n registerRootStorage: (container: Container) => void | Promise<void>;\n}\n\nexport function createWebinyApiHandler(config: CreateWebinyApiHandlerConfig) {\n return createServerHandler({\n root: async rootContainer => {\n // ── Transport (Node HTTP) ──────────────────────────────────\n // NodeHttpFeature registers the event type + HttpFeature (router) + the routing terminal.\n NodeHttpFeature.register(rootContainer);\n\n // ── Tenant + auth (extract → shared load) ──────────────────\n // registerDecorator applies LATER registrations as the OUTER wrapper (whose execute()\n // runs first). TENANT must be established before IDENTITY: API-key authentication resolves\n // the key by tenant partition (ApiKeysRepository reads TenantContext.getTenant()), so\n // identity depends on the tenant; the reverse is not true (RequestTenantLoader has no\n // identity dependency). Register identity first (inner) and tenant last (outer) → tenant\n // runs, then identity, then the router.\n rootContainer.registerDecorator(NodeHttpIdentityLoaderDecorator);\n rootContainer.registerDecorator(NodeHttpTenantLoaderDecorator);\n\n // Preview-as. Mirrors the AWS handler.\n rootContainer.registerDecorator(NodeHttpAssumePermissionsDecorator);\n\n // ── Storage + identity provider (variant-supplied) ─────────\n await config.registerRootStorage(rootContainer);\n\n // ── WebSockets transport (root) ────────────────────────────\n // Live-socket registry + adapter live in the root as singletons: the connection manager\n // is shared between the upgrade acceptor (attachWebsocketsServer, below) and the\n // per-request ServerWebsocketsTransport that sends to those live sockets. The persistent\n // connection registry (ConnectionRegistry) is the storage variant's job (e.g. sql).\n rootContainer.register(ServerConnectionManager).inSingletonScope();\n rootContainer.register(NodeWsAdapter).inSingletonScope();\n\n // ── GraphQL schema cache (root) ────────────────────────────\n // Root, so a built schema outlives the request that built it.\n GraphQLSchemaCacheFeature.register(rootContainer);\n\n // ── Encryption key cache (root) ────────────────────────────\n // Root, so the scrypt-derived key is computed once per process instead of per request.\n EncryptionKeyCacheFeature.register(rootContainer);\n // Root, so identity providers' signing keys are fetched once per process, not per request.\n JwksStoreFeature.register(rootContainer);\n\n // ── Background tasks (root) ────────────────────────────────\n // Mirrors the AWS handler registering its background-task transport at root. Where AWS\n // has Step Functions invoke the Lambda once per iteration, a root task loop dispatches a\n // BackgroundTaskEvent per iteration through the EventDispatcher (registered by\n // createServerHandler), and each one runs in a fresh request container.\n BackgroundTasksStandaloneFeature.register(rootContainer);\n\n // ── Scheduler (root) ───────────────────────────────────────\n // The Bree scheduler is a single long-lived instance for ALL tenants, started once at boot\n // (onServer, below) — the counterpart of the WebSockets connection manager, NOT a per-request\n // transport. Registered here as SchedulerService (per-request create/update/delete during\n // mutations manipulate this one live timer set) plus the run/recover event handlers. When a\n // timer fires (outside any request) it dispatches a ScheduledActionEvent, handled in a fresh\n // request container by the same handler AWS uses.\n registerSchedulerServer(rootContainer);\n\n // ── Bulk actions (root) ───────────────────────────────────\n // Registers the EmptyTrashBinsEvent handler that the periodic trigger\n // (startBulkActionsServer, onServer below) dispatches.\n EmptyTrashBinsFeature.register(rootContainer);\n },\n\n child: async container => {\n // The transport-agnostic per-request stack. The realtime hook installs the server\n // WebSockets transport (overriding the domain's NullWebsocketsTransport); it resolves the\n // shared connection manager + adapter from the root. Scheduler is NOT a per-request\n // transport here — it's a root singleton wired in `root` + `onServer` (see above).\n await registerApiRequestStack(container, {\n extensions: config.extensions,\n // Why hooks (and not just registering the transports ourselves): `.register()` calls\n // are otherwise order-independent — you can register Features in any order, because\n // they only REGISTER, they don't RESOLVE during registration (resolution happens\n // later, in Initializers / SchemaFactories). The one thing that makes order matter is\n // a DEFAULT registration: e.g. `WebsocketsFeature` registers `NullWebsocketsTransport`\n // so the abstraction is always resolvable. Overriding it (last-registration-wins) means\n // our transport MUST be registered AFTER that Feature. These hooks are the seams\n // `registerApiRequestStack` provides for exactly that — each runs right after its Null\n // default, so the override is guaranteed without the caller knowing the internal order.\n transports: {\n // Server WebSockets transport; resolves the shared connection manager + adapter\n // from the root (registered as singletons above).\n realtime: requestContainer => {\n WebsocketsStandaloneFeature.register(requestContainer);\n },\n // File-manager storage transport: local disk. Where AWS uses S3 (+ a separate asset-\n // delivery Lambda), the single-process server stores files on disk and serves them\n // in-process — FileManagerStandaloneFeature registers local asset delivery (overriding\n // the domain's null impls), the upload/multipart HTTP routes, and disk file ops.\n fileManager: requestContainer => {\n FileManagerStandaloneFeature.register(requestContainer);\n }\n }\n });\n },\n\n onServer: async (server, rootContainer) => {\n // Attach the WebSockets upgrade handler to the running HTTP server, backed by the shared\n // (root) connection manager so request-time sends reach the live sockets.\n //\n // Authenticate each WebSocket connection from its `?token` JWT so it's registered under\n // the real identity (targeted server→client sends match by identity id). It's a callback\n // the handler supplies rather than something the WS server resolves itself — see\n // createWebsocketsAuthenticator for why (AuthenticationContext is per-request, unreachable\n // from the root container the WS server is attached to).\n const websockets = attachWebsocketsServer({\n server,\n connectionManager: rootContainer.resolve(WebsocketsConnectionManager),\n authenticate: createWebsocketsAuthenticator(rootContainer, config.extensions)\n });\n await websockets.start();\n\n // Start the in-process scheduler timers, then re-arm persisted schedules in the\n // background (see startSchedulerServer).\n await startSchedulerServer(rootContainer);\n\n // Start the periodic empty-trash-bin trigger.\n startBulkActionsServer(rootContainer);\n }\n });\n}\n"],"names":["createWebinyApiHandler","config","createServerHandler","rootContainer","NodeHttpFeature","NodeHttpIdentityLoaderDecorator","NodeHttpTenantLoaderDecorator","NodeHttpAssumePermissionsDecorator","ServerConnectionManager","NodeWsAdapter","GraphQLSchemaCacheFeature","EncryptionKeyCacheFeature","JwksStoreFeature","BackgroundTasksStandaloneFeature","registerSchedulerServer","EmptyTrashBinsFeature","container","registerApiRequestStack","requestContainer","WebsocketsStandaloneFeature","FileManagerStandaloneFeature","server","websockets","attachWebsocketsServer","WebsocketsConnectionManager","createWebsocketsAuthenticator","startSchedulerServer","startBulkActionsServer"],"mappings":";;;;;;;;;;;;;;;AAsDO,SAASA,uBAAuBC,MAAoC;IACvE,OAAOC,oBAAoB;QACvB,MAAM,OAAMC;YAGRC,gBAAgB,QAAQ,CAACD;YASzBA,cAAc,iBAAiB,CAACE;YAChCF,cAAc,iBAAiB,CAACG;YAGhCH,cAAc,iBAAiB,CAACI;YAGhC,MAAMN,OAAO,mBAAmB,CAACE;YAOjCA,cAAc,QAAQ,CAACK,yBAAyB,gBAAgB;YAChEL,cAAc,QAAQ,CAACM,eAAe,gBAAgB;YAItDC,0BAA0B,QAAQ,CAACP;YAInCQ,0BAA0B,QAAQ,CAACR;YAEnCS,iBAAiB,QAAQ,CAACT;YAO1BU,iCAAiC,QAAQ,CAACV;YAS1CW,wBAAwBX;YAKxBY,sBAAsB,QAAQ,CAACZ;QACnC;QAEA,OAAO,OAAMa;YAKT,MAAMC,wBAAwBD,WAAW;gBACrC,YAAYf,OAAO,UAAU;gBAU7B,YAAY;oBAGR,UAAUiB,CAAAA;wBACNC,4BAA4B,QAAQ,CAACD;oBACzC;oBAKA,aAAaA,CAAAA;wBACTE,6BAA6B,QAAQ,CAACF;oBAC1C;gBACJ;YACJ;QACJ;QAEA,UAAU,OAAOG,QAAQlB;YASrB,MAAMmB,aAAaC,uBAAuB;gBACtCF;gBACA,mBAAmBlB,cAAc,OAAO,CAACqB;gBACzC,cAAcC,8BAA8BtB,eAAeF,OAAO,UAAU;YAChF;YACA,MAAMqB,WAAW,KAAK;YAItB,MAAMI,qBAAqBvB;YAG3BwB,uBAAuBxB;QAC3B;IACJ;AACJ"}
1
+ {"version":3,"file":"createWebinyApiHandler.js","sources":["../src/createWebinyApiHandler.ts"],"sourcesContent":["/**\n * Webiny API handler for the standalone Node HTTP server transport — storage-agnostic BASE.\n *\n * The ROOT container wires the Node HTTP transport (NodeHttpFeature = event type + router + HttpFeature)\n * plus the auth/tenant loader decorators (extract token / x-tenant from the IncomingMessage → shared\n * RequestIdentityLoader / RequestTenantLoader). The per-request feature stack is the transport-agnostic\n * `registerApiRequestStack` from `@webiny/api-event-handler-core` — the SAME stack the AWS handler uses.\n * The realtime transport hook installs the server WebSockets transport (vs AWS's API Gateway Management\n * API) with a SINGLE-PROCESS, in-process implementation. Background tasks and the Bree scheduler are\n * ROOT-container singletons (below) — mirroring how the AWS handler registers background tasks at root,\n * and the WebSockets connection manager — rather than per-request transports; the scheduler fires timers\n * in-process (vs AWS's EventBridge). The storage variant (and identity provider) is injected via\n * `registerRootStorage`.\n *\n * The identity provider (e.g. `@webiny/self-hosted-auth`'s JWT IdP) must be registered by the variant\n * in `registerRootStorage`, so the RequestIdentityLoader driven by the identity decorator can resolve it.\n */\nimport { GraphQLSchemaCacheFeature } from \"@webiny/api-graphql\";\nimport { EncryptionKeyCacheFeature } from \"@webiny/api-core/features/encryption/index.js\";\nimport { JwksStoreFeature } from \"@webiny/api-core/idp/index.js\";\nimport type { Container } from \"@webiny/di\";\nimport { createServerHandler, NodeHttpFeature } from \"@webiny/event-handler-standalone\";\nimport { RandomProcessOwner } from \"@webiny/event-handler-standalone\";\nimport { registerExtensions } from \"@webiny/handler\";\nimport { registerApiRequestStack } from \"@webiny/api-event-handler-core\";\nimport {\n ServerConnectionManager,\n NodeWsAdapter,\n WebsocketsStandaloneFeature,\n WebsocketsConnectionManager,\n attachWebsocketsServer\n} from \"@webiny/api-websockets-standalone\";\nimport { WebsocketsServerAdapter } from \"@webiny/api-websockets-standalone\";\nimport { WebsocketsMessageBus } from \"@webiny/api-websockets-standalone\";\nimport { InProcessMessageBus } from \"@webiny/api-websockets-standalone\";\nimport { createBusMessageHandler } from \"@webiny/api-websockets-standalone\";\nimport { WebsocketsRedisFeature } from \"@webiny/api-websockets-standalone-redis\";\nimport { ProcessOwner } from \"@webiny/event-handler-standalone\";\nimport { BackgroundTasksStandaloneFeature } from \"@webiny/background-tasks-standalone\";\nimport { startTaskRecovery } from \"@webiny/background-tasks-standalone\";\nimport { stopBackgroundTasks } from \"@webiny/background-tasks-standalone\";\nimport { FileManagerStandaloneFeature } from \"@webiny/api-file-manager-standalone\";\nimport { registerSchedulerServer, startSchedulerServer } from \"~/scheduler/schedulerServer.js\";\nimport { startBulkActionsServer } from \"~/bulkActions/bulkActionsServer.js\";\nimport { NodeHttpIdentityLoaderDecorator } from \"~/handlers/NodeHttpIdentityLoaderDecorator.js\";\nimport { NodeHttpTenantLoaderDecorator } from \"~/handlers/NodeHttpTenantLoaderDecorator.js\";\nimport { NodeHttpAssumePermissionsDecorator } from \"~/handlers/NodeHttpAssumePermissionsDecorator.js\";\nimport { createWebsocketsAuthenticator } from \"~/websockets/createWebsocketsAuthenticator.js\";\nimport { EmptyTrashBinsFeature } from \"@webiny/api-headless-cms-bulk-actions-standalone\";\nimport { mkdirSync } from \"node:fs\";\nimport { ConfigPublicUrlsFeature } from \"~/publicUrls/feature.js\";\nimport { StandaloneConfig } from \"~/config/abstractions.js\";\nimport { readStandaloneConfig } from \"~/config/readStandaloneConfig.js\";\nimport { consoleLogger } from \"~/logger/consoleLogger.js\";\n\nexport interface CreateWebinyApiHandlerConfig {\n /**\n * Project-defined extensions, applied at register() time.\n */\n extensions: () => Parameters<typeof registerExtensions>[1];\n /**\n * Register ALL root-container storage for this deployment (the DB feature + CMS storage ops +\n * registries) AND the identity provider. Unlike the AWS base, the server base bakes in no DB or\n * IdP assumption — the variant supplies the complete storage + auth wiring.\n */\n registerRootStorage: (container: Container) => void | Promise<void>;\n}\n\n/*\n * The part of the 8 seconds a shutdown gets that running tasks and scheduled actions may spend\n * finishing or handing back; both wait at the same time. The rest leaves room to exit before a\n * process manager loses patience.\n */\nconst SHUTDOWN_WAIT_MS = 6000;\n\nexport function createWebinyApiHandler(config: CreateWebinyApiHandlerConfig) {\n let stopTaskRecovery: (() => void) | undefined;\n let stopScheduler: ((timeoutMs: number) => Promise<void>) | undefined;\n let stopWebsockets: (() => Promise<void>) | undefined;\n let closeMessageBus: (() => Promise<void>) | undefined;\n\n // Deployment-specific settings (public URLs, file storage) come from env, read once here so a\n // missing value stops the server at boot. Nothing deployment-specific is baked into the build.\n const standaloneConfig = readStandaloneConfig(process.env);\n\n return createServerHandler({\n root: async rootContainer => {\n rootContainer.registerInstance(StandaloneConfig, standaloneConfig);\n\n // Names this process in shared storage: task claims and websocket connections.\n rootContainer.register(RandomProcessOwner).inSingletonScope();\n mkdirSync(standaloneConfig.fileStorage.storagePath, { recursive: true });\n\n // ── Transport (Node HTTP) ──────────────────────────────────\n // NodeHttpFeature registers the event type + HttpFeature (router) + the routing terminal.\n NodeHttpFeature.register(rootContainer);\n\n // ── Tenant + auth (extract → shared load) ──────────────────\n // registerDecorator applies LATER registrations as the OUTER wrapper (whose execute()\n // runs first). TENANT must be established before IDENTITY: API-key authentication resolves\n // the key by tenant partition (ApiKeysRepository reads TenantContext.getTenant()), so\n // identity depends on the tenant; the reverse is not true (RequestTenantLoader has no\n // identity dependency). Register identity first (inner) and tenant last (outer) → tenant\n // runs, then identity, then the router.\n rootContainer.registerDecorator(NodeHttpIdentityLoaderDecorator);\n rootContainer.registerDecorator(NodeHttpTenantLoaderDecorator);\n\n // Preview-as. Mirrors the AWS handler.\n rootContainer.registerDecorator(NodeHttpAssumePermissionsDecorator);\n\n // ── Storage + identity provider (variant-supplied) ─────────\n await config.registerRootStorage(rootContainer);\n\n // ── WebSockets transport (root) ────────────────────────────\n // Live-socket registry + adapter live in the root as singletons: the connection manager\n // is shared between the upgrade acceptor (attachWebsocketsServer, below) and the\n // per-request ServerWebsocketsTransport that sends to those live sockets. The persistent\n // connection registry (ConnectionRegistry) is the storage variant's job (e.g. sql).\n rootContainer.register(ServerConnectionManager).inSingletonScope();\n rootContainer.register(NodeWsAdapter).inSingletonScope();\n\n /*\n * How a send reaches a socket another API process holds. Without a Redis URL there is\n * one process, and every socket is in it.\n */\n if (standaloneConfig.redisUrl) {\n WebsocketsRedisFeature.register(rootContainer, {\n url: standaloneConfig.redisUrl,\n logger: consoleLogger\n });\n } else {\n rootContainer.register(InProcessMessageBus).inSingletonScope();\n }\n\n // ── GraphQL schema cache (root) ────────────────────────────\n // Root, so a built schema outlives the request that built it.\n GraphQLSchemaCacheFeature.register(rootContainer);\n\n // ── Encryption key cache (root) ────────────────────────────\n // Root, so the scrypt-derived key is computed once per process instead of per request.\n EncryptionKeyCacheFeature.register(rootContainer);\n // Root, so identity providers' signing keys are fetched once per process, not per request.\n JwksStoreFeature.register(rootContainer);\n\n // ── Background tasks (root) ────────────────────────────────\n // Mirrors the AWS handler registering its background-task transport at root. Where AWS\n // has Step Functions invoke the Lambda once per iteration, a root task loop dispatches a\n // BackgroundTaskEvent per iteration through the EventDispatcher (registered by\n // createServerHandler), and each one runs in a fresh request container.\n BackgroundTasksStandaloneFeature.register(rootContainer);\n\n // ── Scheduler (root) ───────────────────────────────────────\n // The Bree scheduler is a single long-lived instance for ALL tenants, started once at boot\n // (onServer, below) — the counterpart of the WebSockets connection manager, NOT a per-request\n // transport. Registered here as SchedulerService (per-request create/update/delete during\n // mutations manipulate this one live timer set) plus the run/recover event handlers. When a\n // timer fires (outside any request) it dispatches a ScheduledActionEvent, handled in a fresh\n // request container by the same handler AWS uses.\n registerSchedulerServer(rootContainer);\n\n // ── Bulk actions (root) ───────────────────────────────────\n // Registers the EmptyTrashBinsEvent handler that the periodic trigger\n // (startBulkActionsServer, onServer below) dispatches.\n EmptyTrashBinsFeature.register(rootContainer);\n },\n\n child: async container => {\n // The transport-agnostic per-request stack. The realtime hook installs the server\n // WebSockets transport (overriding the domain's NullWebsocketsTransport); it resolves the\n // shared connection manager + adapter from the root. Scheduler is NOT a per-request\n // transport here — it's a root singleton wired in `root` + `onServer` (see above).\n await registerApiRequestStack(container, {\n extensions: config.extensions,\n // Why hooks (and not just registering the transports ourselves): `.register()` calls\n // are otherwise order-independent — you can register Features in any order, because\n // they only REGISTER, they don't RESOLVE during registration (resolution happens\n // later, in Initializers / SchemaFactories). The one thing that makes order matter is\n // a DEFAULT registration: e.g. `WebsocketsFeature` registers `NullWebsocketsTransport`\n // so the abstraction is always resolvable. Overriding it (last-registration-wins) means\n // our transport MUST be registered AFTER that Feature. These hooks are the seams\n // `registerApiRequestStack` provides for exactly that — each runs right after its Null\n // default, so the override is guaranteed without the caller knowing the internal order.\n transports: {\n // Public URLs: no deploy step to discover them from, so they come from\n // `WEBINY_API_URL` / `WEBINY_ADMIN_URL` (StandaloneConfig, registered on the root).\n publicUrls: requestContainer => {\n ConfigPublicUrlsFeature.register(requestContainer);\n },\n // Server WebSockets transport; resolves the shared connection manager + adapter\n // from the root (registered as singletons above).\n realtime: requestContainer => {\n WebsocketsStandaloneFeature.register(requestContainer);\n },\n // File-manager storage transport: local disk. Where AWS uses S3 (+ a separate asset-\n // delivery Lambda), the single-process server stores files on disk and serves them\n // in-process — FileManagerStandaloneFeature registers local asset delivery (overriding\n // the domain's null impls), the upload/multipart HTTP routes, and disk file ops.\n fileManager: requestContainer => {\n FileManagerStandaloneFeature.register(\n requestContainer,\n standaloneConfig.fileStorage\n );\n }\n }\n });\n },\n\n onServer: async (server, rootContainer) => {\n // Attach the WebSockets upgrade handler to the running HTTP server, backed by the shared\n // (root) connection manager so request-time sends reach the live sockets.\n //\n // Authenticate each WebSocket connection from its `?token` JWT so it's registered under\n // the real identity (targeted server→client sends match by identity id). It's a callback\n // the handler supplies rather than something the WS server resolves itself — see\n // createWebsocketsAuthenticator for why (AuthenticationContext is per-request, unreachable\n // from the root container the WS server is attached to).\n const connectionManager = rootContainer.resolve(WebsocketsConnectionManager);\n const websockets = attachWebsocketsServer({\n server,\n connectionManager,\n authenticate: createWebsocketsAuthenticator(rootContainer, config.extensions)\n });\n await websockets.start();\n stopWebsockets = () => websockets.stop();\n\n // Receive sends and disconnects other API processes publish for this one's sockets.\n const messageBus = rootContainer.resolve(WebsocketsMessageBus);\n const processOwner = rootContainer.resolve(ProcessOwner);\n const adapter = rootContainer.resolve(WebsocketsServerAdapter);\n const handleBusMessage = createBusMessageHandler({\n connectionManager,\n adapter,\n logger: consoleLogger\n });\n await messageBus.subscribe(processOwner.id, handleBusMessage);\n closeMessageBus = () => messageBus.close();\n consoleLogger.info(\n { messageBus: standaloneConfig.redisUrl ? \"redis\" : \"in-process\" },\n \"[websockets] using message bus\"\n );\n\n /*\n * Start the in-process scheduler timers, then re-arm persisted schedules in the\n * background, at boot and then every minute (see startSchedulerServer).\n */\n stopScheduler = await startSchedulerServer(rootContainer);\n\n // Start the periodic empty-trash-bin trigger.\n startBulkActionsServer(rootContainer);\n\n /*\n * Take over tasks a restart or another process left behind, at boot and then every\n * minute (see startTaskRecovery).\n */\n stopTaskRecovery = startTaskRecovery(rootContainer);\n },\n\n onShutdown: async rootContainer => {\n /*\n * Stop starting new work first: task recovery, and the scheduler's timers and recovery.\n * Then wait for what is running: tasks hand themselves back, so another process picks\n * them up at its next sweep, and scheduled actions already running get to finish.\n * Websockets close at the same time, with 1012 so clients reconnect to a process that\n * is up, and their connections leave the registry; that takes about a second. The\n * message bus closes last, once tasks and scheduled actions are done: what they still\n * send to browsers on other processes goes through it.\n */\n stopTaskRecovery?.();\n const tasks = stopBackgroundTasks(rootContainer, SHUTDOWN_WAIT_MS);\n const scheduledActions = stopScheduler?.(SHUTDOWN_WAIT_MS);\n const websockets = stopWebsockets?.();\n const workDone = Promise.allSettled([tasks, scheduledActions]);\n const messageBus = workDone.then(() => closeMessageBus?.());\n\n // One failing part must not cut the others short; the first failure is reported after.\n const results = await Promise.allSettled([\n tasks,\n scheduledActions,\n websockets,\n messageBus\n ]);\n const failed = results.find((result): result is PromiseRejectedResult => {\n return result.status === \"rejected\";\n });\n if (failed) {\n throw failed.reason;\n }\n }\n });\n}\n"],"names":["SHUTDOWN_WAIT_MS","createWebinyApiHandler","config","stopTaskRecovery","stopScheduler","stopWebsockets","closeMessageBus","standaloneConfig","readStandaloneConfig","process","createServerHandler","rootContainer","StandaloneConfig","RandomProcessOwner","mkdirSync","NodeHttpFeature","NodeHttpIdentityLoaderDecorator","NodeHttpTenantLoaderDecorator","NodeHttpAssumePermissionsDecorator","ServerConnectionManager","NodeWsAdapter","WebsocketsRedisFeature","consoleLogger","InProcessMessageBus","GraphQLSchemaCacheFeature","EncryptionKeyCacheFeature","JwksStoreFeature","BackgroundTasksStandaloneFeature","registerSchedulerServer","EmptyTrashBinsFeature","container","registerApiRequestStack","requestContainer","ConfigPublicUrlsFeature","WebsocketsStandaloneFeature","FileManagerStandaloneFeature","server","connectionManager","WebsocketsConnectionManager","websockets","attachWebsocketsServer","createWebsocketsAuthenticator","messageBus","WebsocketsMessageBus","processOwner","ProcessOwner","adapter","WebsocketsServerAdapter","handleBusMessage","createBusMessageHandler","startSchedulerServer","startBulkActionsServer","startTaskRecovery","tasks","stopBackgroundTasks","scheduledActions","workDone","Promise","results","failed","result"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyEA,MAAMA,mBAAmB;AAElB,SAASC,uBAAuBC,MAAoC;IACvE,IAAIC;IACJ,IAAIC;IACJ,IAAIC;IACJ,IAAIC;IAIJ,MAAMC,mBAAmBC,qBAAqBC,QAAQ,GAAG;IAEzD,OAAOC,oBAAoB;QACvB,MAAM,OAAMC;YACRA,cAAc,gBAAgB,CAACC,kBAAkBL;YAGjDI,cAAc,QAAQ,CAACE,oBAAoB,gBAAgB;YAC3DC,UAAUP,iBAAiB,WAAW,CAAC,WAAW,EAAE;gBAAE,WAAW;YAAK;YAItEQ,gBAAgB,QAAQ,CAACJ;YASzBA,cAAc,iBAAiB,CAACK;YAChCL,cAAc,iBAAiB,CAACM;YAGhCN,cAAc,iBAAiB,CAACO;YAGhC,MAAMhB,OAAO,mBAAmB,CAACS;YAOjCA,cAAc,QAAQ,CAACQ,yBAAyB,gBAAgB;YAChER,cAAc,QAAQ,CAACS,eAAe,gBAAgB;YAMtD,IAAIb,iBAAiB,QAAQ,EACzBc,uBAAuB,QAAQ,CAACV,eAAe;gBAC3C,KAAKJ,iBAAiB,QAAQ;gBAC9B,QAAQe;YACZ;iBAEAX,cAAc,QAAQ,CAACY,qBAAqB,gBAAgB;YAKhEC,0BAA0B,QAAQ,CAACb;YAInCc,0BAA0B,QAAQ,CAACd;YAEnCe,iBAAiB,QAAQ,CAACf;YAO1BgB,iCAAiC,QAAQ,CAAChB;YAS1CiB,wBAAwBjB;YAKxBkB,sBAAsB,QAAQ,CAAClB;QACnC;QAEA,OAAO,OAAMmB;YAKT,MAAMC,wBAAwBD,WAAW;gBACrC,YAAY5B,OAAO,UAAU;gBAU7B,YAAY;oBAGR,YAAY8B,CAAAA;wBACRC,wBAAwB,QAAQ,CAACD;oBACrC;oBAGA,UAAUA,CAAAA;wBACNE,4BAA4B,QAAQ,CAACF;oBACzC;oBAKA,aAAaA,CAAAA;wBACTG,6BAA6B,QAAQ,CACjCH,kBACAzB,iBAAiB,WAAW;oBAEpC;gBACJ;YACJ;QACJ;QAEA,UAAU,OAAO6B,QAAQzB;YASrB,MAAM0B,oBAAoB1B,cAAc,OAAO,CAAC2B;YAChD,MAAMC,aAAaC,uBAAuB;gBACtCJ;gBACAC;gBACA,cAAcI,8BAA8B9B,eAAeT,OAAO,UAAU;YAChF;YACA,MAAMqC,WAAW,KAAK;YACtBlC,iBAAiB,IAAMkC,WAAW,IAAI;YAGtC,MAAMG,aAAa/B,cAAc,OAAO,CAACgC;YACzC,MAAMC,eAAejC,cAAc,OAAO,CAACkC;YAC3C,MAAMC,UAAUnC,cAAc,OAAO,CAACoC;YACtC,MAAMC,mBAAmBC,wBAAwB;gBAC7CZ;gBACAS;gBACA,QAAQxB;YACZ;YACA,MAAMoB,WAAW,SAAS,CAACE,aAAa,EAAE,EAAEI;YAC5C1C,kBAAkB,IAAMoC,WAAW,KAAK;YACxCpB,cAAc,IAAI,CACd;gBAAE,YAAYf,iBAAiB,QAAQ,GAAG,UAAU;YAAa,GACjE;YAOJH,gBAAgB,MAAM8C,qBAAqBvC;YAG3CwC,uBAAuBxC;YAMvBR,mBAAmBiD,kBAAkBzC;QACzC;QAEA,YAAY,OAAMA;YAUdR;YACA,MAAMkD,QAAQC,oBAAoB3C,eAAeX;YACjD,MAAMuD,mBAAmBnD,gBAAgBJ;YACzC,MAAMuC,aAAalC;YACnB,MAAMmD,WAAWC,QAAQ,UAAU,CAAC;gBAACJ;gBAAOE;aAAiB;YAC7D,MAAMb,aAAac,SAAS,IAAI,CAAC,IAAMlD;YAGvC,MAAMoD,UAAU,MAAMD,QAAQ,UAAU,CAAC;gBACrCJ;gBACAE;gBACAhB;gBACAG;aACH;YACD,MAAMiB,SAASD,QAAQ,IAAI,CAAC,CAACE,SAClBA,AAAkB,eAAlBA,OAAO,MAAM;YAExB,IAAID,QACA,MAAMA,OAAO,MAAM;QAE3B;IACJ;AACJ"}
@@ -0,0 +1,2 @@
1
+ import type { Logger } from "@webiny/api-core/features/logger/abstractions.js";
2
+ export declare const consoleLogger: Logger.Interface;
@@ -0,0 +1,12 @@
1
+ const consoleLogger = {
2
+ trace: (...args)=>console.trace(...args),
3
+ debug: (...args)=>console.debug(...args),
4
+ info: (...args)=>console.info(...args),
5
+ warn: (...args)=>console.warn(...args),
6
+ error: (...args)=>console.error(...args),
7
+ fatal: (...args)=>console.error(...args),
8
+ log: (...args)=>console.log(...args)
9
+ };
10
+ export { consoleLogger };
11
+
12
+ //# sourceMappingURL=consoleLogger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger/consoleLogger.js","sources":["../../src/logger/consoleLogger.ts"],"sourcesContent":["import type { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\n\n/*\n * Minimal console-backed logger for root singletons: the scheduler and the websocket message bus.\n * The real DI Logger is registered per-request (via ApiCoreFeature/LoggerFeature), so it isn't\n * resolvable at the root where these singletons are built. And even if we bridged to it, they run\n * outside any request, so there's no request context for a context-aware logger to enrich, and\n * console is functionally equivalent, not a downgrade. Revisit (a shared boot-time logger for all\n * root singletons) tracked in https://github.com/webiny/webiny-js/issues/5446.\n */\nexport const consoleLogger: Logger.Interface = {\n trace: (...args: any[]) => console.trace(...args),\n debug: (...args: any[]) => console.debug(...args),\n info: (...args: any[]) => console.info(...args),\n warn: (...args: any[]) => console.warn(...args),\n error: (...args: any[]) => console.error(...args),\n fatal: (...args: any[]) => console.error(...args),\n log: (...args: any[]) => console.log(...args)\n};\n"],"names":["consoleLogger","args","console"],"mappings":"AAUO,MAAMA,gBAAkC;IAC3C,OAAO,CAAC,GAAGC,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,KAAK,CAAC,GAAGA,OAAgBC,QAAQ,GAAG,IAAID;AAC5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/api-event-handler-standalone",
3
- "version": "0.0.0-unstable.5387e5f7c8",
3
+ "version": "0.0.0-unstable.d0f34a8194",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -18,25 +18,26 @@
18
18
  ],
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "@webiny/api-core": "0.0.0-unstable.5387e5f7c8",
22
- "@webiny/api-event-handler-core": "0.0.0-unstable.5387e5f7c8",
23
- "@webiny/api-file-manager-standalone": "0.0.0-unstable.5387e5f7c8",
24
- "@webiny/api-graphql": "0.0.0-unstable.5387e5f7c8",
25
- "@webiny/api-headless-cms-bulk-actions-standalone": "0.0.0-unstable.5387e5f7c8",
26
- "@webiny/api-scheduler": "0.0.0-unstable.5387e5f7c8",
27
- "@webiny/api-scheduler-standalone": "0.0.0-unstable.5387e5f7c8",
28
- "@webiny/api-websockets-standalone": "0.0.0-unstable.5387e5f7c8",
29
- "@webiny/background-tasks-standalone": "0.0.0-unstable.5387e5f7c8",
30
- "@webiny/event-handler-core": "0.0.0-unstable.5387e5f7c8",
31
- "@webiny/event-handler-standalone": "0.0.0-unstable.5387e5f7c8",
32
- "@webiny/feature": "0.0.0-unstable.5387e5f7c8",
33
- "@webiny/handler": "0.0.0-unstable.5387e5f7c8"
21
+ "@webiny/api-core": "0.0.0-unstable.d0f34a8194",
22
+ "@webiny/api-event-handler-core": "0.0.0-unstable.d0f34a8194",
23
+ "@webiny/api-file-manager-standalone": "0.0.0-unstable.d0f34a8194",
24
+ "@webiny/api-graphql": "0.0.0-unstable.d0f34a8194",
25
+ "@webiny/api-headless-cms-bulk-actions-standalone": "0.0.0-unstable.d0f34a8194",
26
+ "@webiny/api-scheduler": "0.0.0-unstable.d0f34a8194",
27
+ "@webiny/api-scheduler-standalone": "0.0.0-unstable.d0f34a8194",
28
+ "@webiny/api-websockets-standalone": "0.0.0-unstable.d0f34a8194",
29
+ "@webiny/api-websockets-standalone-redis": "0.0.0-unstable.d0f34a8194",
30
+ "@webiny/background-tasks-standalone": "0.0.0-unstable.d0f34a8194",
31
+ "@webiny/event-handler-core": "0.0.0-unstable.d0f34a8194",
32
+ "@webiny/event-handler-standalone": "0.0.0-unstable.d0f34a8194",
33
+ "@webiny/feature": "0.0.0-unstable.d0f34a8194",
34
+ "@webiny/handler": "0.0.0-unstable.d0f34a8194"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@webiny/di": "^2.0.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@webiny/build-tools": "0.0.0-unstable.5387e5f7c8",
40
+ "@webiny/build-tools": "0.0.0-unstable.d0f34a8194",
40
41
  "@webiny/di": "2.0.0",
41
42
  "rimraf": "6.1.3",
42
43
  "typescript": "7.0.2",
@@ -0,0 +1,13 @@
1
+ import { PublicUrls } from "@webiny/api-core/features/publicUrls/index.js";
2
+ import { StandaloneConfig } from "../config/abstractions.js";
3
+ /** Standalone: the URLs come from `WEBINY_API_URL` / `WEBINY_ADMIN_URL`, read at startup. */
4
+ declare class ConfigPublicUrlsImpl implements PublicUrls.Interface {
5
+ private config;
6
+ constructor(config: StandaloneConfig.Interface);
7
+ getApiUrl(): Promise<import("@webiny/api-core/features/publicUrls/PublicUrl").PublicUrl>;
8
+ getAdminUrl(): Promise<import("@webiny/api-core/features/publicUrls/PublicUrl").PublicUrl>;
9
+ }
10
+ export declare const ConfigPublicUrls: typeof ConfigPublicUrlsImpl & {
11
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/api-core/features/publicUrls/abstractions").IPublicUrls>;
12
+ };
13
+ export {};
@@ -0,0 +1,22 @@
1
+ import { PublicUrls } from "@webiny/api-core/features/publicUrls/index.js";
2
+ import { StandaloneConfig } from "../config/abstractions.js";
3
+ class ConfigPublicUrlsImpl {
4
+ constructor(config){
5
+ this.config = config;
6
+ }
7
+ async getApiUrl() {
8
+ return this.config.apiUrl;
9
+ }
10
+ async getAdminUrl() {
11
+ return this.config.adminUrl;
12
+ }
13
+ }
14
+ const ConfigPublicUrls = PublicUrls.createImplementation({
15
+ implementation: ConfigPublicUrlsImpl,
16
+ dependencies: [
17
+ StandaloneConfig
18
+ ]
19
+ });
20
+ export { ConfigPublicUrls };
21
+
22
+ //# sourceMappingURL=ConfigPublicUrls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicUrls/ConfigPublicUrls.js","sources":["../../src/publicUrls/ConfigPublicUrls.ts"],"sourcesContent":["import { PublicUrls } from \"@webiny/api-core/features/publicUrls/index.js\";\nimport { StandaloneConfig } from \"~/config/abstractions.js\";\n\n/** Standalone: the URLs come from `WEBINY_API_URL` / `WEBINY_ADMIN_URL`, read at startup. */\nclass ConfigPublicUrlsImpl implements PublicUrls.Interface {\n constructor(private config: StandaloneConfig.Interface) {}\n\n async getApiUrl() {\n return this.config.apiUrl;\n }\n\n async getAdminUrl() {\n return this.config.adminUrl;\n }\n}\n\nexport const ConfigPublicUrls = PublicUrls.createImplementation({\n implementation: ConfigPublicUrlsImpl,\n dependencies: [StandaloneConfig]\n});\n"],"names":["ConfigPublicUrlsImpl","config","ConfigPublicUrls","PublicUrls","StandaloneConfig"],"mappings":";;AAIA,MAAMA;IACF,YAAoBC,MAAkC,CAAE;aAApCA,MAAM,GAANA;IAAqC;IAEzD,MAAM,YAAY;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM;IAC7B;IAEA,MAAM,cAAc;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ;IAC/B;AACJ;AAEO,MAAMC,mBAAmBC,WAAW,oBAAoB,CAAC;IAC5D,gBAAgBH;IAChB,cAAc;QAACI;KAAiB;AACpC"}
@@ -0,0 +1,4 @@
1
+ export declare const ConfigPublicUrlsFeature: {
2
+ name: string;
3
+ register(container: import("@webiny/di").Container): void;
4
+ };
@@ -0,0 +1,11 @@
1
+ import { createFeature } from "@webiny/feature/api";
2
+ import { ConfigPublicUrls } from "./ConfigPublicUrls.js";
3
+ const ConfigPublicUrlsFeature = createFeature({
4
+ name: "ConfigPublicUrlsFeature",
5
+ register (container) {
6
+ container.register(ConfigPublicUrls);
7
+ }
8
+ });
9
+ export { ConfigPublicUrlsFeature };
10
+
11
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicUrls/feature.js","sources":["../../src/publicUrls/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { ConfigPublicUrls } from \"./ConfigPublicUrls.js\";\n\nexport const ConfigPublicUrlsFeature = createFeature({\n name: \"ConfigPublicUrlsFeature\",\n register(container) {\n container.register(ConfigPublicUrls);\n }\n});\n"],"names":["ConfigPublicUrlsFeature","createFeature","container","ConfigPublicUrls"],"mappings":";;AAGO,MAAMA,0BAA0BC,cAAc;IACjD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
@@ -1,26 +1,28 @@
1
- import { RawTenantId } from "@webiny/api-core/features/requestContext/index.js";
2
- import { RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
3
- import { GetRootTenantUseCase } from "@webiny/api-core/features/tenancy/GetRootTenant/index.js";
1
+ import { ListTenantsUseCase } from "@webiny/api-core/features/tenancy/ListTenants/index.js";
2
+ import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
4
3
  import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
4
+ import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
5
5
  import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions/index.js";
6
6
  import { ScheduledActionRecoverEventHandler } from "./ScheduledActionRecoverEventHandler.js";
7
7
  import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
8
8
  /**
9
- * Re-arms the root tenant's pending scheduled actions in the Bree singleton. Overdue ones run at
10
- * once (see `BreeSchedulerService.recover`).
9
+ * Re-arms every tenant's pending scheduled actions in the Bree singleton. Bree keeps its timers in
10
+ * memory, so a restart loses them all. Overdue actions run at once (see
11
+ * `BreeSchedulerService.recover`).
11
12
  *
12
- * Root tenant only. Recovering other tenants means enumerating them and recovering each; until
13
- * then a restart drops their pending schedules.
13
+ * Each tenant is recovered on its own: one that fails is logged and reported in the result, and the
14
+ * rest are still recovered. A fresh install has no tenants yet and recovers nothing.
14
15
  */
15
16
  declare class RecoverScheduledActionsHandlerImpl implements ScheduledActionRecoverEventHandler.Interface {
16
- private readonly getRootTenant;
17
- private readonly rawTenantId;
18
- private readonly tenantLoader;
17
+ private readonly listTenants;
18
+ private readonly tenantContext;
19
19
  private readonly identityContext;
20
20
  private readonly listScheduledActions;
21
21
  private readonly scheduler;
22
- constructor(getRootTenant: GetRootTenantUseCase.Interface, rawTenantId: RawTenantId.Interface, tenantLoader: RequestTenantLoader.Interface, identityContext: IdentityContext.Interface, listScheduledActions: ListScheduledActionsUseCase.Interface, scheduler: SchedulerSingleton.Interface);
22
+ private readonly logger;
23
+ constructor(listTenants: ListTenantsUseCase.Interface, tenantContext: TenantContext.Interface, identityContext: IdentityContext.Interface, listScheduledActions: ListScheduledActionsUseCase.Interface, scheduler: SchedulerSingleton.Interface, logger: Logger.Interface);
23
24
  execute(): Promise<ScheduledActionRecoverEventHandler.Result>;
25
+ private recoverTenant;
24
26
  private recoverAllPending;
25
27
  }
26
28
  export declare const RecoverScheduledActionsHandler: typeof RecoverScheduledActionsHandlerImpl & {
@@ -1,32 +1,45 @@
1
- import { RawTenantId, RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
2
- import { GetRootTenantUseCase } from "@webiny/api-core/features/tenancy/GetRootTenant/index.js";
1
+ import { ListTenantsUseCase } from "@webiny/api-core/features/tenancy/ListTenants/index.js";
2
+ import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
3
3
  import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
4
+ import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
4
5
  import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions/index.js";
5
6
  import { ScheduledActionRecoverEventHandler } from "./ScheduledActionRecoverEventHandler.js";
6
7
  import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
7
8
  const PAGE_SIZE = 1000;
9
+ function canRecover(tenant) {
10
+ return tenant.isInstalled && "disabled" !== tenant.status;
11
+ }
8
12
  class RecoverScheduledActionsHandlerImpl {
9
- constructor(getRootTenant, rawTenantId, tenantLoader, identityContext, listScheduledActions, scheduler){
10
- this.getRootTenant = getRootTenant;
11
- this.rawTenantId = rawTenantId;
12
- this.tenantLoader = tenantLoader;
13
+ constructor(listTenants, tenantContext, identityContext, listScheduledActions, scheduler, logger){
14
+ this.listTenants = listTenants;
15
+ this.tenantContext = tenantContext;
13
16
  this.identityContext = identityContext;
14
17
  this.listScheduledActions = listScheduledActions;
15
18
  this.scheduler = scheduler;
19
+ this.logger = logger;
16
20
  }
17
21
  async execute() {
18
- const rootResult = await this.getRootTenant.execute();
19
- if (rootResult.isFail()) return {
20
- recovered: 0
21
- };
22
- const tenant = rootResult.value.id;
23
- this.rawTenantId.set(tenant);
24
- await this.tenantLoader.establish();
25
- const recovered = await this.identityContext.withoutAuthorization(()=>this.recoverAllPending(tenant));
22
+ const tenantsResult = await this.listTenants.execute();
23
+ if (tenantsResult.isFail()) throw tenantsResult.error;
24
+ let recovered = 0;
25
+ const failedTenants = [];
26
+ for (const tenant of tenantsResult.value)if (canRecover(tenant)) try {
27
+ recovered += await this.recoverTenant(tenant);
28
+ } catch (error) {
29
+ failedTenants.push(tenant.id);
30
+ this.logger.error({
31
+ error,
32
+ tenant: tenant.id
33
+ }, "Could not recover the tenant's scheduled actions.");
34
+ }
26
35
  return {
27
- recovered
36
+ recovered,
37
+ failedTenants
28
38
  };
29
39
  }
40
+ recoverTenant(tenant) {
41
+ return this.tenantContext.withTenant(tenant, ()=>this.identityContext.withoutAuthorization(()=>this.recoverAllPending(tenant.id)));
42
+ }
30
43
  async recoverAllPending(tenant) {
31
44
  let recovered = 0;
32
45
  let after;
@@ -55,12 +68,12 @@ class RecoverScheduledActionsHandlerImpl {
55
68
  const RecoverScheduledActionsHandler = ScheduledActionRecoverEventHandler.createImplementation({
56
69
  implementation: RecoverScheduledActionsHandlerImpl,
57
70
  dependencies: [
58
- GetRootTenantUseCase,
59
- RawTenantId,
60
- RequestTenantLoader,
71
+ ListTenantsUseCase,
72
+ TenantContext,
61
73
  IdentityContext,
62
74
  ListScheduledActionsUseCase,
63
- SchedulerSingleton
75
+ SchedulerSingleton,
76
+ Logger
64
77
  ]
65
78
  });
66
79
  export { RecoverScheduledActionsHandler };
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler/RecoverScheduledActionsHandler.js","sources":["../../src/scheduler/RecoverScheduledActionsHandler.ts"],"sourcesContent":["import { RawTenantId } from \"@webiny/api-core/features/requestContext/index.js\";\nimport { RequestTenantLoader } from \"@webiny/api-core/features/requestContext/index.js\";\nimport { GetRootTenantUseCase } from \"@webiny/api-core/features/tenancy/GetRootTenant/index.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { ListScheduledActionsUseCase } from \"@webiny/api-scheduler/features/ListScheduledActions/index.js\";\nimport { ScheduledActionRecoverEventHandler } from \"./ScheduledActionRecoverEventHandler.js\";\nimport type { IPendingAction } from \"@webiny/api-scheduler-standalone\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\n\n// Pending actions read per page.\nconst PAGE_SIZE = 1000;\n\n/**\n * Re-arms the root tenant's pending scheduled actions in the Bree singleton. Overdue ones run at\n * once (see `BreeSchedulerService.recover`).\n *\n * Root tenant only. Recovering other tenants means enumerating them and recovering each; until\n * then a restart drops their pending schedules.\n */\nclass RecoverScheduledActionsHandlerImpl implements ScheduledActionRecoverEventHandler.Interface {\n public constructor(\n private readonly getRootTenant: GetRootTenantUseCase.Interface,\n private readonly rawTenantId: RawTenantId.Interface,\n private readonly tenantLoader: RequestTenantLoader.Interface,\n private readonly identityContext: IdentityContext.Interface,\n private readonly listScheduledActions: ListScheduledActionsUseCase.Interface,\n private readonly scheduler: SchedulerSingleton.Interface\n ) {}\n\n public async execute(): Promise<ScheduledActionRecoverEventHandler.Result> {\n const rootResult = await this.getRootTenant.execute();\n if (rootResult.isFail()) {\n // No root tenant yet: a fresh install has nothing to recover.\n return { recovered: 0 };\n }\n const tenant = rootResult.value.id;\n\n this.rawTenantId.set(tenant);\n await this.tenantLoader.establish();\n\n /*\n * Nobody is signed in at boot. Listing without authorization is what the run side does too\n * (ExecuteScheduledActionUseCase); otherwise the anonymous identity fails the permission check.\n */\n const recovered = await this.identityContext.withoutAuthorization(() => {\n return this.recoverAllPending(tenant);\n });\n\n return { recovered };\n }\n\n /*\n * Re-arms page by page, so a lookup that fails on a later page still leaves the earlier pages\n * armed. The failure is rethrown and logged by the boot step.\n */\n private async recoverAllPending(tenant: string): Promise<number> {\n let recovered = 0;\n let after: string | undefined;\n\n do {\n const listResult = await this.listScheduledActions.execute({\n where: {},\n limit: PAGE_SIZE,\n after\n });\n if (listResult.isFail()) {\n throw listResult.error;\n }\n\n const page: IPendingAction[] = [];\n for (const action of listResult.value.items) {\n page.push({\n id: action.id,\n namespace: action.namespace,\n tenant,\n scheduledFor: action.scheduledFor\n });\n }\n\n await this.scheduler.recover(page);\n recovered += page.length;\n\n const { meta } = listResult.value;\n after = meta.hasMoreItems && meta.cursor ? meta.cursor : undefined;\n } while (after);\n\n return recovered;\n }\n}\n\nexport const RecoverScheduledActionsHandler =\n ScheduledActionRecoverEventHandler.createImplementation({\n implementation: RecoverScheduledActionsHandlerImpl,\n dependencies: [\n GetRootTenantUseCase,\n RawTenantId,\n RequestTenantLoader,\n IdentityContext,\n ListScheduledActionsUseCase,\n SchedulerSingleton\n ]\n });\n"],"names":["PAGE_SIZE","RecoverScheduledActionsHandlerImpl","getRootTenant","rawTenantId","tenantLoader","identityContext","listScheduledActions","scheduler","rootResult","tenant","recovered","after","listResult","page","action","meta","undefined","RecoverScheduledActionsHandler","ScheduledActionRecoverEventHandler","GetRootTenantUseCase","RawTenantId","RequestTenantLoader","IdentityContext","ListScheduledActionsUseCase","SchedulerSingleton"],"mappings":";;;;;;AAUA,MAAMA,YAAY;AASlB,MAAMC;IACF,YACqBC,aAA6C,EAC7CC,WAAkC,EAClCC,YAA2C,EAC3CC,eAA0C,EAC1CC,oBAA2D,EAC3DC,SAAuC,CAC1D;aANmBL,aAAa,GAAbA;aACAC,WAAW,GAAXA;aACAC,YAAY,GAAZA;aACAC,eAAe,GAAfA;aACAC,oBAAoB,GAApBA;aACAC,SAAS,GAATA;IAClB;IAEH,MAAa,UAA8D;QACvE,MAAMC,aAAa,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO;QACnD,IAAIA,WAAW,MAAM,IAEjB,OAAO;YAAE,WAAW;QAAE;QAE1B,MAAMC,SAASD,WAAW,KAAK,CAAC,EAAE;QAElC,IAAI,CAAC,WAAW,CAAC,GAAG,CAACC;QACrB,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS;QAMjC,MAAMC,YAAY,MAAM,IAAI,CAAC,eAAe,CAAC,oBAAoB,CAAC,IACvD,IAAI,CAAC,iBAAiB,CAACD;QAGlC,OAAO;YAAEC;QAAU;IACvB;IAMA,MAAc,kBAAkBD,MAAc,EAAmB;QAC7D,IAAIC,YAAY;QAChB,IAAIC;QAEJ,GAAG;YACC,MAAMC,aAAa,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;gBACvD,OAAO,CAAC;gBACR,OAAOZ;gBACPW;YACJ;YACA,IAAIC,WAAW,MAAM,IACjB,MAAMA,WAAW,KAAK;YAG1B,MAAMC,OAAyB,EAAE;YACjC,KAAK,MAAMC,UAAUF,WAAW,KAAK,CAAC,KAAK,CACvCC,KAAK,IAAI,CAAC;gBACN,IAAIC,OAAO,EAAE;gBACb,WAAWA,OAAO,SAAS;gBAC3BL;gBACA,cAAcK,OAAO,YAAY;YACrC;YAGJ,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACD;YAC7BH,aAAaG,KAAK,MAAM;YAExB,MAAM,EAAEE,IAAI,EAAE,GAAGH,WAAW,KAAK;YACjCD,QAAQI,KAAK,YAAY,IAAIA,KAAK,MAAM,GAAGA,KAAK,MAAM,GAAGC;QAC7D,QAASL,MAAO;QAEhB,OAAOD;IACX;AACJ;AAEO,MAAMO,iCACTC,mCAAmC,oBAAoB,CAAC;IACpD,gBAAgBjB;IAChB,cAAc;QACVkB;QACAC;QACAC;QACAC;QACAC;QACAC;KACH;AACL"}
1
+ {"version":3,"file":"scheduler/RecoverScheduledActionsHandler.js","sources":["../../src/scheduler/RecoverScheduledActionsHandler.ts"],"sourcesContent":["import { ListTenantsUseCase } from \"@webiny/api-core/features/tenancy/ListTenants/index.js\";\nimport { TenantContext } from \"@webiny/api-core/features/tenancy/TenantContext/index.js\";\nimport type { Tenant } from \"@webiny/api-core/types/tenancy.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport { ListScheduledActionsUseCase } from \"@webiny/api-scheduler/features/ListScheduledActions/index.js\";\nimport { ScheduledActionRecoverEventHandler } from \"./ScheduledActionRecoverEventHandler.js\";\nimport type { IPendingAction } from \"@webiny/api-scheduler-standalone\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\n\n// Pending actions read per page.\nconst PAGE_SIZE = 1000;\n\n/*\n * A tenant that isn't installed has no scheduler model to read, and a disabled one must not run its\n * schedules. Both are left alone; a disabled tenant's schedules come back on the next restart after\n * it is re-enabled.\n */\nfunction canRecover(tenant: Tenant): boolean {\n return tenant.isInstalled && tenant.status !== \"disabled\";\n}\n\n/**\n * Re-arms every tenant's pending scheduled actions in the Bree singleton. Bree keeps its timers in\n * memory, so a restart loses them all. Overdue actions run at once (see\n * `BreeSchedulerService.recover`).\n *\n * Each tenant is recovered on its own: one that fails is logged and reported in the result, and the\n * rest are still recovered. A fresh install has no tenants yet and recovers nothing.\n */\nclass RecoverScheduledActionsHandlerImpl implements ScheduledActionRecoverEventHandler.Interface {\n public constructor(\n private readonly listTenants: ListTenantsUseCase.Interface,\n private readonly tenantContext: TenantContext.Interface,\n private readonly identityContext: IdentityContext.Interface,\n private readonly listScheduledActions: ListScheduledActionsUseCase.Interface,\n private readonly scheduler: SchedulerSingleton.Interface,\n private readonly logger: Logger.Interface\n ) {}\n\n public async execute(): Promise<ScheduledActionRecoverEventHandler.Result> {\n // A failed lookup is not a fresh install: it throws, so the boot step reports it.\n const tenantsResult = await this.listTenants.execute();\n if (tenantsResult.isFail()) {\n throw tenantsResult.error;\n }\n\n let recovered = 0;\n const failedTenants: string[] = [];\n\n for (const tenant of tenantsResult.value) {\n if (!canRecover(tenant)) {\n continue;\n }\n try {\n recovered += await this.recoverTenant(tenant);\n } catch (error) {\n failedTenants.push(tenant.id);\n this.logger.error(\n { error, tenant: tenant.id },\n \"Could not recover the tenant's scheduled actions.\"\n );\n }\n }\n\n return { recovered, failedTenants };\n }\n\n private recoverTenant(tenant: Tenant): Promise<number> {\n return this.tenantContext.withTenant(tenant, () => {\n /*\n * Nobody is signed in at boot. Listing without authorization is what the run side does\n * too (ExecuteScheduledActionUseCase); otherwise the anonymous identity fails the\n * permission check.\n */\n return this.identityContext.withoutAuthorization(() => {\n return this.recoverAllPending(tenant.id);\n });\n });\n }\n\n /*\n * Re-arms page by page, so a lookup that fails on a later page still leaves the earlier pages\n * armed. The failure is rethrown and logged by the boot step.\n */\n private async recoverAllPending(tenant: string): Promise<number> {\n let recovered = 0;\n let after: string | undefined;\n\n do {\n const listResult = await this.listScheduledActions.execute({\n where: {},\n limit: PAGE_SIZE,\n after\n });\n if (listResult.isFail()) {\n throw listResult.error;\n }\n\n const page: IPendingAction[] = [];\n for (const action of listResult.value.items) {\n page.push({\n id: action.id,\n namespace: action.namespace,\n tenant,\n scheduledFor: action.scheduledFor\n });\n }\n\n await this.scheduler.recover(page);\n recovered += page.length;\n\n const { meta } = listResult.value;\n after = meta.hasMoreItems && meta.cursor ? meta.cursor : undefined;\n } while (after);\n\n return recovered;\n }\n}\n\nexport const RecoverScheduledActionsHandler =\n ScheduledActionRecoverEventHandler.createImplementation({\n implementation: RecoverScheduledActionsHandlerImpl,\n dependencies: [\n ListTenantsUseCase,\n TenantContext,\n IdentityContext,\n ListScheduledActionsUseCase,\n SchedulerSingleton,\n Logger\n ]\n });\n"],"names":["PAGE_SIZE","canRecover","tenant","RecoverScheduledActionsHandlerImpl","listTenants","tenantContext","identityContext","listScheduledActions","scheduler","logger","tenantsResult","recovered","failedTenants","error","after","listResult","page","action","meta","undefined","RecoverScheduledActionsHandler","ScheduledActionRecoverEventHandler","ListTenantsUseCase","TenantContext","IdentityContext","ListScheduledActionsUseCase","SchedulerSingleton","Logger"],"mappings":";;;;;;;AAWA,MAAMA,YAAY;AAOlB,SAASC,WAAWC,MAAc;IAC9B,OAAOA,OAAO,WAAW,IAAIA,AAAkB,eAAlBA,OAAO,MAAM;AAC9C;AAUA,MAAMC;IACF,YACqBC,WAAyC,EACzCC,aAAsC,EACtCC,eAA0C,EAC1CC,oBAA2D,EAC3DC,SAAuC,EACvCC,MAAwB,CAC3C;aANmBL,WAAW,GAAXA;aACAC,aAAa,GAAbA;aACAC,eAAe,GAAfA;aACAC,oBAAoB,GAApBA;aACAC,SAAS,GAATA;aACAC,MAAM,GAANA;IAClB;IAEH,MAAa,UAA8D;QAEvE,MAAMC,gBAAgB,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO;QACpD,IAAIA,cAAc,MAAM,IACpB,MAAMA,cAAc,KAAK;QAG7B,IAAIC,YAAY;QAChB,MAAMC,gBAA0B,EAAE;QAElC,KAAK,MAAMV,UAAUQ,cAAc,KAAK,CACpC,IAAKT,WAAWC,SAGhB,IAAI;YACAS,aAAa,MAAM,IAAI,CAAC,aAAa,CAACT;QAC1C,EAAE,OAAOW,OAAO;YACZD,cAAc,IAAI,CAACV,OAAO,EAAE;YAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CACb;gBAAEW;gBAAO,QAAQX,OAAO,EAAE;YAAC,GAC3B;QAER;QAGJ,OAAO;YAAES;YAAWC;QAAc;IACtC;IAEQ,cAAcV,MAAc,EAAmB;QACnD,OAAO,IAAI,CAAC,aAAa,CAAC,UAAU,CAACA,QAAQ,IAMlC,IAAI,CAAC,eAAe,CAAC,oBAAoB,CAAC,IACtC,IAAI,CAAC,iBAAiB,CAACA,OAAO,EAAE;IAGnD;IAMA,MAAc,kBAAkBA,MAAc,EAAmB;QAC7D,IAAIS,YAAY;QAChB,IAAIG;QAEJ,GAAG;YACC,MAAMC,aAAa,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;gBACvD,OAAO,CAAC;gBACR,OAAOf;gBACPc;YACJ;YACA,IAAIC,WAAW,MAAM,IACjB,MAAMA,WAAW,KAAK;YAG1B,MAAMC,OAAyB,EAAE;YACjC,KAAK,MAAMC,UAAUF,WAAW,KAAK,CAAC,KAAK,CACvCC,KAAK,IAAI,CAAC;gBACN,IAAIC,OAAO,EAAE;gBACb,WAAWA,OAAO,SAAS;gBAC3Bf;gBACA,cAAce,OAAO,YAAY;YACrC;YAGJ,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACD;YAC7BL,aAAaK,KAAK,MAAM;YAExB,MAAM,EAAEE,IAAI,EAAE,GAAGH,WAAW,KAAK;YACjCD,QAAQI,KAAK,YAAY,IAAIA,KAAK,MAAM,GAAGA,KAAK,MAAM,GAAGC;QAC7D,QAASL,MAAO;QAEhB,OAAOH;IACX;AACJ;AAEO,MAAMS,iCACTC,mCAAmC,oBAAoB,CAAC;IACpD,gBAAgBlB;IAChB,cAAc;QACVmB;QACAC;QACAC;QACAC;QACAC;QACAC;KACH;AACL"}
@@ -0,0 +1,37 @@
1
+ import { ScheduledActionEventHandler } from "@webiny/event-handler-core";
2
+ import type { IScheduledActionEvent } from "@webiny/event-handler-core";
3
+ import type { IScheduledActionResult } from "@webiny/event-handler-core";
4
+ import type { EventContext } from "@webiny/event-handler-core";
5
+ import type { NextFunction } from "@webiny/event-handler-core";
6
+ import { GetTenantByIdUseCase } from "@webiny/api-core/exports/api/tenancy.js";
7
+ import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
8
+ import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
9
+ import { GlobalKeyValueStore } from "@webiny/api-core/features/keyValueStore/index.js";
10
+ import { GetScheduledActionUseCase } from "@webiny/api-scheduler/features/GetScheduledAction/index.js";
11
+ /**
12
+ * Runs a fired scheduled action at most once across all processes. Every standalone process holds its
13
+ * own timers for the same actions, so the same action fires in each of them; on AWS, EventBridge
14
+ * fires it once and this decorator is not registered.
15
+ *
16
+ * Before the action runs, it is read again. Gone means it was cancelled or already ran. Due later
17
+ * means another process moved it and this timer is stale. Otherwise the process takes a claim on the
18
+ * action at its due time, and only the process that gets it runs the action. Rescheduling changes
19
+ * the due time, so a moved action gets a fresh claim.
20
+ */
21
+ declare class RunScheduledActionOnceDecoratorImpl implements ScheduledActionEventHandler.Interface {
22
+ private readonly getTenantById;
23
+ private readonly tenantContext;
24
+ private readonly identityContext;
25
+ private readonly getScheduledAction;
26
+ private readonly store;
27
+ private readonly decoratee;
28
+ constructor(getTenantById: GetTenantByIdUseCase.Interface, tenantContext: TenantContext.Interface, identityContext: IdentityContext.Interface, getScheduledAction: GetScheduledActionUseCase.Interface, store: GlobalKeyValueStore.Interface, decoratee: ScheduledActionEventHandler.Interface);
29
+ execute(eventCtx: EventContext<IScheduledActionEvent>, next: NextFunction): Promise<IScheduledActionResult>;
30
+ private findDueTime;
31
+ private takeClaim;
32
+ private holdAfterFailure;
33
+ }
34
+ export declare const RunScheduledActionOnceDecorator: typeof RunScheduledActionOnceDecoratorImpl & {
35
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core/features/scheduledAction/ScheduledActionEventHandler").IScheduledActionEventHandler>;
36
+ };
37
+ export {};
@@ -0,0 +1,91 @@
1
+ import { hostname } from "node:os";
2
+ import { SCHEDULED_ACTION_EVENT_IDENTIFIER, ScheduledActionEventHandler } from "@webiny/event-handler-core";
3
+ import { GetTenantByIdUseCase } from "@webiny/api-core/exports/api/tenancy.js";
4
+ import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
5
+ import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
6
+ import { GlobalKeyValueStore, KeyValueConditionFailedError } from "@webiny/api-core/features/keyValueStore/index.js";
7
+ import { GetScheduledActionUseCase } from "@webiny/api-scheduler/features/GetScheduledAction/index.js";
8
+ const CLAIM_SCOPE = "scheduler/claims";
9
+ const RUN_CLAIM_MS = 600000;
10
+ const RETRY_AFTER_FAILURE_MS = 86400000;
11
+ const EARLY_TOLERANCE_MS = 1000;
12
+ const SUCCESS = {
13
+ success: true
14
+ };
15
+ const PROCESS_NAME = `${hostname()}:${process.pid}`;
16
+ const expiresIn = (ms)=>{
17
+ const expiresAtMs = Date.now() + ms;
18
+ return new Date(expiresAtMs);
19
+ };
20
+ class RunScheduledActionOnceDecoratorImpl {
21
+ constructor(getTenantById, tenantContext, identityContext, getScheduledAction, store, decoratee){
22
+ this.getTenantById = getTenantById;
23
+ this.tenantContext = tenantContext;
24
+ this.identityContext = identityContext;
25
+ this.getScheduledAction = getScheduledAction;
26
+ this.store = store;
27
+ this.decoratee = decoratee;
28
+ }
29
+ async execute(eventCtx, next) {
30
+ const input = eventCtx.event[SCHEDULED_ACTION_EVENT_IDENTIFIER];
31
+ const tenantId = input.tenant || this.tenantContext.getTenant().id;
32
+ const dueAt = await this.findDueTime(tenantId, input);
33
+ if (!dueAt) return SUCCESS;
34
+ if (dueAt.getTime() > Date.now() + EARLY_TOLERANCE_MS) return SUCCESS;
35
+ const key = `${tenantId}/${input.id}/${dueAt.toISOString()}`;
36
+ const claimed = await this.takeClaim(key);
37
+ if (!claimed) return SUCCESS;
38
+ try {
39
+ return await this.decoratee.execute(eventCtx, next);
40
+ } catch (error) {
41
+ await this.holdAfterFailure(key);
42
+ throw error;
43
+ }
44
+ }
45
+ async findDueTime(tenantId, input) {
46
+ const tenantResult = await this.getTenantById.execute(tenantId);
47
+ if (tenantResult.isFail()) throw tenantResult.error;
48
+ return this.tenantContext.withTenant(tenantResult.value, ()=>this.identityContext.withoutAuthorization(async ()=>{
49
+ const result = await this.getScheduledAction.execute({
50
+ id: input.id,
51
+ namespace: input.namespace
52
+ });
53
+ if (result.isOk()) return result.value.scheduledFor;
54
+ if ("Scheduler/ScheduledAction/NotFound" === result.error.code) return null;
55
+ throw result.error;
56
+ }));
57
+ }
58
+ async takeClaim(key) {
59
+ const expiresAt = expiresIn(RUN_CLAIM_MS);
60
+ const result = await this.store.set(key, PROCESS_NAME, {
61
+ scope: CLAIM_SCOPE,
62
+ expiresAt,
63
+ condition: {
64
+ exists: false
65
+ }
66
+ });
67
+ if (result.isOk()) return true;
68
+ if (result.error instanceof KeyValueConditionFailedError) return false;
69
+ throw result.error;
70
+ }
71
+ async holdAfterFailure(key) {
72
+ const expiresAt = expiresIn(RETRY_AFTER_FAILURE_MS);
73
+ await this.store.set(key, PROCESS_NAME, {
74
+ scope: CLAIM_SCOPE,
75
+ expiresAt
76
+ });
77
+ }
78
+ }
79
+ const RunScheduledActionOnceDecorator = ScheduledActionEventHandler.createDecorator({
80
+ decorator: RunScheduledActionOnceDecoratorImpl,
81
+ dependencies: [
82
+ GetTenantByIdUseCase,
83
+ TenantContext,
84
+ IdentityContext,
85
+ GetScheduledActionUseCase,
86
+ GlobalKeyValueStore
87
+ ]
88
+ });
89
+ export { RunScheduledActionOnceDecorator };
90
+
91
+ //# sourceMappingURL=RunScheduledActionOnceDecorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler/RunScheduledActionOnceDecorator.js","sources":["../../src/scheduler/RunScheduledActionOnceDecorator.ts"],"sourcesContent":["import { hostname } from \"node:os\";\nimport { ScheduledActionEventHandler } from \"@webiny/event-handler-core\";\nimport { SCHEDULED_ACTION_EVENT_IDENTIFIER } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEvent } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEventPayload } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionResult } from \"@webiny/event-handler-core\";\nimport type { EventContext } from \"@webiny/event-handler-core\";\nimport type { NextFunction } from \"@webiny/event-handler-core\";\nimport { GetTenantByIdUseCase } from \"@webiny/api-core/exports/api/tenancy.js\";\nimport { TenantContext } from \"@webiny/api-core/features/tenancy/TenantContext/index.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { GlobalKeyValueStore } from \"@webiny/api-core/features/keyValueStore/index.js\";\nimport { KeyValueConditionFailedError } from \"@webiny/api-core/features/keyValueStore/index.js\";\nimport { GetScheduledActionUseCase } from \"@webiny/api-scheduler/features/GetScheduledAction/index.js\";\n\nconst CLAIM_SCOPE = \"scheduler/claims\";\n\n/*\n * How long one run of an action holds its claim. Long enough for any action to finish; a run that\n * succeeds deletes the action, so the claim only matters while it runs.\n */\nconst RUN_CLAIM_MS = 10 * 60_000;\n\n// After a failed run, other processes leave the action alone this long before trying it again.\nconst RETRY_AFTER_FAILURE_MS = 24 * 60 * 60_000;\n\n/*\n * Timers fire slightly early or late; an action due within this much is run. One due later than that\n * belongs to a timer another process moved, and this firing is stale.\n */\nconst EARLY_TOLERANCE_MS = 1000;\n\nconst SUCCESS: IScheduledActionResult = { success: true };\n\n// Names this process in the claims it writes, for whoever reads them in the database.\nconst PROCESS_NAME = `${hostname()}:${process.pid}`;\n\nconst expiresIn = (ms: number): Date => {\n const expiresAtMs = Date.now() + ms;\n return new Date(expiresAtMs);\n};\n\n/**\n * Runs a fired scheduled action at most once across all processes. Every standalone process holds its\n * own timers for the same actions, so the same action fires in each of them; on AWS, EventBridge\n * fires it once and this decorator is not registered.\n *\n * Before the action runs, it is read again. Gone means it was cancelled or already ran. Due later\n * means another process moved it and this timer is stale. Otherwise the process takes a claim on the\n * action at its due time, and only the process that gets it runs the action. Rescheduling changes\n * the due time, so a moved action gets a fresh claim.\n */\nclass RunScheduledActionOnceDecoratorImpl implements ScheduledActionEventHandler.Interface {\n public constructor(\n private readonly getTenantById: GetTenantByIdUseCase.Interface,\n private readonly tenantContext: TenantContext.Interface,\n private readonly identityContext: IdentityContext.Interface,\n private readonly getScheduledAction: GetScheduledActionUseCase.Interface,\n private readonly store: GlobalKeyValueStore.Interface,\n private readonly decoratee: ScheduledActionEventHandler.Interface\n ) {}\n\n public async execute(\n eventCtx: EventContext<IScheduledActionEvent>,\n next: NextFunction\n ): Promise<IScheduledActionResult> {\n const input = eventCtx.event[SCHEDULED_ACTION_EVENT_IDENTIFIER];\n const tenantId = input.tenant || this.tenantContext.getTenant().id;\n\n const dueAt = await this.findDueTime(tenantId, input);\n if (!dueAt) {\n return SUCCESS;\n }\n if (dueAt.getTime() > Date.now() + EARLY_TOLERANCE_MS) {\n return SUCCESS;\n }\n\n const key = `${tenantId}/${input.id}/${dueAt.toISOString()}`;\n const claimed = await this.takeClaim(key);\n if (!claimed) {\n return SUCCESS;\n }\n\n try {\n return await this.decoratee.execute(eventCtx, next);\n } catch (error) {\n await this.holdAfterFailure(key);\n throw error;\n }\n }\n\n // The action's due time, or `null` when it no longer exists.\n private async findDueTime(\n tenantId: string,\n input: IScheduledActionEventPayload\n ): Promise<Date | null> {\n const tenantResult = await this.getTenantById.execute(tenantId);\n if (tenantResult.isFail()) {\n throw tenantResult.error;\n }\n\n return this.tenantContext.withTenant(tenantResult.value, () => {\n // GetScheduledAction checks permissions, and nobody is signed in when a timer fires.\n return this.identityContext.withoutAuthorization(async () => {\n const result = await this.getScheduledAction.execute({\n id: input.id,\n namespace: input.namespace\n });\n if (result.isOk()) {\n return result.value.scheduledFor;\n }\n if (result.error.code === \"Scheduler/ScheduledAction/NotFound\") {\n return null;\n }\n throw result.error;\n });\n });\n }\n\n private async takeClaim(key: string): Promise<boolean> {\n const expiresAt = expiresIn(RUN_CLAIM_MS);\n const result = await this.store.set(key, PROCESS_NAME, {\n scope: CLAIM_SCOPE,\n expiresAt,\n condition: { exists: false }\n });\n if (result.isOk()) {\n return true;\n }\n if (result.error instanceof KeyValueConditionFailedError) {\n return false;\n }\n throw result.error;\n }\n\n /*\n * The action stays stored after a failed run, with the error on it, and every process's\n * recovery fires overdue actions each minute. Holding the claim keeps that to one retry a day.\n */\n private async holdAfterFailure(key: string): Promise<void> {\n const expiresAt = expiresIn(RETRY_AFTER_FAILURE_MS);\n await this.store.set(key, PROCESS_NAME, { scope: CLAIM_SCOPE, expiresAt });\n }\n}\n\nexport const RunScheduledActionOnceDecorator = ScheduledActionEventHandler.createDecorator({\n decorator: RunScheduledActionOnceDecoratorImpl,\n dependencies: [\n GetTenantByIdUseCase,\n TenantContext,\n IdentityContext,\n GetScheduledActionUseCase,\n GlobalKeyValueStore\n ]\n});\n"],"names":["CLAIM_SCOPE","RUN_CLAIM_MS","RETRY_AFTER_FAILURE_MS","EARLY_TOLERANCE_MS","SUCCESS","PROCESS_NAME","hostname","process","expiresIn","ms","expiresAtMs","Date","RunScheduledActionOnceDecoratorImpl","getTenantById","tenantContext","identityContext","getScheduledAction","store","decoratee","eventCtx","next","input","SCHEDULED_ACTION_EVENT_IDENTIFIER","tenantId","dueAt","key","claimed","error","tenantResult","result","expiresAt","KeyValueConditionFailedError","RunScheduledActionOnceDecorator","ScheduledActionEventHandler","GetTenantByIdUseCase","TenantContext","IdentityContext","GetScheduledActionUseCase","GlobalKeyValueStore"],"mappings":";;;;;;;AAeA,MAAMA,cAAc;AAMpB,MAAMC,eAAe;AAGrB,MAAMC,yBAAyB;AAM/B,MAAMC,qBAAqB;AAE3B,MAAMC,UAAkC;IAAE,SAAS;AAAK;AAGxD,MAAMC,eAAe,GAAGC,WAAW,CAAC,EAAEC,QAAQ,GAAG,EAAE;AAEnD,MAAMC,YAAY,CAACC;IACf,MAAMC,cAAcC,KAAK,GAAG,KAAKF;IACjC,OAAO,IAAIE,KAAKD;AACpB;AAYA,MAAME;IACF,YACqBC,aAA6C,EAC7CC,aAAsC,EACtCC,eAA0C,EAC1CC,kBAAuD,EACvDC,KAAoC,EACpCC,SAAgD,CACnE;aANmBL,aAAa,GAAbA;aACAC,aAAa,GAAbA;aACAC,eAAe,GAAfA;aACAC,kBAAkB,GAAlBA;aACAC,KAAK,GAALA;aACAC,SAAS,GAATA;IAClB;IAEH,MAAa,QACTC,QAA6C,EAC7CC,IAAkB,EACa;QAC/B,MAAMC,QAAQF,SAAS,KAAK,CAACG,kCAAkC;QAC/D,MAAMC,WAAWF,MAAM,MAAM,IAAI,IAAI,CAAC,aAAa,CAAC,SAAS,GAAG,EAAE;QAElE,MAAMG,QAAQ,MAAM,IAAI,CAAC,WAAW,CAACD,UAAUF;QAC/C,IAAI,CAACG,OACD,OAAOpB;QAEX,IAAIoB,MAAM,OAAO,KAAKb,KAAK,GAAG,KAAKR,oBAC/B,OAAOC;QAGX,MAAMqB,MAAM,GAAGF,SAAS,CAAC,EAAEF,MAAM,EAAE,CAAC,CAAC,EAAEG,MAAM,WAAW,IAAI;QAC5D,MAAME,UAAU,MAAM,IAAI,CAAC,SAAS,CAACD;QACrC,IAAI,CAACC,SACD,OAAOtB;QAGX,IAAI;YACA,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACe,UAAUC;QAClD,EAAE,OAAOO,OAAO;YACZ,MAAM,IAAI,CAAC,gBAAgB,CAACF;YAC5B,MAAME;QACV;IACJ;IAGA,MAAc,YACVJ,QAAgB,EAChBF,KAAmC,EACf;QACpB,MAAMO,eAAe,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAACL;QACtD,IAAIK,aAAa,MAAM,IACnB,MAAMA,aAAa,KAAK;QAG5B,OAAO,IAAI,CAAC,aAAa,CAAC,UAAU,CAACA,aAAa,KAAK,EAAE,IAE9C,IAAI,CAAC,eAAe,CAAC,oBAAoB,CAAC;gBAC7C,MAAMC,SAAS,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC;oBACjD,IAAIR,MAAM,EAAE;oBACZ,WAAWA,MAAM,SAAS;gBAC9B;gBACA,IAAIQ,OAAO,IAAI,IACX,OAAOA,OAAO,KAAK,CAAC,YAAY;gBAEpC,IAAIA,AAAsB,yCAAtBA,OAAO,KAAK,CAAC,IAAI,EACjB,OAAO;gBAEX,MAAMA,OAAO,KAAK;YACtB;IAER;IAEA,MAAc,UAAUJ,GAAW,EAAoB;QACnD,MAAMK,YAAYtB,UAAUP;QAC5B,MAAM4B,SAAS,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAACJ,KAAKpB,cAAc;YACnD,OAAOL;YACP8B;YACA,WAAW;gBAAE,QAAQ;YAAM;QAC/B;QACA,IAAID,OAAO,IAAI,IACX,OAAO;QAEX,IAAIA,OAAO,KAAK,YAAYE,8BACxB,OAAO;QAEX,MAAMF,OAAO,KAAK;IACtB;IAMA,MAAc,iBAAiBJ,GAAW,EAAiB;QACvD,MAAMK,YAAYtB,UAAUN;QAC5B,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAACuB,KAAKpB,cAAc;YAAE,OAAOL;YAAa8B;QAAU;IAC5E;AACJ;AAEO,MAAME,kCAAkCC,4BAA4B,eAAe,CAAC;IACvF,WAAWrB;IACX,cAAc;QACVsB;QACAC;QACAC;QACAC;QACAC;KACH;AACL"}
@@ -3,6 +3,7 @@ import type { IEventHandler } from "@webiny/event-handler-core";
3
3
  import type { IScheduledActionRecoverEvent } from "./ScheduledActionRecoverEventType.js";
4
4
  export interface IScheduledActionRecoverResult {
5
5
  recovered: number;
6
+ failedTenants: string[];
6
7
  }
7
8
  export interface IScheduledActionRecoverEventHandler extends IEventHandler<IScheduledActionRecoverEvent, IScheduledActionRecoverResult> {
8
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler/ScheduledActionRecoverEventHandler.js","sources":["../../src/scheduler/ScheduledActionRecoverEventHandler.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\nimport type { IEventHandler } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\n\nexport interface IScheduledActionRecoverResult {\n recovered: number;\n}\n\nexport interface IScheduledActionRecoverEventHandler extends IEventHandler<\n IScheduledActionRecoverEvent,\n IScheduledActionRecoverResult\n> {}\n\nexport const ScheduledActionRecoverEventHandler =\n new Abstraction<IScheduledActionRecoverEventHandler>(\"ScheduledActionRecoverEventHandler\");\n\nexport namespace ScheduledActionRecoverEventHandler {\n export type Interface = IScheduledActionRecoverEventHandler;\n export type Result = IScheduledActionRecoverResult;\n}\n"],"names":["ScheduledActionRecoverEventHandler","Abstraction"],"mappings":";AAaO,MAAMA,qCACT,IAAIC,YAAiD"}
1
+ {"version":3,"file":"scheduler/ScheduledActionRecoverEventHandler.js","sources":["../../src/scheduler/ScheduledActionRecoverEventHandler.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\nimport type { IEventHandler } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\n\nexport interface IScheduledActionRecoverResult {\n recovered: number;\n // Tenants whose schedules could not be read or re-armed; the others were still recovered.\n failedTenants: string[];\n}\n\nexport interface IScheduledActionRecoverEventHandler extends IEventHandler<\n IScheduledActionRecoverEvent,\n IScheduledActionRecoverResult\n> {}\n\nexport const ScheduledActionRecoverEventHandler =\n new Abstraction<IScheduledActionRecoverEventHandler>(\"ScheduledActionRecoverEventHandler\");\n\nexport namespace ScheduledActionRecoverEventHandler {\n export type Interface = IScheduledActionRecoverEventHandler;\n export type Result = IScheduledActionRecoverResult;\n}\n"],"names":["ScheduledActionRecoverEventHandler","Abstraction"],"mappings":";AAeO,MAAMA,qCACT,IAAIC,YAAiD"}
@@ -10,8 +10,16 @@ import type { Container } from "@webiny/di";
10
10
  */
11
11
  export declare function registerSchedulerServer(rootContainer: Container): void;
12
12
  /**
13
- * Boot step (onServer): start the timers, then re-arm the persisted schedules. Recovery runs in the
14
- * background: it executes overdue actions one by one, and the server should not wait for that
15
- * before it starts listening. A failure is logged and never crashes startup.
13
+ * Boot step (onServer): start the timers, then re-arm the persisted schedules, at boot and then every
14
+ * minute. Recovery runs in the background: it executes overdue actions one by one, and the server
15
+ * should not wait for that before it starts listening. A failure is logged and never crashes startup.
16
+ *
17
+ * Running it every minute is what makes several processes work. Each holds its own timers, so an
18
+ * action scheduled or moved through one process reaches the others' timers within a minute, and an
19
+ * action whose process died is still fired, as an overdue one. `RunScheduledActionOnceDecorator`
20
+ * keeps the processes from all running it. Runs never overlap.
21
+ *
22
+ * Returns a function for the shutdown: it stops the timers and recovery, then waits up to
23
+ * `timeoutMs` for actions already running.
16
24
  */
17
- export declare function startSchedulerServer(rootContainer: Container): Promise<void>;
25
+ export declare function startSchedulerServer(rootContainer: Container): Promise<(timeoutMs: number) => Promise<void>>;
@@ -5,20 +5,13 @@ import { BreeSchedulerService } from "@webiny/api-scheduler-standalone";
5
5
  import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
6
6
  import { SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER, ScheduledActionRecoverEventType } from "./ScheduledActionRecoverEventType.js";
7
7
  import { RecoverScheduledActionsHandler } from "./RecoverScheduledActionsHandler.js";
8
- const consoleLogger = {
9
- trace: (...args)=>console.trace(...args),
10
- debug: (...args)=>console.debug(...args),
11
- info: (...args)=>console.info(...args),
12
- warn: (...args)=>console.warn(...args),
13
- error: (...args)=>console.error(...args),
14
- fatal: (...args)=>console.error(...args),
15
- log: (...args)=>console.log(...args)
16
- };
8
+ import { RunScheduledActionOnceDecorator } from "./RunScheduledActionOnceDecorator.js";
9
+ import { consoleLogger } from "../logger/consoleLogger.js";
17
10
  function registerSchedulerServer(rootContainer) {
18
11
  const dispatcher = rootContainer.resolve(EventDispatcher);
19
12
  const service = new BreeSchedulerService({
20
13
  logger: consoleLogger,
21
- onTrigger: async (id, namespace, tenant)=>{
14
+ runAction: async (id, namespace, tenant)=>{
22
15
  const event = {
23
16
  [SCHEDULED_ACTION_EVENT_IDENTIFIER]: {
24
17
  id,
@@ -30,7 +23,11 @@ function registerSchedulerServer(rootContainer) {
30
23
  try {
31
24
  await dispatcher.dispatch(event);
32
25
  } catch (err) {
33
- console.error(`[scheduler] scheduled action "${id}" failed:`, err);
26
+ consoleLogger.error({
27
+ error: err,
28
+ id,
29
+ tenant
30
+ }, "[scheduler] scheduled action failed");
34
31
  }
35
32
  }
36
33
  });
@@ -38,24 +35,52 @@ function registerSchedulerServer(rootContainer) {
38
35
  rootContainer.registerInstance(SchedulerService, service);
39
36
  rootContainer.register(ScheduledActionEventType);
40
37
  rootContainer.register(ScheduledActionLambdaHandler);
38
+ rootContainer.registerDecorator(RunScheduledActionOnceDecorator);
41
39
  rootContainer.register(ScheduledActionRecoverEventType);
42
40
  rootContainer.register(RecoverScheduledActionsHandler);
43
41
  }
44
- async function recoverPendingSchedules(dispatcher) {
42
+ const RECOVER_EVERY_MS = 60000;
43
+ async function recoverPendingSchedules(dispatcher, atBoot) {
45
44
  const event = {
46
45
  [SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER]: true
47
46
  };
48
47
  try {
49
48
  const result = await dispatcher.dispatch(event);
50
- console.log(`[scheduler] boot recovery: re-armed ${result.recovered} pending action(s)`);
49
+ if (atBoot) consoleLogger.info({
50
+ recovered: result.recovered
51
+ }, "[scheduler] boot recovery re-armed pending actions");
52
+ if (result.failedTenants.length > 0) consoleLogger.error({
53
+ tenants: result.failedTenants
54
+ }, "[scheduler] recovery failed for some tenants");
51
55
  } catch (err) {
52
- console.error("[scheduler] boot recovery failed:", err);
56
+ consoleLogger.error({
57
+ error: err
58
+ }, "[scheduler] recovery failed");
53
59
  }
54
60
  }
55
61
  async function startSchedulerServer(rootContainer) {
56
- await rootContainer.resolve(SchedulerSingleton).start();
62
+ const scheduler = rootContainer.resolve(SchedulerSingleton);
63
+ await scheduler.start();
57
64
  const dispatcher = rootContainer.resolve(EventDispatcher);
58
- recoverPendingSchedules(dispatcher);
65
+ let recovering = false;
66
+ const recover = async (atBoot)=>{
67
+ if (recovering) return;
68
+ recovering = true;
69
+ try {
70
+ await recoverPendingSchedules(dispatcher, atBoot);
71
+ } finally{
72
+ recovering = false;
73
+ }
74
+ };
75
+ recover(true);
76
+ const everyMinute = setInterval(()=>{
77
+ recover(false);
78
+ }, RECOVER_EVERY_MS);
79
+ return async (timeoutMs)=>{
80
+ clearInterval(everyMinute);
81
+ await scheduler.stop();
82
+ await scheduler.waitForRunning(timeoutMs);
83
+ };
59
84
  }
60
85
  export { registerSchedulerServer, startSchedulerServer };
61
86
 
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler/schedulerServer.js","sources":["../../src/scheduler/schedulerServer.ts"],"sourcesContent":["import type { Container } from \"@webiny/di\";\nimport { EventDispatcher } from \"@webiny/event-handler-core\";\nimport { ScheduledActionEventType } from \"@webiny/event-handler-core\";\nimport { SCHEDULED_ACTION_EVENT_IDENTIFIER } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEvent } from \"@webiny/event-handler-core\";\nimport { SchedulerService } from \"@webiny/api-scheduler/shared/abstractions.js\";\nimport { ScheduledActionLambdaHandler } from \"@webiny/api-scheduler\";\nimport { BreeSchedulerService } from \"@webiny/api-scheduler-standalone\";\nimport type { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\nimport { ScheduledActionRecoverEventType } from \"./ScheduledActionRecoverEventType.js\";\nimport { SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER } from \"./ScheduledActionRecoverEventType.js\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\nimport { RecoverScheduledActionsHandler } from \"./RecoverScheduledActionsHandler.js\";\nimport { ScheduledActionRecoverEventHandler } from \"./ScheduledActionRecoverEventHandler.js\";\n\n/**\n * Minimal console-backed logger for the root scheduler singleton. The real DI Logger is registered\n * per-request (via ApiCoreFeature/LoggerFeature), so it isn't resolvable here at the root where the\n * singleton is built. And even if we bridged to it, timers fire outside any request — there's no\n * request context for a context-aware logger to enrich — so console is functionally equivalent, not\n * a downgrade. Revisit (a shared boot-time logger for all root singletons) tracked in\n * https://github.com/webiny/webiny-js/issues/5446.\n */\nconst consoleLogger: Logger.Interface = {\n trace: (...args: any[]) => console.trace(...args),\n debug: (...args: any[]) => console.debug(...args),\n info: (...args: any[]) => console.info(...args),\n warn: (...args: any[]) => console.warn(...args),\n error: (...args: any[]) => console.error(...args),\n fatal: (...args: any[]) => console.error(...args),\n log: (...args: any[]) => console.log(...args)\n};\n\n/**\n * ROOT wiring for the standalone (Bree, in-process) scheduler. Unlike AWS (per-request EventBridge\n * binding), the server holds ONE long-lived Bree instance for all tenants, started once at boot —\n * the counterpart of the WebSockets connection manager. Registered as `SchedulerService` so\n * per-request create/update/delete (during GraphQL mutations) manipulate that single live timer set.\n *\n * When a timer fires (outside any request), the singleton dispatches a `ScheduledActionEvent`. The\n * handler is the one AWS runs for an EventBridge Scheduler invocation, in a fresh request container.\n */\nexport function registerSchedulerServer(rootContainer: Container): void {\n const dispatcher = rootContainer.resolve(EventDispatcher);\n\n const service = new BreeSchedulerService({\n logger: consoleLogger,\n onTrigger: async (id, namespace, tenant) => {\n /*\n * `scheduleFor` is the time it fired. The event type needs one to recognise the event,\n * and nothing downstream reads it.\n */\n const event: IScheduledActionEvent = {\n [SCHEDULED_ACTION_EVENT_IDENTIFIER]: {\n id,\n namespace,\n tenant,\n scheduleFor: new Date().toISOString()\n }\n };\n try {\n await dispatcher.dispatch(event);\n } catch (err) {\n console.error(`[scheduler] scheduled action \"${id}\" failed:`, err);\n }\n }\n });\n\n // Register the ONE Bree instance under TWO tokens — two views of the same object:\n //\n // - SchedulerService: the hosting-agnostic contract (create/update/delete/exists) that AWS\n // implements too. This is what per-request GraphQL mutations resolve to schedule/reschedule.\n // - SchedulerSingleton: typed as the CONCRETE BreeSchedulerService, so it also exposes the\n // methods that aren't on that contract — start() and recover(). Those are Bree-only: AWS's\n // EventBridge is managed infra (nothing to start) and persists its own schedules (nothing to\n // recover), so they don't belong on the shared interface. The boot step + recover handler\n // resolve this token precisely because they need start()/recover().\n //\n // registerInstance (not register) because it's a single live object holding all tenants' timers —\n // every caller must get the SAME instance, not a per-scope construction. Registered at root, so\n // per-request child containers inherit it via the parent chain; nothing registers a per-request\n // SchedulerService default that would shadow it (unlike e.g. the WS transport's Null default).\n rootContainer.registerInstance(SchedulerSingleton, service);\n rootContainer.registerInstance(SchedulerService, service);\n\n rootContainer.register(ScheduledActionEventType);\n rootContainer.register(ScheduledActionLambdaHandler);\n\n rootContainer.register(ScheduledActionRecoverEventType);\n rootContainer.register(RecoverScheduledActionsHandler);\n}\n\nasync function recoverPendingSchedules(dispatcher: EventDispatcher.Interface): Promise<void> {\n const event: IScheduledActionRecoverEvent = {\n [SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER]: true\n };\n\n try {\n const result = await dispatcher.dispatch<ScheduledActionRecoverEventHandler.Result>(event);\n console.log(`[scheduler] boot recovery: re-armed ${result.recovered} pending action(s)`);\n } catch (err) {\n console.error(\"[scheduler] boot recovery failed:\", err);\n }\n}\n\n/**\n * Boot step (onServer): start the timers, then re-arm the persisted schedules. Recovery runs in the\n * background: it executes overdue actions one by one, and the server should not wait for that\n * before it starts listening. A failure is logged and never crashes startup.\n */\nexport async function startSchedulerServer(rootContainer: Container): Promise<void> {\n await rootContainer.resolve(SchedulerSingleton).start();\n\n const dispatcher = rootContainer.resolve(EventDispatcher);\n void recoverPendingSchedules(dispatcher);\n}\n"],"names":["consoleLogger","args","console","registerSchedulerServer","rootContainer","dispatcher","EventDispatcher","service","BreeSchedulerService","id","namespace","tenant","event","SCHEDULED_ACTION_EVENT_IDENTIFIER","Date","err","SchedulerSingleton","SchedulerService","ScheduledActionEventType","ScheduledActionLambdaHandler","ScheduledActionRecoverEventType","RecoverScheduledActionsHandler","recoverPendingSchedules","SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER","result","startSchedulerServer"],"mappings":";;;;;;;AAwBA,MAAMA,gBAAkC;IACpC,OAAO,CAAC,GAAGC,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,MAAM,CAAC,GAAGA,OAAgBC,QAAQ,IAAI,IAAID;IAC1C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,OAAO,CAAC,GAAGA,OAAgBC,QAAQ,KAAK,IAAID;IAC5C,KAAK,CAAC,GAAGA,OAAgBC,QAAQ,GAAG,IAAID;AAC5C;AAWO,SAASE,wBAAwBC,aAAwB;IAC5D,MAAMC,aAAaD,cAAc,OAAO,CAACE;IAEzC,MAAMC,UAAU,IAAIC,qBAAqB;QACrC,QAAQR;QACR,WAAW,OAAOS,IAAIC,WAAWC;YAK7B,MAAMC,QAA+B;gBACjC,CAACC,kCAAkC,EAAE;oBACjCJ;oBACAC;oBACAC;oBACA,aAAa,IAAIG,OAAO,WAAW;gBACvC;YACJ;YACA,IAAI;gBACA,MAAMT,WAAW,QAAQ,CAACO;YAC9B,EAAE,OAAOG,KAAK;gBACVb,QAAQ,KAAK,CAAC,CAAC,8BAA8B,EAAEO,GAAG,SAAS,CAAC,EAAEM;YAClE;QACJ;IACJ;IAgBAX,cAAc,gBAAgB,CAACY,oBAAoBT;IACnDH,cAAc,gBAAgB,CAACa,kBAAkBV;IAEjDH,cAAc,QAAQ,CAACc;IACvBd,cAAc,QAAQ,CAACe;IAEvBf,cAAc,QAAQ,CAACgB;IACvBhB,cAAc,QAAQ,CAACiB;AAC3B;AAEA,eAAeC,wBAAwBjB,UAAqC;IACxE,MAAMO,QAAsC;QACxC,CAACW,0CAA0C,EAAE;IACjD;IAEA,IAAI;QACA,MAAMC,SAAS,MAAMnB,WAAW,QAAQ,CAA4CO;QACpFV,QAAQ,GAAG,CAAC,CAAC,oCAAoC,EAAEsB,OAAO,SAAS,CAAC,kBAAkB,CAAC;IAC3F,EAAE,OAAOT,KAAK;QACVb,QAAQ,KAAK,CAAC,qCAAqCa;IACvD;AACJ;AAOO,eAAeU,qBAAqBrB,aAAwB;IAC/D,MAAMA,cAAc,OAAO,CAACY,oBAAoB,KAAK;IAErD,MAAMX,aAAaD,cAAc,OAAO,CAACE;IACpCgB,wBAAwBjB;AACjC"}
1
+ {"version":3,"file":"scheduler/schedulerServer.js","sources":["../../src/scheduler/schedulerServer.ts"],"sourcesContent":["import type { Container } from \"@webiny/di\";\nimport { EventDispatcher } from \"@webiny/event-handler-core\";\nimport { ScheduledActionEventType } from \"@webiny/event-handler-core\";\nimport { SCHEDULED_ACTION_EVENT_IDENTIFIER } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEvent } from \"@webiny/event-handler-core\";\nimport { SchedulerService } from \"@webiny/api-scheduler/shared/abstractions.js\";\nimport { ScheduledActionLambdaHandler } from \"@webiny/api-scheduler\";\nimport { BreeSchedulerService } from \"@webiny/api-scheduler-standalone\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\nimport { ScheduledActionRecoverEventType } from \"./ScheduledActionRecoverEventType.js\";\nimport { SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER } from \"./ScheduledActionRecoverEventType.js\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\nimport { RecoverScheduledActionsHandler } from \"./RecoverScheduledActionsHandler.js\";\nimport { ScheduledActionRecoverEventHandler } from \"./ScheduledActionRecoverEventHandler.js\";\nimport { RunScheduledActionOnceDecorator } from \"./RunScheduledActionOnceDecorator.js\";\nimport { consoleLogger } from \"~/logger/consoleLogger.js\";\n\n/**\n * ROOT wiring for the standalone (Bree, in-process) scheduler. Unlike AWS (per-request EventBridge\n * binding), the server holds ONE long-lived Bree instance for all tenants, started once at boot —\n * the counterpart of the WebSockets connection manager. Registered as `SchedulerService` so\n * per-request create/update/delete (during GraphQL mutations) manipulate that single live timer set.\n *\n * When a timer fires (outside any request), the singleton dispatches a `ScheduledActionEvent`. The\n * handler is the one AWS runs for an EventBridge Scheduler invocation, in a fresh request container.\n */\nexport function registerSchedulerServer(rootContainer: Container): void {\n const dispatcher = rootContainer.resolve(EventDispatcher);\n\n const service = new BreeSchedulerService({\n logger: consoleLogger,\n runAction: async (id, namespace, tenant) => {\n /*\n * `scheduleFor` is the time it fired. The event type needs one to recognise the event,\n * and nothing downstream reads it.\n */\n const event: IScheduledActionEvent = {\n [SCHEDULED_ACTION_EVENT_IDENTIFIER]: {\n id,\n namespace,\n tenant,\n scheduleFor: new Date().toISOString()\n }\n };\n try {\n await dispatcher.dispatch(event);\n } catch (err) {\n consoleLogger.error(\n { error: err, id, tenant },\n \"[scheduler] scheduled action failed\"\n );\n }\n }\n });\n\n // Register the ONE Bree instance under TWO tokens — two views of the same object:\n //\n // - SchedulerService: the hosting-agnostic contract (create/update/delete/exists) that AWS\n // implements too. This is what per-request GraphQL mutations resolve to schedule/reschedule.\n // - SchedulerSingleton: typed as the CONCRETE BreeSchedulerService, so it also exposes the\n // methods that aren't on that contract — start() and recover(). Those are Bree-only: AWS's\n // EventBridge is managed infra (nothing to start) and persists its own schedules (nothing to\n // recover), so they don't belong on the shared interface. The boot step + recover handler\n // resolve this token precisely because they need start()/recover().\n //\n // registerInstance (not register) because it's a single live object holding all tenants' timers —\n // every caller must get the SAME instance, not a per-scope construction. Registered at root, so\n // per-request child containers inherit it via the parent chain; nothing registers a per-request\n // SchedulerService default that would shadow it (unlike e.g. the WS transport's Null default).\n rootContainer.registerInstance(SchedulerSingleton, service);\n rootContainer.registerInstance(SchedulerService, service);\n\n rootContainer.register(ScheduledActionEventType);\n rootContainer.register(ScheduledActionLambdaHandler);\n // Every process holds timers for the same actions; this lets only one of them run each action.\n rootContainer.registerDecorator(RunScheduledActionOnceDecorator);\n\n rootContainer.register(ScheduledActionRecoverEventType);\n rootContainer.register(RecoverScheduledActionsHandler);\n}\n\n// How often each process re-arms pending actions and fires overdue ones.\nconst RECOVER_EVERY_MS = 60_000;\n\nasync function recoverPendingSchedules(\n dispatcher: EventDispatcher.Interface,\n atBoot: boolean\n): Promise<void> {\n const event: IScheduledActionRecoverEvent = {\n [SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER]: true\n };\n\n try {\n const result = await dispatcher.dispatch<ScheduledActionRecoverEventHandler.Result>(event);\n if (atBoot) {\n consoleLogger.info(\n { recovered: result.recovered },\n \"[scheduler] boot recovery re-armed pending actions\"\n );\n }\n if (result.failedTenants.length > 0) {\n consoleLogger.error(\n { tenants: result.failedTenants },\n \"[scheduler] recovery failed for some tenants\"\n );\n }\n } catch (err) {\n consoleLogger.error({ error: err }, \"[scheduler] recovery failed\");\n }\n}\n\n/**\n * Boot step (onServer): start the timers, then re-arm the persisted schedules, at boot and then every\n * minute. Recovery runs in the background: it executes overdue actions one by one, and the server\n * should not wait for that before it starts listening. A failure is logged and never crashes startup.\n *\n * Running it every minute is what makes several processes work. Each holds its own timers, so an\n * action scheduled or moved through one process reaches the others' timers within a minute, and an\n * action whose process died is still fired, as an overdue one. `RunScheduledActionOnceDecorator`\n * keeps the processes from all running it. Runs never overlap.\n *\n * Returns a function for the shutdown: it stops the timers and recovery, then waits up to\n * `timeoutMs` for actions already running.\n */\nexport async function startSchedulerServer(\n rootContainer: Container\n): Promise<(timeoutMs: number) => Promise<void>> {\n const scheduler = rootContainer.resolve(SchedulerSingleton);\n await scheduler.start();\n\n const dispatcher = rootContainer.resolve(EventDispatcher);\n let recovering = false;\n const recover = async (atBoot: boolean) => {\n if (recovering) {\n return;\n }\n recovering = true;\n try {\n await recoverPendingSchedules(dispatcher, atBoot);\n } finally {\n recovering = false;\n }\n };\n\n void recover(true);\n const everyMinute = setInterval(() => {\n void recover(false);\n }, RECOVER_EVERY_MS);\n\n return async (timeoutMs: number) => {\n clearInterval(everyMinute);\n await scheduler.stop();\n await scheduler.waitForRunning(timeoutMs);\n };\n}\n"],"names":["registerSchedulerServer","rootContainer","dispatcher","EventDispatcher","service","BreeSchedulerService","consoleLogger","id","namespace","tenant","event","SCHEDULED_ACTION_EVENT_IDENTIFIER","Date","err","SchedulerSingleton","SchedulerService","ScheduledActionEventType","ScheduledActionLambdaHandler","RunScheduledActionOnceDecorator","ScheduledActionRecoverEventType","RecoverScheduledActionsHandler","RECOVER_EVERY_MS","recoverPendingSchedules","atBoot","SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER","result","startSchedulerServer","scheduler","recovering","recover","everyMinute","setInterval","timeoutMs","clearInterval"],"mappings":";;;;;;;;;AA0BO,SAASA,wBAAwBC,aAAwB;IAC5D,MAAMC,aAAaD,cAAc,OAAO,CAACE;IAEzC,MAAMC,UAAU,IAAIC,qBAAqB;QACrC,QAAQC;QACR,WAAW,OAAOC,IAAIC,WAAWC;YAK7B,MAAMC,QAA+B;gBACjC,CAACC,kCAAkC,EAAE;oBACjCJ;oBACAC;oBACAC;oBACA,aAAa,IAAIG,OAAO,WAAW;gBACvC;YACJ;YACA,IAAI;gBACA,MAAMV,WAAW,QAAQ,CAACQ;YAC9B,EAAE,OAAOG,KAAK;gBACVP,cAAc,KAAK,CACf;oBAAE,OAAOO;oBAAKN;oBAAIE;gBAAO,GACzB;YAER;QACJ;IACJ;IAgBAR,cAAc,gBAAgB,CAACa,oBAAoBV;IACnDH,cAAc,gBAAgB,CAACc,kBAAkBX;IAEjDH,cAAc,QAAQ,CAACe;IACvBf,cAAc,QAAQ,CAACgB;IAEvBhB,cAAc,iBAAiB,CAACiB;IAEhCjB,cAAc,QAAQ,CAACkB;IACvBlB,cAAc,QAAQ,CAACmB;AAC3B;AAGA,MAAMC,mBAAmB;AAEzB,eAAeC,wBACXpB,UAAqC,EACrCqB,MAAe;IAEf,MAAMb,QAAsC;QACxC,CAACc,0CAA0C,EAAE;IACjD;IAEA,IAAI;QACA,MAAMC,SAAS,MAAMvB,WAAW,QAAQ,CAA4CQ;QACpF,IAAIa,QACAjB,cAAc,IAAI,CACd;YAAE,WAAWmB,OAAO,SAAS;QAAC,GAC9B;QAGR,IAAIA,OAAO,aAAa,CAAC,MAAM,GAAG,GAC9BnB,cAAc,KAAK,CACf;YAAE,SAASmB,OAAO,aAAa;QAAC,GAChC;IAGZ,EAAE,OAAOZ,KAAK;QACVP,cAAc,KAAK,CAAC;YAAE,OAAOO;QAAI,GAAG;IACxC;AACJ;AAeO,eAAea,qBAClBzB,aAAwB;IAExB,MAAM0B,YAAY1B,cAAc,OAAO,CAACa;IACxC,MAAMa,UAAU,KAAK;IAErB,MAAMzB,aAAaD,cAAc,OAAO,CAACE;IACzC,IAAIyB,aAAa;IACjB,MAAMC,UAAU,OAAON;QACnB,IAAIK,YACA;QAEJA,aAAa;QACb,IAAI;YACA,MAAMN,wBAAwBpB,YAAYqB;QAC9C,SAAU;YACNK,aAAa;QACjB;IACJ;IAEKC,QAAQ;IACb,MAAMC,cAAcC,YAAY;QACvBF,QAAQ;IACjB,GAAGR;IAEH,OAAO,OAAOW;QACVC,cAAcH;QACd,MAAMH,UAAU,IAAI;QACpB,MAAMA,UAAU,cAAc,CAACK;IACnC;AACJ"}