scrapme-flow 0.0.0-stage → 0.2.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/dist/src/commands/commands.models.d.ts +41 -0
  4. package/dist/src/commands/commands.models.js +2 -0
  5. package/dist/src/commands/commands.models.js.map +1 -0
  6. package/dist/src/commands/save-document.command.d.ts +9 -0
  7. package/dist/src/commands/save-document.command.js +33 -0
  8. package/dist/src/commands/save-document.command.js.map +1 -0
  9. package/dist/src/commands/scrape-page.command.d.ts +20 -0
  10. package/dist/src/commands/scrape-page.command.js +93 -0
  11. package/dist/src/commands/scrape-page.command.js.map +1 -0
  12. package/dist/src/commands/send-mail.command.d.ts +11 -0
  13. package/dist/src/commands/send-mail.command.js +39 -0
  14. package/dist/src/commands/send-mail.command.js.map +1 -0
  15. package/dist/src/config/config-builder.d.ts +24 -0
  16. package/dist/src/config/config-builder.js +129 -0
  17. package/dist/src/config/config-builder.js.map +1 -0
  18. package/dist/src/config/config-loader.d.ts +10 -0
  19. package/dist/src/config/config-loader.js +79 -0
  20. package/dist/src/config/config-loader.js.map +1 -0
  21. package/dist/src/config/config.models.d.ts +144 -0
  22. package/dist/src/config/config.models.js +2 -0
  23. package/dist/src/config/config.models.js.map +1 -0
  24. package/dist/src/config/config.schema.d.ts +75 -0
  25. package/dist/src/config/config.schema.js +87 -0
  26. package/dist/src/config/config.schema.js.map +1 -0
  27. package/dist/src/config/dsn-check.d.ts +5 -0
  28. package/dist/src/config/dsn-check.js +12 -0
  29. package/dist/src/config/dsn-check.js.map +1 -0
  30. package/dist/src/di/container.d.ts +13 -0
  31. package/dist/src/di/container.js +67 -0
  32. package/dist/src/di/container.js.map +1 -0
  33. package/dist/src/di/types.d.ts +27 -0
  34. package/dist/src/di/types.js +28 -0
  35. package/dist/src/di/types.js.map +1 -0
  36. package/dist/src/flow/command-executor.d.ts +18 -0
  37. package/dist/src/flow/command-executor.js +87 -0
  38. package/dist/src/flow/command-executor.js.map +1 -0
  39. package/dist/src/flow/command-resolver.d.ts +11 -0
  40. package/dist/src/flow/command-resolver.js +35 -0
  41. package/dist/src/flow/command-resolver.js.map +1 -0
  42. package/dist/src/flow/flow-catalog.d.ts +12 -0
  43. package/dist/src/flow/flow-catalog.js +31 -0
  44. package/dist/src/flow/flow-catalog.js.map +1 -0
  45. package/dist/src/flow/flow-errors.d.ts +66 -0
  46. package/dist/src/flow/flow-errors.js +119 -0
  47. package/dist/src/flow/flow-errors.js.map +1 -0
  48. package/dist/src/flow/flow-job-processor.d.ts +33 -0
  49. package/dist/src/flow/flow-job-processor.js +208 -0
  50. package/dist/src/flow/flow-job-processor.js.map +1 -0
  51. package/dist/src/flow/flow-params.d.ts +9 -0
  52. package/dist/src/flow/flow-params.js +52 -0
  53. package/dist/src/flow/flow-params.js.map +1 -0
  54. package/dist/src/flow/flow-runner.d.ts +36 -0
  55. package/dist/src/flow/flow-runner.js +209 -0
  56. package/dist/src/flow/flow-runner.js.map +1 -0
  57. package/dist/src/flow/flow-scheduler.d.ts +24 -0
  58. package/dist/src/flow/flow-scheduler.js +96 -0
  59. package/dist/src/flow/flow-scheduler.js.map +1 -0
  60. package/dist/src/flow/flow-validator.d.ts +29 -0
  61. package/dist/src/flow/flow-validator.js +141 -0
  62. package/dist/src/flow/flow-validator.js.map +1 -0
  63. package/dist/src/flow/flow.d.ts +32 -0
  64. package/dist/src/flow/flow.js +50 -0
  65. package/dist/src/flow/flow.js.map +1 -0
  66. package/dist/src/flow/flow.models.d.ts +288 -0
  67. package/dist/src/flow/flow.models.js +38 -0
  68. package/dist/src/flow/flow.models.js.map +1 -0
  69. package/dist/src/flow/retry-policy.d.ts +9 -0
  70. package/dist/src/flow/retry-policy.js +15 -0
  71. package/dist/src/flow/retry-policy.js.map +1 -0
  72. package/dist/src/flow/state-path.d.ts +21 -0
  73. package/dist/src/flow/state-path.js +37 -0
  74. package/dist/src/flow/state-path.js.map +1 -0
  75. package/dist/src/flow/step-input-builder.d.ts +15 -0
  76. package/dist/src/flow/step-input-builder.js +63 -0
  77. package/dist/src/flow/step-input-builder.js.map +1 -0
  78. package/dist/src/flows/scrape.flow.d.ts +6 -0
  79. package/dist/src/flows/scrape.flow.js +31 -0
  80. package/dist/src/flows/scrape.flow.js.map +1 -0
  81. package/dist/src/index.d.ts +57 -0
  82. package/dist/src/index.js +50 -0
  83. package/dist/src/index.js.map +1 -0
  84. package/dist/src/logging/logger.d.ts +17 -0
  85. package/dist/src/logging/logger.js +55 -0
  86. package/dist/src/logging/logger.js.map +1 -0
  87. package/dist/src/logging/logger.models.d.ts +25 -0
  88. package/dist/src/logging/logger.models.js +7 -0
  89. package/dist/src/logging/logger.models.js.map +1 -0
  90. package/dist/src/mail/mail.models.d.ts +15 -0
  91. package/dist/src/mail/mail.models.js +2 -0
  92. package/dist/src/mail/mail.models.js.map +1 -0
  93. package/dist/src/mail/smtp-mailer.d.ts +13 -0
  94. package/dist/src/mail/smtp-mailer.js +70 -0
  95. package/dist/src/mail/smtp-mailer.js.map +1 -0
  96. package/dist/src/mail/unconfigured-mailer.d.ts +6 -0
  97. package/dist/src/mail/unconfigured-mailer.js +20 -0
  98. package/dist/src/mail/unconfigured-mailer.js.map +1 -0
  99. package/dist/src/models.d.ts +11 -0
  100. package/dist/src/models.js +10 -0
  101. package/dist/src/models.js.map +1 -0
  102. package/dist/src/monitor/monitor-api.d.ts +18 -0
  103. package/dist/src/monitor/monitor-api.js +62 -0
  104. package/dist/src/monitor/monitor-api.js.map +1 -0
  105. package/dist/src/monitor/monitor-server.d.ts +33 -0
  106. package/dist/src/monitor/monitor-server.js +142 -0
  107. package/dist/src/monitor/monitor-server.js.map +1 -0
  108. package/dist/src/monitor/monitor.models.d.ts +27 -0
  109. package/dist/src/monitor/monitor.models.js +2 -0
  110. package/dist/src/monitor/monitor.models.js.map +1 -0
  111. package/dist/src/queue/pg-boss-flow-queue.d.ts +22 -0
  112. package/dist/src/queue/pg-boss-flow-queue.js +102 -0
  113. package/dist/src/queue/pg-boss-flow-queue.js.map +1 -0
  114. package/dist/src/queue/queue.models.d.ts +38 -0
  115. package/dist/src/queue/queue.models.js +2 -0
  116. package/dist/src/queue/queue.models.js.map +1 -0
  117. package/dist/src/reporting/lighty-error-reporter.d.ts +24 -0
  118. package/dist/src/reporting/lighty-error-reporter.js +96 -0
  119. package/dist/src/reporting/lighty-error-reporter.js.map +1 -0
  120. package/dist/src/reporting/log-error-reporter.d.ts +12 -0
  121. package/dist/src/reporting/log-error-reporter.js +35 -0
  122. package/dist/src/reporting/log-error-reporter.js.map +1 -0
  123. package/dist/src/reporting/reporting.models.d.ts +27 -0
  124. package/dist/src/reporting/reporting.models.js +2 -0
  125. package/dist/src/reporting/reporting.models.js.map +1 -0
  126. package/dist/src/scrapme.d.ts +61 -0
  127. package/dist/src/scrapme.js +133 -0
  128. package/dist/src/scrapme.js.map +1 -0
  129. package/dist/src/scrapme.models.d.ts +18 -0
  130. package/dist/src/scrapme.models.js +2 -0
  131. package/dist/src/scrapme.models.js.map +1 -0
  132. package/dist/src/storage/couch-connection.d.ts +19 -0
  133. package/dist/src/storage/couch-connection.js +67 -0
  134. package/dist/src/storage/couch-connection.js.map +1 -0
  135. package/dist/src/storage/couch-database.d.ts +44 -0
  136. package/dist/src/storage/couch-database.js +118 -0
  137. package/dist/src/storage/couch-database.js.map +1 -0
  138. package/dist/src/storage/couch-document-store.d.ts +14 -0
  139. package/dist/src/storage/couch-document-store.js +36 -0
  140. package/dist/src/storage/couch-document-store.js.map +1 -0
  141. package/dist/src/storage/couch-error.d.ts +12 -0
  142. package/dist/src/storage/couch-error.js +19 -0
  143. package/dist/src/storage/couch-error.js.map +1 -0
  144. package/dist/src/storage/couch-flow-run-store.d.ts +24 -0
  145. package/dist/src/storage/couch-flow-run-store.js +66 -0
  146. package/dist/src/storage/couch-flow-run-store.js.map +1 -0
  147. package/dist/src/storage/couch-run-store.d.ts +15 -0
  148. package/dist/src/storage/couch-run-store.js +52 -0
  149. package/dist/src/storage/couch-run-store.js.map +1 -0
  150. package/dist/src/storage/run-doc-ids.d.ts +9 -0
  151. package/dist/src/storage/run-doc-ids.js +15 -0
  152. package/dist/src/storage/run-doc-ids.js.map +1 -0
  153. package/dist/src/storage/storage.models.d.ts +95 -0
  154. package/dist/src/storage/storage.models.js +5 -0
  155. package/dist/src/storage/storage.models.js.map +1 -0
  156. package/dist/src/storage/unconfigured-document-store.d.ts +8 -0
  157. package/dist/src/storage/unconfigured-document-store.js +26 -0
  158. package/dist/src/storage/unconfigured-document-store.js.map +1 -0
  159. package/dist/src/util/preview.d.ts +10 -0
  160. package/dist/src/util/preview.js +44 -0
  161. package/dist/src/util/preview.js.map +1 -0
  162. package/dist/src/util/text.d.ts +8 -0
  163. package/dist/src/util/text.js +16 -0
  164. package/dist/src/util/text.js.map +1 -0
  165. package/dist/src/util/timing.d.ts +10 -0
  166. package/dist/src/util/timing.js +59 -0
  167. package/dist/src/util/timing.js.map +1 -0
  168. package/dist/src/util/url.d.ts +5 -0
  169. package/dist/src/util/url.js +11 -0
  170. package/dist/src/util/url.js.map +1 -0
  171. package/package.json +75 -4
@@ -0,0 +1,35 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { inject, injectable } from 'inversify';
11
+ import { TYPES } from '../di/types.js';
12
+ /** Used when LIGHTY_DSN is empty: the final error of a run goes to the log (and CouchDB, via FlowRunStore). */
13
+ let LogErrorReporter = class LogErrorReporter {
14
+ logger;
15
+ constructor(logger) {
16
+ this.logger = logger.child({ component: 'error-report' });
17
+ }
18
+ report(error, context) {
19
+ this.logger.error('flow step failed (set LIGHTY_DSN to send it to LightyErrors)', { error, ...context });
20
+ return '';
21
+ }
22
+ log(_message, _data, _level) {
23
+ // the regular logger already printed it
24
+ }
25
+ async flush() {
26
+ return true;
27
+ }
28
+ async close() { }
29
+ };
30
+ LogErrorReporter = __decorate([
31
+ injectable(),
32
+ __param(0, inject(TYPES.Logger))
33
+ ], LogErrorReporter);
34
+ export { LogErrorReporter };
35
+ //# sourceMappingURL=log-error-reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-error-reporter.js","sourceRoot":"","sources":["../../../src/reporting/log-error-reporter.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAKvC,+GAA+G;AAExG,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACV,MAAM,CAAS;IAEhC,YAAyC,MAAc;QACrD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;IAC5D,CAAC;IAEM,MAAM,CAAC,KAAc,EAAE,OAA2B;QACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8DAA8D,EAAE,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;QACzG,OAAO,EAAE,CAAC;IACZ,CAAC;IAEM,GAAG,CAAC,QAAgB,EAAE,KAA+B,EAAE,MAAkB;QAC9E,wCAAwC;IAC1C,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,KAAK,CAAC,KAAK,KAAmB,CAAC;CACvC,CAAA;AArBY,gBAAgB;IAD5B,UAAU,EAAE;IAIS,WAAA,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;GAH7B,gBAAgB,CAqB5B"}
@@ -0,0 +1,27 @@
1
+ import type { LineLevel } from '../logging/logger.models.js';
2
+ /** What an error report is about (sent as tags + context to LightyErrors). */
3
+ export interface ErrorReportContext {
4
+ flow: string;
5
+ step: string;
6
+ command: string;
7
+ runId: string;
8
+ attempt: number;
9
+ maxAttempts: number;
10
+ /** Shortened step input. */
11
+ input?: unknown;
12
+ tags?: Record<string, string>;
13
+ extra?: Record<string, unknown>;
14
+ }
15
+ /**
16
+ * Error tracking for flows. FlowRunner reports every step that finally failed (once per run) and feeds
17
+ * the log of the run (step started/failed/retrying) so the report shows what led to the error.
18
+ */
19
+ export interface ErrorReporter {
20
+ /** Returns the event id ('' when not reported). Never throws. */
21
+ report(error: unknown, context: ErrorReportContext): string;
22
+ /** A line of the run's log, sent with the next report (breadcrumbs). */
23
+ log(message: string, data?: Record<string, unknown>, level?: LineLevel): void;
24
+ /** Send what is waiting; true when everything was delivered. */
25
+ flush(timeoutMs?: number): Promise<boolean>;
26
+ close(timeoutMs?: number): Promise<void>;
27
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reporting.models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporting.models.js","sourceRoot":"","sources":["../../../src/reporting/reporting.models.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
1
+ import 'reflect-metadata';
2
+ import type { Container } from 'inversify';
3
+ import type { ScrapmeConfig } from './config/config.models.js';
4
+ import type { Flow } from './flow/flow.js';
5
+ import { type ScheduleOptions, type WaitOptions } from './flow/flow.models.js';
6
+ import type { FlowRunDoc } from './storage/storage.models.js';
7
+ import type { ScrapmeOptions, StartOptions } from './scrapme.models.js';
8
+ /** waitFor gave up (the run goes on). */
9
+ export declare class RunWaitTimeoutError extends Error {
10
+ readonly runId: string;
11
+ readonly lastStatus: string | null;
12
+ constructor(runId: string, lastStatus: string | null);
13
+ }
14
+ /**
15
+ * Runs flows on pg-boss queues: every run is a job, retried by pg-boss; its progress is in CouchDB (the separate
16
+ * scrapme monitor app shows it).
17
+ *
18
+ * const scrapme = Scrapme.create({
19
+ * container: appContainer,
20
+ * postgres: { connectionString: 'postgres://…' }, couch: { url, user, password },
21
+ * scrapAnything: { url, apiSecret },
22
+ * });
23
+ * await scrapme.start(); // connects pg-boss
24
+ * const runId = await scrapme.flow(titlesFlow, { url: 'https://…' }); // queues a run, starts this flow's worker
25
+ * const run = await scrapme.waitFor(runId); // optional: wait for the end
26
+ * await scrapme.stop();
27
+ */
28
+ export declare class Scrapme {
29
+ /** scrapme's own container (child of your app's container). */
30
+ readonly container: Container;
31
+ readonly config: ScrapmeConfig;
32
+ private static readonly DEFAULT_WAIT_MS;
33
+ private static readonly DEFAULT_POLL_MS;
34
+ private started;
35
+ private worker;
36
+ /** Flows this process runs, by name (their workers are started on first use). */
37
+ private readonly workers;
38
+ private constructor();
39
+ /** Checks the options (ConfigError); connects nothing yet. */
40
+ static create(options: ScrapmeOptions): Scrapme;
41
+ /** Connect pg-boss (it creates its tables on the first start). */
42
+ start(options?: StartOptions): Promise<void>;
43
+ /**
44
+ * Queue a run of this flow with this input; returns its run id at once. The flow runs in a worker of any process
45
+ * that runs this flow — this one too (unless started with `worker: false`). Throws before anything is queued for
46
+ * an input that doesn't fit the flow's params (FlowInputError).
47
+ */
48
+ flow(flow: Flow, input?: Readonly<Record<string, unknown>>, options?: ScheduleOptions): Promise<string>;
49
+ /** Run this flow's queued runs in this process (flow() does it for you; use it in a worker-only process). */
50
+ work(flow: Flow): Promise<void>;
51
+ /** The run document: status, every step with its attempts, output and error, every flow attempt. */
52
+ getRun(runId: string): Promise<FlowRunDoc | null>;
53
+ /** Newest first. */
54
+ listRuns(limit?: number): Promise<FlowRunDoc[]>;
55
+ /** Wait until the run succeeded or failed for good (polls CouchDB). Throws RunWaitTimeoutError. */
56
+ waitFor(runId: string, options?: WaitOptions): Promise<FlowRunDoc>;
57
+ /** Stop the workers (running flows get the shutdown timeout) and the queue connection. */
58
+ stop(): Promise<void>;
59
+ private requireStarted;
60
+ private store;
61
+ }
@@ -0,0 +1,133 @@
1
+ import 'reflect-metadata';
2
+ import { ConfigBuilder } from './config/config-builder.js';
3
+ import { ContainerBuilder } from './di/container.js';
4
+ import { TYPES } from './di/types.js';
5
+ import { FlowDefinitionError } from './flow/flow-errors.js';
6
+ import { FINAL_RUN_STATUSES } from './flow/flow.models.js';
7
+ import { Timing } from './util/timing.js';
8
+ /** waitFor gave up (the run goes on). */
9
+ export class RunWaitTimeoutError extends Error {
10
+ runId;
11
+ lastStatus;
12
+ constructor(runId, lastStatus) {
13
+ super(`run ${runId} not finished in time (last status: ${lastStatus ?? 'unknown'})`);
14
+ this.runId = runId;
15
+ this.lastStatus = lastStatus;
16
+ this.name = 'RunWaitTimeoutError';
17
+ }
18
+ }
19
+ /**
20
+ * Runs flows on pg-boss queues: every run is a job, retried by pg-boss; its progress is in CouchDB (the separate
21
+ * scrapme monitor app shows it).
22
+ *
23
+ * const scrapme = Scrapme.create({
24
+ * container: appContainer,
25
+ * postgres: { connectionString: 'postgres://…' }, couch: { url, user, password },
26
+ * scrapAnything: { url, apiSecret },
27
+ * });
28
+ * await scrapme.start(); // connects pg-boss
29
+ * const runId = await scrapme.flow(titlesFlow, { url: 'https://…' }); // queues a run, starts this flow's worker
30
+ * const run = await scrapme.waitFor(runId); // optional: wait for the end
31
+ * await scrapme.stop();
32
+ */
33
+ export class Scrapme {
34
+ container;
35
+ config;
36
+ static DEFAULT_WAIT_MS = 600_000;
37
+ static DEFAULT_POLL_MS = 1_000;
38
+ started = false;
39
+ worker = true;
40
+ /** Flows this process runs, by name (their workers are started on first use). */
41
+ workers = new Map();
42
+ constructor(
43
+ /** scrapme's own container (child of your app's container). */
44
+ container, config) {
45
+ this.container = container;
46
+ this.config = config;
47
+ }
48
+ /** Checks the options (ConfigError); connects nothing yet. */
49
+ static create(options) {
50
+ const { container, ...services } = options;
51
+ const config = ConfigBuilder.build(services);
52
+ return new Scrapme(ContainerBuilder.build(config, container), config);
53
+ }
54
+ /** Connect pg-boss (it creates its tables on the first start). */
55
+ async start(options = {}) {
56
+ if (this.started)
57
+ return;
58
+ this.worker = options.worker ?? true;
59
+ await this.container.get(TYPES.FlowQueue).start();
60
+ this.started = true;
61
+ }
62
+ /**
63
+ * Queue a run of this flow with this input; returns its run id at once. The flow runs in a worker of any process
64
+ * that runs this flow — this one too (unless started with `worker: false`). Throws before anything is queued for
65
+ * an input that doesn't fit the flow's params (FlowInputError).
66
+ */
67
+ async flow(flow, input = {}, options) {
68
+ this.requireStarted();
69
+ if (this.worker)
70
+ await this.work(flow);
71
+ return this.container.get(TYPES.FlowScheduler).schedule(flow, input, options);
72
+ }
73
+ /** Run this flow's queued runs in this process (flow() does it for you; use it in a worker-only process). */
74
+ async work(flow) {
75
+ this.requireStarted();
76
+ const known = this.workers.get(flow.name);
77
+ if (known) {
78
+ if (known.flow !== flow)
79
+ throw new FlowDefinitionError('another flow with this name runs here already (names must be unique)', flow.name);
80
+ return known.ready;
81
+ }
82
+ const processor = this.container.get(TYPES.FlowJobProcessor);
83
+ const queueName = this.container.get(TYPES.FlowScheduler).queueName(flow);
84
+ const ready = this.container.get(TYPES.FlowQueue).work(queueName, this.config.queue.concurrency, (job) => processor.process(flow, job));
85
+ this.workers.set(flow.name, { flow, ready });
86
+ try {
87
+ await ready;
88
+ }
89
+ catch (error) {
90
+ this.workers.delete(flow.name);
91
+ throw error;
92
+ }
93
+ }
94
+ /** The run document: status, every step with its attempts, output and error, every flow attempt. */
95
+ async getRun(runId) {
96
+ return this.store().get(runId);
97
+ }
98
+ /** Newest first. */
99
+ async listRuns(limit = 50) {
100
+ return this.store().list(limit);
101
+ }
102
+ /** Wait until the run succeeded or failed for good (polls CouchDB). Throws RunWaitTimeoutError. */
103
+ async waitFor(runId, options = {}) {
104
+ const deadline = Date.now() + (options.timeoutMs ?? Scrapme.DEFAULT_WAIT_MS);
105
+ let last = null;
106
+ for (;;) {
107
+ last = await this.getRun(runId);
108
+ if (last && FINAL_RUN_STATUSES.includes(last.status))
109
+ return last;
110
+ const left = deadline - Date.now();
111
+ if (left <= 0)
112
+ throw new RunWaitTimeoutError(runId, last?.status ?? null);
113
+ await Timing.sleep(Math.min(options.pollIntervalMs ?? Scrapme.DEFAULT_POLL_MS, left));
114
+ }
115
+ }
116
+ /** Stop the workers (running flows get the shutdown timeout) and the queue connection. */
117
+ async stop() {
118
+ if (!this.started)
119
+ return;
120
+ this.started = false;
121
+ this.workers.clear();
122
+ await this.container.get(TYPES.FlowQueue).stop();
123
+ await this.container.get(TYPES.ErrorReporter).close();
124
+ }
125
+ requireStarted() {
126
+ if (!this.started)
127
+ throw new Error('scrapme is not started - call await scrapme.start() first');
128
+ }
129
+ store() {
130
+ return this.container.get(TYPES.RunStore);
131
+ }
132
+ }
133
+ //# sourceMappingURL=scrapme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapme.js","sourceRoot":"","sources":["../../src/scrapme.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Container } from 'inversify';
2
+ import type { ServiceOptions } from './config/config.models.js';
3
+ /**
4
+ * What Scrapme.create needs: the app's container and the services scrapme connects to (postgres and couch required;
5
+ * scrapAnything, mail, lighty when used — see ServiceOptions). scrapme reads no environment variables:
6
+ * the app passes everything. Flows are not registered here — pass them to scrapme.flow().
7
+ */
8
+ export interface ScrapmeOptions extends ServiceOptions {
9
+ /**
10
+ * Your app's container. Commands are created by scrapme (no bindings needed) and their `@inject(...)`s are
11
+ * looked up here, so they can use your services. Use the same `inversify` as scrapme (it is a peer dependency).
12
+ */
13
+ container?: Container;
14
+ }
15
+ export interface StartOptions {
16
+ /** Run the flows this process schedules (default true). false = only schedule; workers run elsewhere (scrapme.work). */
17
+ worker?: boolean;
18
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scrapme.models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapme.models.js","sourceRoot":"","sources":["../../src/scrapme.models.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import type { CouchConfig } from '../config/config.models.js';
2
+ import type { Logger } from '../logging/logger.js';
3
+ import { CouchDatabase } from './couch-database.js';
4
+ import type { CouchDatabases } from './storage.models.js';
5
+ /** Shared HTTP connection to CouchDB; hands out CouchDatabase objects (one per name, cached). */
6
+ export declare class CouchConnection implements CouchDatabases {
7
+ private readonly couch;
8
+ private static readonly PING_TIMEOUT_MS;
9
+ private static readonly UP_PATH;
10
+ private static readonly OK;
11
+ private static readonly RETRY_METHODS;
12
+ private http;
13
+ private readonly databases;
14
+ private readonly logger;
15
+ constructor(couch: CouchConfig, logger: Logger);
16
+ database(name: string): CouchDatabase;
17
+ ping(): Promise<boolean>;
18
+ private client;
19
+ }
@@ -0,0 +1,67 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ var CouchConnection_1;
11
+ import got, {} from 'got';
12
+ import { inject, injectable } from 'inversify';
13
+ import { TYPES } from '../di/types.js';
14
+ import { CouchDatabase } from './couch-database.js';
15
+ /** Shared HTTP connection to CouchDB; hands out CouchDatabase objects (one per name, cached). */
16
+ let CouchConnection = class CouchConnection {
17
+ static { CouchConnection_1 = this; }
18
+ couch;
19
+ static PING_TIMEOUT_MS = 3_000;
20
+ static UP_PATH = '_up';
21
+ static OK = 200;
22
+ static RETRY_METHODS = ['GET', 'PUT', 'HEAD'];
23
+ http = null;
24
+ databases = new Map();
25
+ logger;
26
+ constructor(couch, logger) {
27
+ this.couch = couch;
28
+ this.logger = logger.child({ component: 'couch' });
29
+ }
30
+ database(name) {
31
+ let db = this.databases.get(name);
32
+ if (!db) {
33
+ db = new CouchDatabase(name, this.client(), this.couch.publicUrl, this.logger);
34
+ this.databases.set(name, db);
35
+ }
36
+ return db;
37
+ }
38
+ async ping() {
39
+ try {
40
+ const res = await this.client().get(CouchConnection_1.UP_PATH, { retry: { limit: 0 }, timeout: { request: CouchConnection_1.PING_TIMEOUT_MS } });
41
+ return res.statusCode === CouchConnection_1.OK;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ client() {
48
+ this.http ??= got.extend({
49
+ prefixUrl: this.couch.url,
50
+ username: this.couch.user,
51
+ password: this.couch.password,
52
+ responseType: 'json',
53
+ throwHttpErrors: false,
54
+ timeout: { request: this.couch.timeoutMs },
55
+ // PUT is safe to retry here: creates answer 409 the second time, upserts re-read the _rev.
56
+ retry: { limit: this.couch.retryLimit, methods: [...CouchConnection_1.RETRY_METHODS] },
57
+ });
58
+ return this.http;
59
+ }
60
+ };
61
+ CouchConnection = CouchConnection_1 = __decorate([
62
+ injectable(),
63
+ __param(0, inject(TYPES.CouchConfig)),
64
+ __param(1, inject(TYPES.Logger))
65
+ ], CouchConnection);
66
+ export { CouchConnection };
67
+ //# sourceMappingURL=couch-connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"couch-connection.js","sourceRoot":"","sources":["../../../src/storage/couch-connection.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,OAAO,GAAG,EAAE,EAAY,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,iGAAiG;AAE1F,IAAM,eAAe,GAArB,MAAM,eAAe;;IAUoB;IATtC,MAAM,CAAU,eAAe,GAAG,KAAK,CAAC;IACxC,MAAM,CAAU,OAAO,GAAG,KAAK,CAAC;IAChC,MAAM,CAAU,EAAE,GAAG,GAAG,CAAC;IACzB,MAAM,CAAU,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAU,CAAC;IAChE,IAAI,GAAe,IAAI,CAAC;IACf,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC7C,MAAM,CAAS;IAEhC,YAC8C,KAAkB,EACxC,MAAc;QADQ,UAAK,GAAL,KAAK,CAAa;QAG9D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IACrD,CAAC;IAEM,QAAQ,CAAC,IAAY;QAC1B,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,EAAE,GAAG,IAAI,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC/E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,iBAAe,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,iBAAe,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;YAC7I,OAAO,GAAG,CAAC,UAAU,KAAK,iBAAe,CAAC,EAAE,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,IAAI,KAAK,GAAG,CAAC,MAAM,CAAC;YACvB,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG;YACzB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI;YACzB,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ;YAC7B,YAAY,EAAE,MAAM;YACpB,eAAe,EAAE,KAAK;YACtB,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE;YAC1C,2FAA2F;YAC3F,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC,GAAG,iBAAe,CAAC,aAAa,CAAC,EAAE;SACrF,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;;AA9CU,eAAe;IAD3B,UAAU,EAAE;IAWR,WAAA,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACzB,WAAA,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;GAXZ,eAAe,CA+C3B"}
@@ -0,0 +1,44 @@
1
+ import type { Got } from 'got';
2
+ import type { Logger } from '../logging/logger.js';
3
+ import { type CouchDatabaseApi, type CouchDoc, type CreateResult } from './storage.models.js';
4
+ /**
5
+ * One CouchDB database over a shared got instance. Created by CouchConnection. The database is created on first
6
+ * use (idempotent), so a fresh CouchDB needs no setup step.
7
+ */
8
+ export declare class CouchDatabase implements CouchDatabaseApi {
9
+ readonly name: string;
10
+ private readonly http;
11
+ private readonly publicUrl;
12
+ private readonly logger;
13
+ private static readonly CONFLICT_ATTEMPTS;
14
+ private static readonly OK;
15
+ /** 201 written, 202 accepted (written, not yet on every node). */
16
+ private static readonly WRITTEN;
17
+ private static readonly NOT_FOUND;
18
+ private static readonly CONFLICT;
19
+ /** PUT /<db> when the database exists. */
20
+ private static readonly DB_EXISTS;
21
+ private static readonly FAUXTON;
22
+ private static readonly ALL_DOCS;
23
+ /** CouchDB's documented "end of a prefix range" character. */
24
+ private static readonly HIGHEST_CHAR;
25
+ private ready;
26
+ constructor(name: string, http: Got, publicUrl: string, logger: Logger);
27
+ get<T extends CouchDoc>(id: string): Promise<T | null>;
28
+ /** Write a new document; 'exists' when the id is taken (never overwrites). */
29
+ create(doc: CouchDoc): Promise<CreateResult>;
30
+ /** Create or replace a document (optimistic locking, retried on conflicts). */
31
+ upsert(doc: CouchDoc): Promise<{
32
+ rev: string;
33
+ created: boolean;
34
+ }>;
35
+ /** Read-modify-write with optimistic locking: on a conflict the document is read again and `change` re-applied. */
36
+ update<T extends CouchDoc>(id: string, change: (doc: T) => void): Promise<T | null>;
37
+ /** `_all_docs` over an id range: every id that starts with `prefix`. */
38
+ byPrefix<T extends CouchDoc>(prefix: string, limit: number, descending?: boolean): Promise<T[]>;
39
+ docUrl(id: string): string;
40
+ dbUrl(): string;
41
+ /** PUT /<db> once per process: 201 created, 412 exists. */
42
+ private ensure;
43
+ private docPath;
44
+ }
@@ -0,0 +1,118 @@
1
+ import { CouchError } from './couch-error.js';
2
+ import { CREATE_RESULT } from './storage.models.js';
3
+ /**
4
+ * One CouchDB database over a shared got instance. Created by CouchConnection. The database is created on first
5
+ * use (idempotent), so a fresh CouchDB needs no setup step.
6
+ */
7
+ export class CouchDatabase {
8
+ name;
9
+ http;
10
+ publicUrl;
11
+ logger;
12
+ static CONFLICT_ATTEMPTS = 5;
13
+ static OK = 200;
14
+ /** 201 written, 202 accepted (written, not yet on every node). */
15
+ static WRITTEN = new Set([201, 202]);
16
+ static NOT_FOUND = 404;
17
+ static CONFLICT = 409;
18
+ /** PUT /<db> when the database exists. */
19
+ static DB_EXISTS = 412;
20
+ static FAUXTON = '_utils/#database';
21
+ static ALL_DOCS = '_all_docs';
22
+ /** CouchDB's documented "end of a prefix range" character. */
23
+ static HIGHEST_CHAR = '\ufff0';
24
+ ready = null;
25
+ constructor(name, http, publicUrl, logger) {
26
+ this.name = name;
27
+ this.http = http;
28
+ this.publicUrl = publicUrl;
29
+ this.logger = logger;
30
+ }
31
+ async get(id) {
32
+ await this.ensure();
33
+ const res = await this.http.get(this.docPath(id));
34
+ if (res.statusCode === CouchDatabase.NOT_FOUND)
35
+ return null;
36
+ if (res.statusCode !== CouchDatabase.OK)
37
+ throw new CouchError(`GET ${this.name}/${id}`, res.statusCode, res.body);
38
+ return res.body;
39
+ }
40
+ /** Write a new document; 'exists' when the id is taken (never overwrites). */
41
+ async create(doc) {
42
+ await this.ensure();
43
+ const { _rev: _ignored, ...body } = doc;
44
+ const res = await this.http.put(this.docPath(doc._id), { json: body });
45
+ if (CouchDatabase.WRITTEN.has(res.statusCode))
46
+ return CREATE_RESULT.CREATED;
47
+ if (res.statusCode === CouchDatabase.CONFLICT)
48
+ return CREATE_RESULT.EXISTS;
49
+ throw new CouchError(`create ${this.name}/${doc._id}`, res.statusCode, res.body);
50
+ }
51
+ /** Create or replace a document (optimistic locking, retried on conflicts). */
52
+ async upsert(doc) {
53
+ await this.ensure();
54
+ for (let attempt = 1; attempt <= CouchDatabase.CONFLICT_ATTEMPTS; attempt++) {
55
+ const current = await this.get(doc._id);
56
+ const body = { ...doc, ...(current?._rev ? { _rev: current._rev } : {}) };
57
+ if (!current)
58
+ delete body._rev;
59
+ const res = await this.http.put(this.docPath(doc._id), { json: body });
60
+ if (CouchDatabase.WRITTEN.has(res.statusCode))
61
+ return { rev: res.body.rev, created: !current };
62
+ if (res.statusCode !== CouchDatabase.CONFLICT)
63
+ throw new CouchError(`upsert ${this.name}/${doc._id}`, res.statusCode, res.body);
64
+ }
65
+ throw new CouchError(`upsert ${this.name}/${doc._id} (too many conflicts)`, CouchDatabase.CONFLICT, null);
66
+ }
67
+ /** Read-modify-write with optimistic locking: on a conflict the document is read again and `change` re-applied. */
68
+ async update(id, change) {
69
+ await this.ensure();
70
+ for (let attempt = 1; attempt <= CouchDatabase.CONFLICT_ATTEMPTS; attempt++) {
71
+ const doc = await this.get(id);
72
+ if (!doc)
73
+ return null;
74
+ change(doc);
75
+ const res = await this.http.put(this.docPath(id), { json: doc });
76
+ if (CouchDatabase.WRITTEN.has(res.statusCode))
77
+ return { ...doc, _rev: res.body.rev };
78
+ if (res.statusCode !== CouchDatabase.CONFLICT)
79
+ throw new CouchError(`update ${this.name}/${id}`, res.statusCode, res.body);
80
+ }
81
+ throw new CouchError(`update ${this.name}/${id} (too many conflicts)`, CouchDatabase.CONFLICT, null);
82
+ }
83
+ /** `_all_docs` over an id range: every id that starts with `prefix`. */
84
+ async byPrefix(prefix, limit, descending = false) {
85
+ await this.ensure();
86
+ const low = JSON.stringify(prefix);
87
+ const high = JSON.stringify(prefix + CouchDatabase.HIGHEST_CHAR);
88
+ const searchParams = { include_docs: 'true', limit: String(limit), descending: String(descending), startkey: descending ? high : low, endkey: descending ? low : high };
89
+ const res = await this.http.get(`${encodeURIComponent(this.name)}/${CouchDatabase.ALL_DOCS}`, { searchParams });
90
+ if (res.statusCode !== CouchDatabase.OK)
91
+ throw new CouchError(`list ${this.name}/${prefix}*`, res.statusCode, res.body);
92
+ return res.body.rows.flatMap((r) => (r.doc ? [r.doc] : []));
93
+ }
94
+ docUrl(id) {
95
+ return `${this.publicUrl}/${CouchDatabase.FAUXTON}/${encodeURIComponent(this.name)}/${encodeURIComponent(id)}`;
96
+ }
97
+ dbUrl() {
98
+ return `${this.publicUrl}/${CouchDatabase.FAUXTON}/${encodeURIComponent(this.name)}/${CouchDatabase.ALL_DOCS}`;
99
+ }
100
+ /** PUT /<db> once per process: 201 created, 412 exists. */
101
+ ensure() {
102
+ this.ready ??= (async () => {
103
+ const res = await this.http.put(encodeURIComponent(this.name));
104
+ if (!CouchDatabase.WRITTEN.has(res.statusCode) && res.statusCode !== CouchDatabase.DB_EXISTS)
105
+ throw new CouchError(`create db ${this.name}`, res.statusCode, res.body);
106
+ if (res.statusCode !== CouchDatabase.DB_EXISTS)
107
+ this.logger.info('couchdb database created', { db: this.name });
108
+ })().catch((err) => {
109
+ this.ready = null; // try again next time (CouchDB may come up later)
110
+ throw err;
111
+ });
112
+ return this.ready;
113
+ }
114
+ docPath(id) {
115
+ return `${encodeURIComponent(this.name)}/${encodeURIComponent(id)}`;
116
+ }
117
+ }
118
+ //# sourceMappingURL=couch-database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"couch-database.js","sourceRoot":"","sources":["../../../src/storage/couch-database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { CouchConfig } from '../config/config.models.js';
2
+ import type { CouchDatabases, CouchDoc, DocumentStore, SavedDocument } from './storage.models.js';
3
+ /** DocumentStore on CouchDB: couch.db by default, any database by name. */
4
+ export declare class CouchDocumentStore implements DocumentStore {
5
+ private readonly config;
6
+ private readonly couch;
7
+ constructor(config: CouchConfig, couch: CouchDatabases);
8
+ save(id: string, document: Record<string, unknown>, options?: {
9
+ db?: string;
10
+ }): Promise<SavedDocument>;
11
+ get<T extends object>(id: string, options?: {
12
+ db?: string;
13
+ }): Promise<(T & CouchDoc) | null>;
14
+ }
@@ -0,0 +1,36 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { inject, injectable } from 'inversify';
11
+ import { TYPES } from '../di/types.js';
12
+ /** DocumentStore on CouchDB: couch.db by default, any database by name. */
13
+ let CouchDocumentStore = class CouchDocumentStore {
14
+ config;
15
+ couch;
16
+ constructor(config, couch) {
17
+ this.config = config;
18
+ this.couch = couch;
19
+ }
20
+ async save(id, document, options = {}) {
21
+ const db = this.couch.database(options.db ?? this.config.db);
22
+ const { _id: _ignoredId, _rev: _ignoredRev, ...body } = document;
23
+ const { rev, created } = await db.upsert({ ...body, _id: id, savedAt: new Date().toISOString() });
24
+ return { db: db.name, id, rev, created, url: db.docUrl(id) };
25
+ }
26
+ async get(id, options = {}) {
27
+ return this.couch.database(options.db ?? this.config.db).get(id);
28
+ }
29
+ };
30
+ CouchDocumentStore = __decorate([
31
+ injectable(),
32
+ __param(0, inject(TYPES.CouchConfig)),
33
+ __param(1, inject(TYPES.CouchConnection))
34
+ ], CouchDocumentStore);
35
+ export { CouchDocumentStore };
36
+ //# sourceMappingURL=couch-document-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"couch-document-store.js","sourceRoot":"","sources":["../../../src/storage/couch-document-store.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,2EAA2E;AAEpE,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEiB;IACI;IAFlD,YAC8C,MAAmB,EACf,KAAqB;QADzB,WAAM,GAAN,MAAM,CAAa;QACf,UAAK,GAAL,KAAK,CAAgB;IACpE,CAAC;IAEG,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,QAAiC,EAAE,UAA2B,EAAE;QAC5F,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC7D,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;QACjE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAClG,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;IAC/D,CAAC;IAEM,KAAK,CAAC,GAAG,CAAmB,EAAU,EAAE,UAA2B,EAAE;QAC1E,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,CAAe,EAAE,CAAC,CAAC;IACjF,CAAC;CACF,CAAA;AAhBY,kBAAkB;IAD9B,UAAU,EAAE;IAGR,WAAA,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACzB,WAAA,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;GAHrB,kBAAkB,CAgB9B"}