paperkite 0.1.0

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.
Files changed (121) hide show
  1. package/README.md +81 -0
  2. package/data/flows.example.yml +65 -0
  3. package/data/settings.example.yml +13 -0
  4. package/dist/data/hooks/weekend-slot.d.ts +12 -0
  5. package/dist/data/hooks/weekend-slot.js +107 -0
  6. package/dist/data/hooks/weekend-slot.js.map +1 -0
  7. package/dist/packages/account-health/src/index.d.ts +8 -0
  8. package/dist/packages/account-health/src/index.js +37 -0
  9. package/dist/packages/account-health/src/index.js.map +1 -0
  10. package/dist/packages/archive/src/archiver.d.ts +98 -0
  11. package/dist/packages/archive/src/archiver.js +399 -0
  12. package/dist/packages/archive/src/archiver.js.map +1 -0
  13. package/dist/packages/archive/src/config.d.ts +22 -0
  14. package/dist/packages/archive/src/config.js +62 -0
  15. package/dist/packages/archive/src/config.js.map +1 -0
  16. package/dist/packages/archive/src/console/live.d.ts +24 -0
  17. package/dist/packages/archive/src/console/live.js +144 -0
  18. package/dist/packages/archive/src/console/live.js.map +1 -0
  19. package/dist/packages/archive/src/console/media.d.ts +66 -0
  20. package/dist/packages/archive/src/console/media.js +368 -0
  21. package/dist/packages/archive/src/console/media.js.map +1 -0
  22. package/dist/packages/archive/src/console/server.d.ts +12 -0
  23. package/dist/packages/archive/src/console/server.js +583 -0
  24. package/dist/packages/archive/src/console/server.js.map +1 -0
  25. package/dist/packages/archive/src/console/service.d.ts +13 -0
  26. package/dist/packages/archive/src/console/service.js +86 -0
  27. package/dist/packages/archive/src/console/service.js.map +1 -0
  28. package/dist/packages/archive/src/storage/index.d.ts +6 -0
  29. package/dist/packages/archive/src/storage/index.js +23 -0
  30. package/dist/packages/archive/src/storage/index.js.map +1 -0
  31. package/dist/packages/archive/src/storage/model.d.ts +296 -0
  32. package/dist/packages/archive/src/storage/model.js +151 -0
  33. package/dist/packages/archive/src/storage/model.js.map +1 -0
  34. package/dist/packages/archive/src/storage/postgres.d.ts +67 -0
  35. package/dist/packages/archive/src/storage/postgres.js +1021 -0
  36. package/dist/packages/archive/src/storage/postgres.js.map +1 -0
  37. package/dist/packages/archive/src/storage/sqlite.d.ts +59 -0
  38. package/dist/packages/archive/src/storage/sqlite.js +981 -0
  39. package/dist/packages/archive/src/storage/sqlite.js.map +1 -0
  40. package/dist/packages/notify-bark/src/index.d.ts +33 -0
  41. package/dist/packages/notify-bark/src/index.js +96 -0
  42. package/dist/packages/notify-bark/src/index.js.map +1 -0
  43. package/dist/packages/runtime-console/src/console/errors.d.ts +5 -0
  44. package/dist/packages/runtime-console/src/console/errors.js +11 -0
  45. package/dist/packages/runtime-console/src/console/errors.js.map +1 -0
  46. package/dist/packages/runtime-console/src/console/logs.d.ts +1 -0
  47. package/dist/packages/runtime-console/src/console/logs.js +27 -0
  48. package/dist/packages/runtime-console/src/console/logs.js.map +1 -0
  49. package/dist/packages/runtime-console/src/console/server.d.ts +6 -0
  50. package/dist/packages/runtime-console/src/console/server.js +182 -0
  51. package/dist/packages/runtime-console/src/console/server.js.map +1 -0
  52. package/dist/packages/runtime-console/src/index.d.ts +10 -0
  53. package/dist/packages/runtime-console/src/index.js +83 -0
  54. package/dist/packages/runtime-console/src/index.js.map +1 -0
  55. package/dist/src/app.d.ts +15 -0
  56. package/dist/src/app.js +34 -0
  57. package/dist/src/app.js.map +1 -0
  58. package/dist/src/cli.d.ts +2 -0
  59. package/dist/src/cli.js +250 -0
  60. package/dist/src/cli.js.map +1 -0
  61. package/dist/src/config/editor.d.ts +2 -0
  62. package/dist/src/config/editor.js +19 -0
  63. package/dist/src/config/editor.js.map +1 -0
  64. package/dist/src/config/loader.d.ts +6 -0
  65. package/dist/src/config/loader.js +188 -0
  66. package/dist/src/config/loader.js.map +1 -0
  67. package/dist/src/config/model.d.ts +71 -0
  68. package/dist/src/config/model.js +97 -0
  69. package/dist/src/config/model.js.map +1 -0
  70. package/dist/src/config/paths.d.ts +3 -0
  71. package/dist/src/config/paths.js +12 -0
  72. package/dist/src/config/paths.js.map +1 -0
  73. package/dist/src/config/settings.d.ts +19 -0
  74. package/dist/src/config/settings.js +44 -0
  75. package/dist/src/config/settings.js.map +1 -0
  76. package/dist/src/control/process-lock.d.ts +4 -0
  77. package/dist/src/control/process-lock.js +57 -0
  78. package/dist/src/control/process-lock.js.map +1 -0
  79. package/dist/src/control/socket.d.ts +14 -0
  80. package/dist/src/control/socket.js +131 -0
  81. package/dist/src/control/socket.js.map +1 -0
  82. package/dist/src/engine/hooks.d.ts +4 -0
  83. package/dist/src/engine/hooks.js +38 -0
  84. package/dist/src/engine/hooks.js.map +1 -0
  85. package/dist/src/engine/logger.d.ts +17 -0
  86. package/dist/src/engine/logger.js +78 -0
  87. package/dist/src/engine/logger.js.map +1 -0
  88. package/dist/src/engine/runtime.d.ts +78 -0
  89. package/dist/src/engine/runtime.js +799 -0
  90. package/dist/src/engine/runtime.js.map +1 -0
  91. package/dist/src/engine/scheduler.d.ts +9 -0
  92. package/dist/src/engine/scheduler.js +58 -0
  93. package/dist/src/engine/scheduler.js.map +1 -0
  94. package/dist/src/extensions/loader.d.ts +11 -0
  95. package/dist/src/extensions/loader.js +202 -0
  96. package/dist/src/extensions/loader.js.map +1 -0
  97. package/dist/src/extensions/manager.d.ts +1 -0
  98. package/dist/src/extensions/manager.js +121 -0
  99. package/dist/src/extensions/manager.js.map +1 -0
  100. package/dist/src/extensions/profile.d.ts +14 -0
  101. package/dist/src/extensions/profile.js +57 -0
  102. package/dist/src/extensions/profile.js.map +1 -0
  103. package/dist/src/extensions/registry.d.ts +19 -0
  104. package/dist/src/extensions/registry.js +62 -0
  105. package/dist/src/extensions/registry.js.map +1 -0
  106. package/dist/src/telegram/client.d.ts +20 -0
  107. package/dist/src/telegram/client.js +54 -0
  108. package/dist/src/telegram/client.js.map +1 -0
  109. package/dist/src/telegram/failure.d.ts +2 -0
  110. package/dist/src/telegram/failure.js +98 -0
  111. package/dist/src/telegram/failure.js.map +1 -0
  112. package/dist/src/telegram/login.d.ts +1 -0
  113. package/dist/src/telegram/login.js +31 -0
  114. package/dist/src/telegram/login.js.map +1 -0
  115. package/dist/src/telegram/pool.d.ts +47 -0
  116. package/dist/src/telegram/pool.js +300 -0
  117. package/dist/src/telegram/pool.js.map +1 -0
  118. package/dist/src/telegram/session-files.d.ts +4 -0
  119. package/dist/src/telegram/session-files.js +34 -0
  120. package/dist/src/telegram/session-files.js.map +1 -0
  121. package/package.json +62 -0
@@ -0,0 +1,799 @@
1
+ import { SessionUnavailableError } from "@paperkite/sdk";
2
+ import { basename, join } from "node:path";
3
+ import { updateFlowItem } from "../config/loader.js";
4
+ import { AppLogger } from "./logger.js";
5
+ import { invalidateAllHooks, invalidateHook, loadHook } from "./hooks.js";
6
+ import { RuntimeScheduler } from "./scheduler.js";
7
+ import { CapabilityRegistry } from "../extensions/registry.js";
8
+ export class Runtime {
9
+ options;
10
+ static DEFAULT_STOP_GRACE_MS = 10_000;
11
+ scheduler = new RuntimeScheduler();
12
+ controllers = new Map();
13
+ runs = new Map();
14
+ tasks = new Set();
15
+ listeners = new Set();
16
+ serviceStartedAt = new Map();
17
+ activeActionEntries = new Map();
18
+ suspended = new Map();
19
+ catalog;
20
+ lifecycle = new AbortController();
21
+ started = false;
22
+ startedAt = 0;
23
+ stopping;
24
+ reloading;
25
+ constructor(options) {
26
+ this.options = options;
27
+ this.catalog = options.catalog;
28
+ this.sessionUnsub = options.sessions.subscribe((change) => this.handleSessionChange(change));
29
+ }
30
+ sessionUnsub;
31
+ get snapshot() {
32
+ return {
33
+ running: this.started,
34
+ pid: process.pid,
35
+ uptimeSeconds: this.started ? (Date.now() - this.startedAt) / 1000 : 0,
36
+ triggers: this.catalog.enabled("trigger").map((item) => item.id),
37
+ services: this.catalog.enabled("service").map((item) => item.id),
38
+ schedules: this.catalog.enabled("schedule").map((item) => item.id),
39
+ activeServices: [...this.controllers.keys()]
40
+ .filter((key) => key.startsWith("service:"))
41
+ .map((key) => key.slice("service:".length)),
42
+ activeActions: [...this.activeActionEntries.values()].map((entry) => ({
43
+ id: entry.id,
44
+ capability: entry.capability,
45
+ session: entry.session,
46
+ flow: entry.flow,
47
+ startedAt: new Date(entry.startedAt).toISOString()
48
+ })),
49
+ flows: this.catalog.definitions().map((item) => this.flowSnapshot(item)),
50
+ sessions: this.sessionSnapshots(),
51
+ logs: this.logScopes()
52
+ };
53
+ }
54
+ listPlugins() {
55
+ return this.options.installed;
56
+ }
57
+ subscribe(listener) {
58
+ this.listeners.add(listener);
59
+ return () => this.listeners.delete(listener);
60
+ }
61
+ async start() {
62
+ if (this.started)
63
+ return;
64
+ await this.settleStopping();
65
+ if (this.lifecycle.signal.aborted)
66
+ this.lifecycle = new AbortController();
67
+ const sessions = collectSessions(this.catalog);
68
+ try {
69
+ if (sessions.size)
70
+ await this.options.sessions.ensure(sessions);
71
+ this.started = true;
72
+ this.startedAt = Date.now();
73
+ this.startFlows();
74
+ for (const info of this.options.sessions.states()) {
75
+ if (info.state === "waiting-auth") {
76
+ this.options.logger.warn("session " + info.name + " is waiting for login; run `paperkite session login " + info.name + "` to log in");
77
+ }
78
+ }
79
+ }
80
+ catch (error) {
81
+ this.started = false;
82
+ this.lifecycle.abort();
83
+ await this.stopFlows();
84
+ await this.options.sessions.closeAll();
85
+ throw error;
86
+ }
87
+ }
88
+ async reload() {
89
+ if (this.reloading)
90
+ return this.reloading;
91
+ const reloading = this.performReload();
92
+ this.reloading = reloading;
93
+ try {
94
+ await reloading;
95
+ }
96
+ finally {
97
+ if (this.reloading === reloading)
98
+ this.reloading = undefined;
99
+ }
100
+ }
101
+ async performReload() {
102
+ await this.settleStopping();
103
+ if (!this.options.reloadCatalog)
104
+ throw new Error("config reload is not configured");
105
+ this.emit({ type: "flows.reloading" });
106
+ let next;
107
+ try {
108
+ next = await this.options.reloadCatalog();
109
+ }
110
+ catch (error) {
111
+ this.emit({ type: "flows.reloaded", ok: false, error: messageOf(error) });
112
+ throw error;
113
+ }
114
+ const wasStarted = this.started;
115
+ await this.stopFlows();
116
+ this.catalog = next;
117
+ invalidateAllHooks();
118
+ if (wasStarted) {
119
+ const sessions = collectSessions(this.catalog);
120
+ if (sessions.size)
121
+ await this.options.sessions.ensure(sessions);
122
+ this.startFlows();
123
+ }
124
+ this.emit({ type: "flows.reloaded", ok: true });
125
+ }
126
+ async executeAction(spec, flow) {
127
+ const capability = String(spec.capability ?? "").trim();
128
+ if (!capability)
129
+ throw new Error("executeAction needs capability");
130
+ const action = {
131
+ capability,
132
+ config: spec.config ?? {},
133
+ session: spec.session,
134
+ hook: spec.hook
135
+ };
136
+ const sessions = collectActionSessions(action);
137
+ if (sessions.size)
138
+ await this.options.sessions.ensure(sessions);
139
+ await this.runAction(action, spec.label ?? `action:${capability}`, undefined, spec.session, this.lifecycle.signal, flow);
140
+ }
141
+ async runFlow(identifier) {
142
+ const command = this.catalog.find(identifier, "command");
143
+ const definition = command ?? this.catalog.find(identifier, "schedule");
144
+ if (!definition || (definition.kind !== "command" && definition.kind !== "schedule")) {
145
+ throw new Error("unknown flow: " + identifier);
146
+ }
147
+ if (definition.kind === "schedule" && !definition.enabled) {
148
+ throw new Error("schedule is disabled: " + definition.id);
149
+ }
150
+ if (definition.kind === "command")
151
+ this.invalidateFlowHooks(definition);
152
+ const action = {
153
+ ...definition.action,
154
+ session: definition.action.session ?? (definition.kind === "schedule" ? definition.session : undefined)
155
+ };
156
+ await this.completeFlow(definition.kind, definition.id, action.capability, () => this.executeAction({ ...action, label: `${definition.kind}:${definition.id}` }, { kind: definition.kind, id: definition.id }));
157
+ }
158
+ async completeFlow(kind, id, capability, operation) {
159
+ const startedAt = Date.now();
160
+ try {
161
+ await operation();
162
+ }
163
+ catch (error) {
164
+ this.emit({ type: "flow.finished", kind, id, capability, ok: false, durationMs: Date.now() - startedAt });
165
+ throw error;
166
+ }
167
+ this.emit({ type: "flow.finished", kind, id, capability, ok: true, durationMs: Date.now() - startedAt });
168
+ }
169
+ async startService(identifier) {
170
+ await this.settleStopping();
171
+ if (this.lifecycle.signal.aborted)
172
+ this.lifecycle = new AbortController();
173
+ const definition = this.catalog.find(identifier, "service");
174
+ if (!definition || definition.kind !== "service")
175
+ throw new Error("unknown service: " + identifier);
176
+ if (!definition.enabled)
177
+ throw new Error("service is disabled: " + definition.id);
178
+ if (this.controllers.has(`service:${definition.id}`))
179
+ return;
180
+ const gate = this.boundSession(definition);
181
+ const state = this.sessionState(gate);
182
+ if (gate && state !== undefined && state !== "connected") {
183
+ throw new SessionUnavailableError(gate, state, this.sessionReason(gate));
184
+ }
185
+ const sessions = new Set();
186
+ if (definition.session)
187
+ sessions.add(definition.session);
188
+ if (sessions.size)
189
+ await this.options.sessions.ensure(sessions);
190
+ this.launchService(definition);
191
+ }
192
+ async reconnectSession(name) {
193
+ await this.options.sessions.reconnectSession(name);
194
+ }
195
+ async stopService(identifier) {
196
+ const definition = this.catalog.find(identifier, "service");
197
+ if (!definition || definition.kind !== "service")
198
+ throw new Error("unknown service: " + identifier);
199
+ await this.stopFlow(`service:${definition.id}`);
200
+ }
201
+ async updateFlow(identifier, patch) {
202
+ const next = await updateFlowItem(this.catalog, identifier, patch);
203
+ if (!next)
204
+ return false;
205
+ this.catalog = next;
206
+ const item = next.find(identifier);
207
+ if (item)
208
+ this.emit({ type: "flow.updated", id: item.id, kind: item.kind });
209
+ return true;
210
+ }
211
+ async reloadFlow(identifier) {
212
+ const item = this.catalog.find(identifier);
213
+ if (!item)
214
+ return false;
215
+ this.invalidateFlowHooks(item);
216
+ if (item.kind === "schedule") {
217
+ this.scheduler.remove(item.id);
218
+ if (this.started && item.enabled)
219
+ this.startSchedule(item);
220
+ }
221
+ else if (item.kind === "trigger") {
222
+ await this.stopFlow(`trigger:${item.id}`);
223
+ if (this.started && item.enabled)
224
+ this.startTrigger(item);
225
+ }
226
+ else if (item.kind === "service") {
227
+ await this.stopFlow(`service:${item.id}`);
228
+ if (this.started && item.enabled && item.autoStart)
229
+ this.launchService(item);
230
+ }
231
+ this.emit({ type: "flow.reloaded", id: item.id, kind: item.kind });
232
+ return true;
233
+ }
234
+ invalidateFlowHooks(item) {
235
+ const references = item.kind === "trigger"
236
+ ? item.actions.map((action) => action.hook)
237
+ : "action" in item
238
+ ? [item.action.hook]
239
+ : [];
240
+ for (const reference of references)
241
+ invalidateHook(reference, this.catalog.path);
242
+ }
243
+ async stop() {
244
+ if (this.stopping)
245
+ return this.stopping;
246
+ const stopping = (async () => {
247
+ this.started = false;
248
+ this.lifecycle.abort();
249
+ await this.stopFlows();
250
+ await this.options.sessions.closeAll();
251
+ })();
252
+ this.stopping = stopping;
253
+ await stopping;
254
+ if (this.stopping === stopping)
255
+ this.stopping = undefined;
256
+ }
257
+ async settleStopping() {
258
+ const previousStop = this.stopping;
259
+ if (previousStop) {
260
+ await previousStop;
261
+ if (this.stopping === previousStop)
262
+ this.stopping = undefined;
263
+ }
264
+ }
265
+ async stopFlows() {
266
+ this.scheduler.stopAll();
267
+ const running = [...this.runs.entries()];
268
+ for (const controller of this.controllers.values())
269
+ controller.abort();
270
+ this.controllers.clear();
271
+ this.suspended.clear();
272
+ const tasks = [...this.tasks];
273
+ if (tasks.length) {
274
+ const graceMs = this.stopGraceMs();
275
+ const pending = await settleWithin(tasks, graceMs);
276
+ if (pending > 0) {
277
+ const leftovers = await stillRunning(running);
278
+ const names = leftovers.length ? leftovers.join(", ") : `${pending} unknown`;
279
+ this.options.logger.warn(`${pending} flow task(s) did not stop within ${graceMs}ms and keep running: ${names}`);
280
+ }
281
+ }
282
+ this.runs.clear();
283
+ }
284
+ async stopFlow(key) {
285
+ const controller = this.controllers.get(key);
286
+ const run = this.runs.get(key);
287
+ this.clearSuspension(key);
288
+ if (!controller && !run)
289
+ return;
290
+ controller?.abort();
291
+ if (run)
292
+ await settleWithin([run], this.stopGraceMs());
293
+ }
294
+ stopGraceMs() {
295
+ return this.options.stopGraceMs ?? Runtime.DEFAULT_STOP_GRACE_MS;
296
+ }
297
+ startFlows() {
298
+ for (const definition of this.catalog.enabled("trigger")) {
299
+ if (definition.kind !== "trigger")
300
+ continue;
301
+ if (this.flowBlocked(definition))
302
+ continue;
303
+ this.startTrigger(definition);
304
+ }
305
+ for (const definition of this.catalog.enabled("service")) {
306
+ if (definition.kind !== "service")
307
+ continue;
308
+ if (!definition.autoStart)
309
+ continue;
310
+ if (this.flowBlocked(definition))
311
+ continue;
312
+ this.launchService(definition);
313
+ }
314
+ for (const definition of this.catalog.enabled("schedule")) {
315
+ if (definition.kind !== "schedule")
316
+ continue;
317
+ if (this.flowBlocked(definition))
318
+ continue;
319
+ this.startSchedule(definition);
320
+ }
321
+ }
322
+ flowBlocked(definition) {
323
+ const session = this.boundSession(definition);
324
+ const state = this.sessionState(session);
325
+ if (!session || state === undefined || state === "connected")
326
+ return false;
327
+ this.recordSuspension(definition, session, this.sessionReason(session));
328
+ return true;
329
+ }
330
+ async handleSessionChange(change) {
331
+ this.emit({
332
+ type: "session.state",
333
+ name: change.name,
334
+ state: change.state,
335
+ reason: change.reason,
336
+ since: new Date().toISOString()
337
+ });
338
+ if (!this.started)
339
+ return;
340
+ if (change.state === "connected") {
341
+ this.resumeForSession(change.name);
342
+ return;
343
+ }
344
+ this.suspendForSession(change.name, change.reason);
345
+ }
346
+ suspendForSession(name, error) {
347
+ for (const definition of this.catalog.definitions()) {
348
+ if (definition.kind === "command")
349
+ continue;
350
+ const session = this.boundSession(definition);
351
+ if (session !== name)
352
+ continue;
353
+ const key = `${definition.kind}:${definition.id}`;
354
+ if (this.suspended.has(key))
355
+ continue;
356
+ if (definition.kind === "schedule") {
357
+ if (!this.scheduler.has(definition.id))
358
+ continue;
359
+ this.scheduler.remove(definition.id);
360
+ }
361
+ else {
362
+ const controller = this.controllers.get(key);
363
+ if (!controller)
364
+ continue;
365
+ controller.abort();
366
+ }
367
+ this.recordSuspension(definition, name, error);
368
+ this.emit({ type: "flow.suspended", id: definition.id, kind: definition.kind, session: name, error });
369
+ }
370
+ }
371
+ resumeForSession(name) {
372
+ for (const [key, suspension] of [...this.suspended.entries()]) {
373
+ if (suspension.session !== name)
374
+ continue;
375
+ this.suspended.delete(key);
376
+ const separator = key.indexOf(":");
377
+ const kind = key.slice(0, separator);
378
+ const definition = this.catalog.find(key.slice(separator + 1), kind);
379
+ if (!definition)
380
+ continue;
381
+ if (definition.kind !== "command" && !definition.enabled)
382
+ continue;
383
+ if (definition.kind === "schedule") {
384
+ this.startSchedule(definition);
385
+ }
386
+ else if (definition.kind === "service") {
387
+ this.launchService(definition);
388
+ }
389
+ else if (definition.kind === "trigger") {
390
+ this.startTrigger(definition);
391
+ }
392
+ else {
393
+ continue;
394
+ }
395
+ this.emit({ type: "flow.resumed", id: definition.id, kind: definition.kind, session: name });
396
+ }
397
+ }
398
+ recordSuspension(definition, session, error) {
399
+ this.suspended.set(`${definition.kind}:${definition.id}`, { kind: definition.kind, id: definition.id, session, error, since: Date.now() });
400
+ }
401
+ clearSuspension(key) {
402
+ this.suspended.delete(key);
403
+ }
404
+ startTrigger(definition) {
405
+ const key = `trigger:${definition.id}`;
406
+ const controller = new AbortController();
407
+ this.controllers.set(key, controller);
408
+ const task = this.runTrigger(definition, controller).catch((error) => {
409
+ if (!controller.signal.aborted)
410
+ this.options.logger.error("trigger stopped: " + definition.id, error);
411
+ }).finally(() => {
412
+ if (this.controllers.get(key) === controller)
413
+ this.controllers.delete(key);
414
+ if (this.runs.get(key) === task)
415
+ this.runs.delete(key);
416
+ });
417
+ this.runs.set(key, task);
418
+ this.track(task);
419
+ }
420
+ launchService(definition) {
421
+ const key = `service:${definition.id}`;
422
+ const controller = new AbortController();
423
+ this.controllers.set(key, controller);
424
+ this.serviceStartedAt.set(key, Date.now());
425
+ let failure;
426
+ const task = this.runService(definition, controller).catch((error) => {
427
+ failure = error;
428
+ if (!controller.signal.aborted)
429
+ this.options.logger.error("service stopped: " + definition.id, error);
430
+ }).finally(() => {
431
+ if (this.controllers.get(key) === controller)
432
+ this.controllers.delete(key);
433
+ if (this.runs.get(key) === task)
434
+ this.runs.delete(key);
435
+ const startedAt = this.serviceStartedAt.get(key);
436
+ this.serviceStartedAt.delete(key);
437
+ this.emit({
438
+ type: "service.stopped",
439
+ id: definition.id,
440
+ capability: definition.capability,
441
+ session: definition.session,
442
+ reason: failure !== undefined ? "error" : controller.signal.aborted ? "stop" : "finished",
443
+ error: failure instanceof Error ? failure.message : undefined,
444
+ durationMs: startedAt === undefined ? 0 : Date.now() - startedAt
445
+ });
446
+ });
447
+ this.runs.set(key, task);
448
+ this.track(task);
449
+ this.emit({
450
+ type: "service.started",
451
+ id: definition.id,
452
+ capability: definition.capability,
453
+ session: definition.session
454
+ });
455
+ }
456
+ startSchedule(definition) {
457
+ this.scheduler.add(definition, () => {
458
+ const task = this.completeFlow("schedule", definition.id, definition.action.capability, () => this.runAction(definition.action, `schedule:${definition.id}`, undefined, definition.session, this.scheduleSignal, { kind: "schedule", id: definition.id })).catch((error) => this.options.logger.error("schedule failed: " + definition.id, error));
459
+ this.track(task);
460
+ return task;
461
+ }, this.scheduleSignal, (item) => this.emit({
462
+ type: "schedule.fired",
463
+ id: item.id,
464
+ cron: item.cron,
465
+ intervalSeconds: item.intervalSeconds
466
+ }));
467
+ }
468
+ get scheduleSignal() {
469
+ return this.lifecycle.signal;
470
+ }
471
+ async runTrigger(definition, controller) {
472
+ const Constructor = this.options.registry.getTrigger(definition.capability);
473
+ let emitted = 0;
474
+ const context = {
475
+ id: definition.id,
476
+ capability: definition.capability,
477
+ config: definition.config,
478
+ session: definition.session,
479
+ signal: controller.signal,
480
+ sessions: this.options.sessions.access(definition.session),
481
+ control: this.options.registry.grantsControl("trigger", definition.capability) ? this : undefined,
482
+ logger: this.capabilityLogger("trigger", definition.capability),
483
+ maxRuns: definition.maxRuns,
484
+ emit: async (event) => {
485
+ if (controller.signal.aborted)
486
+ return;
487
+ emitted += 1;
488
+ const emission = {
489
+ source: { id: definition.id, capability: definition.capability },
490
+ event
491
+ };
492
+ const startedAt = Date.now();
493
+ let ok = true;
494
+ try {
495
+ for (const [index, action] of definition.actions.entries()) {
496
+ const outcome = await this.runAction(action, `trigger:${definition.id}:${index + 1}`, emission, action.session ?? definition.session, controller.signal, { kind: "trigger", id: definition.id }, true);
497
+ if (outcome.skipped)
498
+ ok = false;
499
+ }
500
+ }
501
+ catch (error) {
502
+ this.emit({
503
+ type: "flow.finished",
504
+ kind: "trigger",
505
+ id: definition.id,
506
+ capability: definition.capability,
507
+ ok: false,
508
+ durationMs: Date.now() - startedAt
509
+ });
510
+ throw error;
511
+ }
512
+ this.emit({
513
+ type: "flow.finished",
514
+ kind: "trigger",
515
+ id: definition.id,
516
+ capability: definition.capability,
517
+ ok,
518
+ durationMs: Date.now() - startedAt
519
+ });
520
+ if (definition.maxRuns && emitted >= definition.maxRuns)
521
+ controller.abort();
522
+ }
523
+ };
524
+ const trigger = new Constructor(context);
525
+ await trigger.run();
526
+ }
527
+ async runService(definition, controller) {
528
+ const Constructor = this.options.registry.getService(definition.capability);
529
+ const context = {
530
+ id: definition.id,
531
+ capability: definition.capability,
532
+ config: definition.config,
533
+ session: definition.session,
534
+ signal: controller.signal,
535
+ sessions: this.options.sessions.access(definition.session),
536
+ control: this.options.registry.grantsControl("service", definition.capability) ? this : undefined,
537
+ logger: this.capabilityLogger("service", definition.capability)
538
+ };
539
+ await new Constructor(context).run();
540
+ }
541
+ async runAction(specification, id, emission, parentSession, signal, flow, soft = false) {
542
+ const capability = specification.capability;
543
+ const session = specification.session ?? parentSession;
544
+ const state = this.sessionState(session);
545
+ if (session && state !== undefined && state !== "connected") {
546
+ const error = new SessionUnavailableError(session, state, this.sessionReason(session));
547
+ if (soft) {
548
+ this.emit({
549
+ type: "action.finished",
550
+ id,
551
+ capability,
552
+ session,
553
+ flow,
554
+ ok: false,
555
+ skipped: false,
556
+ durationMs: 0,
557
+ error: error.message
558
+ });
559
+ return { skipped: true };
560
+ }
561
+ throw error;
562
+ }
563
+ const Constructor = this.options.registry.getAction(capability);
564
+ const hook = await loadHook(specification.hook, this.catalog.path);
565
+ const context = {
566
+ id,
567
+ config: specification.config,
568
+ session,
569
+ signal,
570
+ sessions: this.options.sessions.access(session),
571
+ control: this.options.registry.grantsControl("action", capability) ? this : undefined,
572
+ logger: this.capabilityLogger("action", capability),
573
+ emission,
574
+ hook,
575
+ outcome: undefined,
576
+ spawn: (task) => this.track(task.then(() => undefined))
577
+ };
578
+ const startedAt = Date.now();
579
+ this.activeActionEntries.set(id, { id, capability, session, flow, startedAt });
580
+ this.emit({
581
+ type: "action.started",
582
+ id,
583
+ capability,
584
+ session,
585
+ flow,
586
+ hook: specification.hook,
587
+ config: exportable(specification.config)
588
+ });
589
+ let failure;
590
+ try {
591
+ await new Constructor(context).execute();
592
+ }
593
+ catch (error) {
594
+ failure = error;
595
+ throw error;
596
+ }
597
+ finally {
598
+ this.activeActionEntries.delete(id);
599
+ this.emit({
600
+ type: "action.finished",
601
+ id,
602
+ capability,
603
+ session,
604
+ flow,
605
+ ok: failure === undefined,
606
+ skipped: context.outcome?.skipped === true,
607
+ durationMs: Date.now() - startedAt,
608
+ error: failure instanceof Error ? failure.message : failure === undefined ? undefined : String(failure),
609
+ effectiveConfig: exportable(context.outcome?.effectiveConfig)
610
+ });
611
+ }
612
+ return { skipped: false };
613
+ }
614
+ capabilityLogger(kind, capability) {
615
+ const scope = this.options.registry.scopeOf(kind, capability) ?? capability;
616
+ return this.options.logger.child(scope);
617
+ }
618
+ flowSnapshot(definition) {
619
+ const base = {
620
+ kind: definition.kind,
621
+ id: definition.id,
622
+ capability: "capability" in definition ? definition.capability : definition.action.capability,
623
+ enabled: definition.kind === "command" ? true : definition.enabled,
624
+ active: definition.kind === "service" || definition.kind === "trigger"
625
+ ? this.controllers.has(`${definition.kind}:${definition.id}`)
626
+ : false,
627
+ session: "session" in definition ? definition.session : undefined,
628
+ logFile: "logFile" in definition ? definition.logFile : false,
629
+ suspended: this.suspensionOf(definition)
630
+ };
631
+ if (definition.kind === "trigger") {
632
+ return {
633
+ ...base,
634
+ maxRuns: definition.maxRuns,
635
+ config: copyOf(definition.config),
636
+ actions: definition.actions.map((action) => actionView(action))
637
+ };
638
+ }
639
+ if (definition.kind === "service") {
640
+ return {
641
+ ...base,
642
+ autoStart: definition.autoStart,
643
+ config: copyOf(definition.config),
644
+ startedAt: this.serviceStartedAt.get(`service:${definition.id}`) === undefined
645
+ ? undefined
646
+ : new Date(this.serviceStartedAt.get(`service:${definition.id}`)).toISOString()
647
+ };
648
+ }
649
+ return {
650
+ ...base,
651
+ title: definition.kind === "command" ? definition.title : undefined,
652
+ symbol: definition.kind === "command" ? definition.symbol : undefined,
653
+ cron: definition.kind === "schedule" ? definition.cron : undefined,
654
+ intervalSeconds: definition.kind === "schedule" ? definition.intervalSeconds : undefined,
655
+ actionSession: definition.action.session,
656
+ hook: definition.action.hook,
657
+ config: copyOf(definition.action.config)
658
+ };
659
+ }
660
+ suspensionOf(definition) {
661
+ const suspension = this.suspended.get(`${definition.kind}:${definition.id}`);
662
+ if (!suspension)
663
+ return undefined;
664
+ return {
665
+ since: new Date(suspension.since).toISOString(),
666
+ reason: "session",
667
+ session: suspension.session,
668
+ error: suspension.error
669
+ };
670
+ }
671
+ sessionSnapshots() {
672
+ const states = this.options.sessions.states();
673
+ return states.map((info) => ({
674
+ name: info.name,
675
+ state: info.state,
676
+ since: new Date(info.since).toISOString(),
677
+ reason: info.reason,
678
+ attempts: info.attempts,
679
+ flows: [...this.suspended.values()]
680
+ .filter((item) => item.session === info.name)
681
+ .map((item) => ({ kind: item.kind, id: item.id }))
682
+ }));
683
+ }
684
+ sessionState(name) {
685
+ if (!name)
686
+ return undefined;
687
+ return this.options.sessions.state(name);
688
+ }
689
+ sessionReason(name) {
690
+ return this.options.sessions.states().find((info) => info.name === name)?.reason;
691
+ }
692
+ boundSession(definition) {
693
+ if (definition.kind === "command")
694
+ return definition.action.session;
695
+ if (definition.kind === "schedule")
696
+ return definition.action.session ?? definition.session;
697
+ return definition.session;
698
+ }
699
+ logScopes() {
700
+ const directory = this.options.logger.logDirectory();
701
+ if (!directory)
702
+ return [];
703
+ const scopes = ["paperkite", ...this.options.logger.registeredScopes()];
704
+ return scopes.map((scope) => ({ scope, path: join(directory, basename(scope) + ".log") }));
705
+ }
706
+ emit(event) {
707
+ const withAt = { ...event, at: new Date().toISOString() };
708
+ for (const listener of [...this.listeners]) {
709
+ try {
710
+ listener(withAt);
711
+ }
712
+ catch (error) {
713
+ this.options.logger.error("runtime event listener failed", error);
714
+ }
715
+ }
716
+ }
717
+ track(task) {
718
+ const observed = task.catch((error) => {
719
+ this.options.logger.error("background task failed", error);
720
+ });
721
+ this.tasks.add(observed);
722
+ void observed.then(() => this.tasks.delete(observed));
723
+ }
724
+ }
725
+ function collectSessions(catalog) {
726
+ const names = new Set();
727
+ for (const definition of catalog.enabled()) {
728
+ if ("session" in definition && definition.session)
729
+ names.add(definition.session);
730
+ if (definition.kind === "trigger") {
731
+ for (const action of definition.actions)
732
+ collectActionSessions(action, names);
733
+ }
734
+ else if (definition.kind === "command" || definition.kind === "schedule") {
735
+ collectActionSessions(definition.action, names);
736
+ }
737
+ }
738
+ return names;
739
+ }
740
+ function collectActionSessions(action, names = new Set()) {
741
+ if (action.session)
742
+ names.add(action.session);
743
+ return names;
744
+ }
745
+ function messageOf(error) {
746
+ return error instanceof Error ? error.message : String(error);
747
+ }
748
+ async function settleWithin(tasks, milliseconds) {
749
+ let pending = tasks.length;
750
+ const observed = tasks.map((task) => task.then(() => {
751
+ pending -= 1;
752
+ }, () => {
753
+ pending -= 1;
754
+ }));
755
+ let timer;
756
+ const timeout = new Promise((resolve) => {
757
+ timer = setTimeout(resolve, milliseconds);
758
+ });
759
+ try {
760
+ await Promise.race([Promise.allSettled(observed), timeout]);
761
+ }
762
+ finally {
763
+ if (timer)
764
+ clearTimeout(timer);
765
+ }
766
+ return pending;
767
+ }
768
+ async function stillRunning(entries) {
769
+ const keys = [];
770
+ for (const [key, task] of entries) {
771
+ const settled = await Promise.race([
772
+ task.then(() => true, () => true),
773
+ new Promise((resolve) => queueMicrotask(() => resolve(false)))
774
+ ]);
775
+ if (!settled)
776
+ keys.push(key);
777
+ }
778
+ return keys;
779
+ }
780
+ function actionView(action) {
781
+ return { capability: action.capability, session: action.session, config: copyOf(action.config), hook: action.hook };
782
+ }
783
+ function exportable(value) {
784
+ if (value === undefined)
785
+ return undefined;
786
+ try {
787
+ const text = JSON.stringify(value);
788
+ if (!text || text.length > 4096)
789
+ return undefined;
790
+ return JSON.parse(text);
791
+ }
792
+ catch {
793
+ return undefined;
794
+ }
795
+ }
796
+ function copyOf(value) {
797
+ return structuredClone(value);
798
+ }
799
+ //# sourceMappingURL=runtime.js.map