@jondotsoy/pika 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/lib/esm/auth/dto/connection.d.ts +43 -0
- package/lib/esm/auth/dto/connection.js +16 -0
- package/lib/esm/auth/index.d.ts +10 -0
- package/lib/esm/auth/index.js +64 -0
- package/lib/esm/cli/__samples__/parent-child-script.d.ts +1 -0
- package/lib/esm/cli/__samples__/parent-child-script.js +9 -0
- package/lib/esm/cli/cli-event-payload.d.ts +53 -0
- package/lib/esm/cli/cli-event-payload.js +32 -0
- package/lib/esm/cli/index.d.ts +2 -0
- package/lib/esm/cli/index.js +167 -0
- package/lib/esm/cli/utils/create-server.d.ts +20 -0
- package/lib/esm/cli/utils/create-server.js +70 -0
- package/lib/esm/flow/__samples__/credentials-job.d.ts +1 -0
- package/lib/esm/flow/__samples__/credentials-job.js +9 -0
- package/lib/esm/flow/__samples__/only-app.d.ts +1 -0
- package/lib/esm/flow/__samples__/only-app.js +4 -0
- package/lib/esm/flow/__samples__/parent-child-jobs.d.ts +1 -0
- package/lib/esm/flow/__samples__/parent-child-jobs.js +10 -0
- package/lib/esm/flow/__samples__/parent-with-subjobs.d.ts +1 -0
- package/lib/esm/flow/__samples__/parent-with-subjobs.js +9 -0
- package/lib/esm/flow/__samples__/wait-job.d.ts +1 -0
- package/lib/esm/flow/__samples__/wait-job.js +6 -0
- package/lib/esm/flow/__samples__/zod-invalid-job.d.ts +1 -0
- package/lib/esm/flow/__samples__/zod-invalid-job.js +4 -0
- package/lib/esm/flow/__samples__/zod-validation-job.d.ts +1 -0
- package/lib/esm/flow/__samples__/zod-validation-job.js +6 -0
- package/lib/esm/flow/configs/env-configs.d.ts +37 -0
- package/lib/esm/flow/configs/env-configs.js +59 -0
- package/lib/esm/flow/constants/run-mode.d.ts +5 -0
- package/lib/esm/flow/constants/run-mode.js +6 -0
- package/lib/esm/flow/context-reflects/cli-http-reflect.d.ts +9 -0
- package/lib/esm/flow/context-reflects/cli-http-reflect.js +55 -0
- package/lib/esm/flow/context-reflects/dsn-reflect.d.ts +29 -0
- package/lib/esm/flow/context-reflects/dsn-reflect.js +181 -0
- package/lib/esm/flow/context-reflects/file-context-reflect.d.ts +22 -0
- package/lib/esm/flow/context-reflects/file-context-reflect.js +47 -0
- package/lib/esm/flow/context.d.ts +43 -0
- package/lib/esm/flow/context.js +104 -0
- package/lib/esm/flow/current.d.ts +4 -0
- package/lib/esm/flow/current.js +1 -0
- package/lib/esm/flow/dto/cb-app.d.ts +2 -0
- package/lib/esm/flow/dto/cb-app.js +1 -0
- package/lib/esm/flow/dto/job-id.d.ts +1 -0
- package/lib/esm/flow/dto/job-id.js +1 -0
- package/lib/esm/flow/dto/job.d.ts +18 -0
- package/lib/esm/flow/dto/job.js +9 -0
- package/lib/esm/flow/dto/manifest.d.ts +5 -0
- package/lib/esm/flow/dto/manifest.js +1 -0
- package/lib/esm/flow/index.d.ts +26 -0
- package/lib/esm/flow/index.js +46 -0
- package/lib/esm/flow/loader.d.ts +1 -0
- package/lib/esm/flow/loader.js +227 -0
- package/lib/esm/flow/util/json-rpc-transporter.d.ts +80 -0
- package/lib/esm/flow/util/json-rpc-transporter.js +175 -0
- package/lib/esm/flow/utils/bytes.d.ts +29 -0
- package/lib/esm/flow/utils/bytes.js +32 -0
- package/lib/esm/flow/utils/event-manager.d.ts +11 -0
- package/lib/esm/flow/utils/event-manager.js +41 -0
- package/lib/esm/flow/utils/file.d.ts +17 -0
- package/lib/esm/flow/utils/file.js +52 -0
- package/lib/esm/flow/utils/observable-set.d.ts +9 -0
- package/lib/esm/flow/utils/observable-set.js +31 -0
- package/lib/esm/flow/utils/queue.d.ts +32 -0
- package/lib/esm/flow/utils/queue.js +86 -0
- package/lib/esm/flow/worker_ctx.d.ts +5 -0
- package/lib/esm/flow/worker_ctx.js +2 -0
- package/lib/esm/http/index.d.ts +32 -0
- package/lib/esm/http/index.js +54 -0
- package/lib/esm/telemetry/metrics/gateway.d.ts +2 -0
- package/lib/esm/telemetry/metrics/gateway.js +6 -0
- package/lib/esm/telemetry/metrics/job-gauges.d.ts +13 -0
- package/lib/esm/telemetry/metrics/job-gauges.js +39 -0
- package/lib/esm/telemetry/metrics/job-store.d.ts +3 -0
- package/lib/esm/telemetry/metrics/job-store.js +31 -0
- package/lib/esm/telemetry/metrics/push.d.ts +1 -0
- package/lib/esm/telemetry/metrics/push.js +4 -0
- package/lib/esm/telemetry/metrics/registry.d.ts +2 -0
- package/lib/esm/telemetry/metrics/registry.js +2 -0
- package/lib/esm/telemetry/metrics/telemetry-url.d.ts +1 -0
- package/lib/esm/telemetry/metrics/telemetry-url.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RunMode } from "../constants/run-mode.js";
|
|
3
|
+
export declare class EnvConfigs {
|
|
4
|
+
private constructor();
|
|
5
|
+
static configParser: z.ZodObject<{
|
|
6
|
+
contextPath: z.ZodCatch<z.ZodString>;
|
|
7
|
+
runMode: z.ZodCatch<z.ZodUnion<readonly [z.ZodPipe<z.ZodLiteral<"IN_PROCESS">, z.ZodTransform<RunMode, "IN_PROCESS">>, z.ZodPipe<z.ZodLiteral<"CLUSTER">, z.ZodTransform<RunMode, "CLUSTER">>, z.ZodPipe<z.ZodLiteral<"CLUSTER_PRIMARY">, z.ZodTransform<RunMode, "CLUSTER_PRIMARY">>, z.ZodPipe<z.ZodLiteral<"CLUSTER_WORKER">, z.ZodTransform<RunMode, "CLUSTER_WORKER">>]>>;
|
|
8
|
+
concurrentWorkers: z.ZodCatch<z.ZodUnion<readonly [z.ZodNumber, z.ZodPipe<z.ZodString, z.ZodTransform<number, string>>]>>;
|
|
9
|
+
disableLoadJob: z.ZodCatch<z.ZodUnion<readonly [z.ZodBoolean, z.ZodPipe<z.ZodNumber, z.ZodTransform<boolean, number>>, z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>]>>;
|
|
10
|
+
disableContextFile: z.ZodCatch<z.ZodUnion<readonly [z.ZodBoolean, z.ZodPipe<z.ZodNumber, z.ZodTransform<boolean, number>>, z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>]>>;
|
|
11
|
+
cliInspectorUrl: z.ZodCatch<z.ZodNullable<z.ZodString>>;
|
|
12
|
+
dsn: z.ZodCatch<z.ZodNullable<z.ZodString>>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
static loadEnvironmentSettings(): Promise<{
|
|
15
|
+
contextPath: string;
|
|
16
|
+
runMode: RunMode;
|
|
17
|
+
concurrentWorkers: number;
|
|
18
|
+
disableLoadJob: boolean;
|
|
19
|
+
disableContextFile: boolean;
|
|
20
|
+
cliInspectorUrl: string | null;
|
|
21
|
+
dsn: string | null;
|
|
22
|
+
}>;
|
|
23
|
+
private static __values;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the parsed environment configuration, loading it once and caching the result.
|
|
26
|
+
* @returns The parsed environment configuration object.
|
|
27
|
+
*/
|
|
28
|
+
static configs(): Promise<{
|
|
29
|
+
contextPath: string;
|
|
30
|
+
runMode: RunMode;
|
|
31
|
+
concurrentWorkers: number;
|
|
32
|
+
disableLoadJob: boolean;
|
|
33
|
+
disableContextFile: boolean;
|
|
34
|
+
cliInspectorUrl: string | null;
|
|
35
|
+
dsn: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ulid } from "ulid";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { RunMode } from "../constants/run-mode.js";
|
|
4
|
+
const booleable = z.union([
|
|
5
|
+
z.boolean(),
|
|
6
|
+
z.number().transform((num) => !!num),
|
|
7
|
+
z
|
|
8
|
+
.string()
|
|
9
|
+
.transform((str) => ["1", "true", "yes"].includes(str.toLowerCase())),
|
|
10
|
+
]);
|
|
11
|
+
const numeric = z.union([
|
|
12
|
+
z.number(),
|
|
13
|
+
z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^\d+$/)
|
|
16
|
+
.transform((str) => parseInt(str, 10)),
|
|
17
|
+
]);
|
|
18
|
+
export class EnvConfigs {
|
|
19
|
+
constructor() { }
|
|
20
|
+
static configParser = z.object({
|
|
21
|
+
contextPath: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.catch(() => `${process.cwd()}/.session/${ulid()}.yaml`),
|
|
25
|
+
runMode: z
|
|
26
|
+
.union([
|
|
27
|
+
z.literal("IN_PROCESS").transform(() => RunMode.IN_PROCESS),
|
|
28
|
+
z.literal("CLUSTER").transform(() => RunMode.CLUSTER_PRIMARY),
|
|
29
|
+
z.literal("CLUSTER_PRIMARY").transform(() => RunMode.CLUSTER_PRIMARY),
|
|
30
|
+
z.literal("CLUSTER_WORKER").transform(() => RunMode.CLUSTER_WORKER),
|
|
31
|
+
])
|
|
32
|
+
.catch(RunMode.IN_PROCESS),
|
|
33
|
+
concurrentWorkers: numeric.catch(1),
|
|
34
|
+
disableLoadJob: booleable.catch(false),
|
|
35
|
+
disableContextFile: booleable.catch(false),
|
|
36
|
+
cliInspectorUrl: z.string().url().nullable().catch(null),
|
|
37
|
+
dsn: z.string().url().nullable().catch(null),
|
|
38
|
+
});
|
|
39
|
+
static async loadEnvironmentSettings() {
|
|
40
|
+
return this.configParser.parse({
|
|
41
|
+
contextPath: process.env.PIKA_CONTEXT_PATH,
|
|
42
|
+
runMode: process.env.PIKA_RUN_MODE,
|
|
43
|
+
concurrentWorkers: process.env.PIKA_CONCURRENT_WORKERS,
|
|
44
|
+
disableLoadJob: process.env.PIKA_DISABLE_LOAD_JOB,
|
|
45
|
+
disableContextFile: process.env.PIKA_DISABLE_CONTEXT_FILE,
|
|
46
|
+
cliInspectorUrl: process.env.PIKA_CLI_INSPECTOR_URL,
|
|
47
|
+
dsn: process.env.PIKA_DSN,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
static __values = null;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the parsed environment configuration, loading it once and caching the result.
|
|
53
|
+
* @returns The parsed environment configuration object.
|
|
54
|
+
*/
|
|
55
|
+
static async configs() {
|
|
56
|
+
EnvConfigs.__values ??= await EnvConfigs.loadEnvironmentSettings();
|
|
57
|
+
return EnvConfigs.__values;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from "../context.js";
|
|
2
|
+
export declare class CliHttpReflect {
|
|
3
|
+
readonly context: Context;
|
|
4
|
+
readonly inspectorUrl: string;
|
|
5
|
+
constructor(context: Context, inspectorUrl: string);
|
|
6
|
+
private post;
|
|
7
|
+
private attach;
|
|
8
|
+
static attach(context: Context, inspectorUrl: string): CliHttpReflect;
|
|
9
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class CliHttpReflect {
|
|
2
|
+
context;
|
|
3
|
+
inspectorUrl;
|
|
4
|
+
constructor(context, inspectorUrl) {
|
|
5
|
+
this.context = context;
|
|
6
|
+
this.inspectorUrl = inspectorUrl;
|
|
7
|
+
this.attach();
|
|
8
|
+
}
|
|
9
|
+
post(payload) {
|
|
10
|
+
fetch(this.inspectorUrl, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify(payload),
|
|
14
|
+
}).catch(() => { });
|
|
15
|
+
}
|
|
16
|
+
attach() {
|
|
17
|
+
const { context } = this;
|
|
18
|
+
context.events.addEventListener("job_created", (jobId) => {
|
|
19
|
+
const job = context.jobs.get(jobId);
|
|
20
|
+
if (job)
|
|
21
|
+
this.post({ event: "job_created", jobId, job });
|
|
22
|
+
});
|
|
23
|
+
context.events.addEventListener("job_started", (jobId) => {
|
|
24
|
+
const job = context.jobs.get(jobId);
|
|
25
|
+
if (job)
|
|
26
|
+
this.post({ event: "job_started", jobId, job });
|
|
27
|
+
});
|
|
28
|
+
context.events.addEventListener("job_done", (jobId) => {
|
|
29
|
+
const job = context.jobs.get(jobId);
|
|
30
|
+
if (job)
|
|
31
|
+
this.post({ event: "job_done", jobId, job });
|
|
32
|
+
});
|
|
33
|
+
context.events.addEventListener("job_error", (jobId, err) => {
|
|
34
|
+
const job = context.jobs.get(jobId);
|
|
35
|
+
if (job)
|
|
36
|
+
this.post({
|
|
37
|
+
event: "job_error",
|
|
38
|
+
jobId,
|
|
39
|
+
job,
|
|
40
|
+
error: {
|
|
41
|
+
name: err?.name ?? "Error",
|
|
42
|
+
message: err?.message ?? String(err),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
context.events.addEventListener("job_retry", (jobId, retryCount) => {
|
|
47
|
+
const job = context.jobs.get(jobId);
|
|
48
|
+
if (job)
|
|
49
|
+
this.post({ event: "job_retry", jobId, job, retryCount });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
static attach(context, inspectorUrl) {
|
|
53
|
+
return new CliHttpReflect(context, inspectorUrl);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Context } from "../context.js";
|
|
2
|
+
import { EventManager } from "../utils/event-manager.js";
|
|
3
|
+
import { Bytes } from "../utils/bytes.js";
|
|
4
|
+
export type DsnReflectOptions = {
|
|
5
|
+
dsn: string;
|
|
6
|
+
maxEventsPerRequest?: number;
|
|
7
|
+
waitTime?: number;
|
|
8
|
+
maxRequestSize?: Bytes;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare class DsnReflect {
|
|
12
|
+
readonly context: Context;
|
|
13
|
+
private messages;
|
|
14
|
+
private messageResolver;
|
|
15
|
+
private options;
|
|
16
|
+
readonly events: EventManager<{
|
|
17
|
+
push_message: (message: {
|
|
18
|
+
eventId: string;
|
|
19
|
+
body: Uint8Array;
|
|
20
|
+
}) => void;
|
|
21
|
+
}>;
|
|
22
|
+
constructor(context: Context, options: DsnReflectOptions);
|
|
23
|
+
static defaultOptions(partial?: DsnReflectOptions): Required<DsnReflectOptions>;
|
|
24
|
+
private post;
|
|
25
|
+
private startSendLoop;
|
|
26
|
+
private sendRequest;
|
|
27
|
+
private attach;
|
|
28
|
+
static attach(context: Context, options: DsnReflectOptions): DsnReflect;
|
|
29
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { EventManager } from "../utils/event-manager.js";
|
|
2
|
+
import { Bytes } from "../utils/bytes.js";
|
|
3
|
+
export class DsnReflect {
|
|
4
|
+
context;
|
|
5
|
+
messages = new Set();
|
|
6
|
+
messageResolver = Promise.withResolvers();
|
|
7
|
+
options;
|
|
8
|
+
events = new EventManager();
|
|
9
|
+
constructor(context, options) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.options = DsnReflect.defaultOptions(options);
|
|
12
|
+
this.events.addEventListener("push_message", () => {
|
|
13
|
+
this.messageResolver.resolve();
|
|
14
|
+
});
|
|
15
|
+
this.startSendLoop();
|
|
16
|
+
this.attach();
|
|
17
|
+
}
|
|
18
|
+
static defaultOptions(partial) {
|
|
19
|
+
return {
|
|
20
|
+
dsn: partial?.dsn ?? "",
|
|
21
|
+
maxEventsPerRequest: partial?.maxEventsPerRequest ?? 100,
|
|
22
|
+
waitTime: partial?.waitTime ?? 0,
|
|
23
|
+
maxRequestSize: partial?.maxRequestSize ?? Bytes.from({ mb: 1 }),
|
|
24
|
+
maxRetries: partial?.maxRetries ?? 3,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
post(notification) {
|
|
28
|
+
const body = new TextEncoder().encode(JSON.stringify(notification));
|
|
29
|
+
const message = { eventId: notification.params.eventId, body };
|
|
30
|
+
this.messages.add(message);
|
|
31
|
+
this.events.emit("push_message", message);
|
|
32
|
+
}
|
|
33
|
+
async startSendLoop() {
|
|
34
|
+
const { maxEventsPerRequest, waitTime, maxRequestSize } = this.options;
|
|
35
|
+
const maxBytes = maxRequestSize.bytes;
|
|
36
|
+
while (true) {
|
|
37
|
+
await this.messageResolver.promise;
|
|
38
|
+
this.messageResolver = Promise.withResolvers();
|
|
39
|
+
const requests = [];
|
|
40
|
+
while (this.messages.size > 0) {
|
|
41
|
+
const batch = [];
|
|
42
|
+
let totalSize = 0n;
|
|
43
|
+
for (const msg of this.messages) {
|
|
44
|
+
if (batch.length >= maxEventsPerRequest)
|
|
45
|
+
break;
|
|
46
|
+
if (batch.length > 0 &&
|
|
47
|
+
totalSize + BigInt(msg.body.length) > maxBytes)
|
|
48
|
+
break;
|
|
49
|
+
batch.push(msg);
|
|
50
|
+
totalSize += BigInt(msg.body.length);
|
|
51
|
+
}
|
|
52
|
+
if (batch.length === 0)
|
|
53
|
+
break;
|
|
54
|
+
for (const msg of batch)
|
|
55
|
+
this.messages.delete(msg);
|
|
56
|
+
const newline = new TextEncoder().encode("\n");
|
|
57
|
+
const parts = [];
|
|
58
|
+
for (let i = 0; i < batch.length; i++) {
|
|
59
|
+
parts.push(batch[i].body);
|
|
60
|
+
if (i < batch.length - 1)
|
|
61
|
+
parts.push(newline);
|
|
62
|
+
}
|
|
63
|
+
const combined = new Uint8Array(parts.reduce((s, p) => s + p.length, 0));
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const p of parts) {
|
|
66
|
+
combined.set(p, offset);
|
|
67
|
+
offset += p.length;
|
|
68
|
+
}
|
|
69
|
+
requests.push({
|
|
70
|
+
url: this.options.dsn,
|
|
71
|
+
init: {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/vnd.pika.jsonrpc+jsonl; charset=utf-8",
|
|
75
|
+
"X-Pika-Event-Count": String(batch.length),
|
|
76
|
+
},
|
|
77
|
+
body: combined,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (const request of requests) {
|
|
82
|
+
await this.sendRequest(request);
|
|
83
|
+
}
|
|
84
|
+
if (waitTime > 0)
|
|
85
|
+
await new Promise((r) => setTimeout(r, waitTime));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async sendRequest(request) {
|
|
89
|
+
const { maxRetries } = this.options;
|
|
90
|
+
let attempt = 0;
|
|
91
|
+
while (attempt <= maxRetries) {
|
|
92
|
+
let response = null;
|
|
93
|
+
try {
|
|
94
|
+
response = await fetch(request.url, request.init);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (response.status === 429) {
|
|
100
|
+
attempt++;
|
|
101
|
+
if (attempt > maxRetries)
|
|
102
|
+
return;
|
|
103
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
104
|
+
const waitMs = retryAfter ? parseFloat(retryAfter) * 1000 : 1000;
|
|
105
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
attach() {
|
|
112
|
+
const { context } = this;
|
|
113
|
+
this.post({
|
|
114
|
+
jsonrpc: "2.0",
|
|
115
|
+
method: "system.ready",
|
|
116
|
+
params: { eventId: crypto.randomUUID(), timestamp: Date.now() },
|
|
117
|
+
});
|
|
118
|
+
context.events.addEventListener("job_created", (jobId) => {
|
|
119
|
+
const job = context.jobs.get(jobId);
|
|
120
|
+
if (job)
|
|
121
|
+
this.post({
|
|
122
|
+
jsonrpc: "2.0",
|
|
123
|
+
method: "job.create",
|
|
124
|
+
params: { eventId: crypto.randomUUID(), timestamp: Date.now(), job },
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
context.events.addEventListener("job_started", (jobId) => {
|
|
128
|
+
const job = context.jobs.get(jobId);
|
|
129
|
+
if (job)
|
|
130
|
+
this.post({
|
|
131
|
+
jsonrpc: "2.0",
|
|
132
|
+
method: "job.start",
|
|
133
|
+
params: { eventId: crypto.randomUUID(), timestamp: Date.now(), job },
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
context.events.addEventListener("job_done", (jobId) => {
|
|
137
|
+
const job = context.jobs.get(jobId);
|
|
138
|
+
if (job)
|
|
139
|
+
this.post({
|
|
140
|
+
jsonrpc: "2.0",
|
|
141
|
+
method: "job.done",
|
|
142
|
+
params: { eventId: crypto.randomUUID(), timestamp: Date.now(), job },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
context.events.addEventListener("job_error", (jobId, err) => {
|
|
146
|
+
const job = context.jobs.get(jobId);
|
|
147
|
+
if (!job)
|
|
148
|
+
return;
|
|
149
|
+
const error = err instanceof Error
|
|
150
|
+
? { name: err.name, message: err.message, stack: err.stack ?? null }
|
|
151
|
+
: { name: "Error", message: String(err), stack: null };
|
|
152
|
+
this.post({
|
|
153
|
+
jsonrpc: "2.0",
|
|
154
|
+
method: "job.error",
|
|
155
|
+
params: {
|
|
156
|
+
eventId: crypto.randomUUID(),
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
job,
|
|
159
|
+
error,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
context.events.addEventListener("job_retry", (jobId, retryCount) => {
|
|
164
|
+
const job = context.jobs.get(jobId);
|
|
165
|
+
if (job)
|
|
166
|
+
this.post({
|
|
167
|
+
jsonrpc: "2.0",
|
|
168
|
+
method: "job.retry",
|
|
169
|
+
params: {
|
|
170
|
+
eventId: crypto.randomUUID(),
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
job,
|
|
173
|
+
retryCount,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
static attach(context, options) {
|
|
179
|
+
return new DsnReflect(context, options);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context } from "../context";
|
|
2
|
+
export declare class FileContextReflect {
|
|
3
|
+
readonly context: Context;
|
|
4
|
+
readonly path: URL | string;
|
|
5
|
+
private _writePending;
|
|
6
|
+
constructor(context: Context, path: URL | string);
|
|
7
|
+
write(): Promise<void>;
|
|
8
|
+
static read(path: URL | string): Promise<{
|
|
9
|
+
context: Context;
|
|
10
|
+
reflector: FileContextReflect;
|
|
11
|
+
}>;
|
|
12
|
+
static create(path: URL | string): Promise<{
|
|
13
|
+
context: Context;
|
|
14
|
+
reflector: FileContextReflect;
|
|
15
|
+
}>;
|
|
16
|
+
static load(path: URL | string, options?: {
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
context: Context;
|
|
20
|
+
reflector: FileContextReflect | null;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as YAML from "yaml";
|
|
2
|
+
import { Context } from "../context";
|
|
3
|
+
import { File } from "../utils/file";
|
|
4
|
+
export class FileContextReflect {
|
|
5
|
+
context;
|
|
6
|
+
path;
|
|
7
|
+
_writePending = Promise.resolve();
|
|
8
|
+
constructor(context, path) {
|
|
9
|
+
this.context = context;
|
|
10
|
+
this.path = path;
|
|
11
|
+
context.events.addEventListener("manifest_updated", async () => {
|
|
12
|
+
await this.write();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async write() {
|
|
16
|
+
this._writePending = this._writePending
|
|
17
|
+
.catch(() => { })
|
|
18
|
+
.then(async () => {
|
|
19
|
+
const yaml = YAML.stringify(this.context.manifest);
|
|
20
|
+
await new File(this.path).write(yaml);
|
|
21
|
+
this.context.events.emit("manifest_stored", this.path.toString());
|
|
22
|
+
});
|
|
23
|
+
await this._writePending;
|
|
24
|
+
}
|
|
25
|
+
static async read(path) {
|
|
26
|
+
const content = await new File(path).text();
|
|
27
|
+
const manifest = YAML.parse(content);
|
|
28
|
+
const context = new Context(manifest);
|
|
29
|
+
const reflector = new FileContextReflect(context, path);
|
|
30
|
+
return { context, reflector };
|
|
31
|
+
}
|
|
32
|
+
static async create(path) {
|
|
33
|
+
const context = new Context();
|
|
34
|
+
const reflector = new FileContextReflect(context, path);
|
|
35
|
+
await reflector.write();
|
|
36
|
+
return { context, reflector };
|
|
37
|
+
}
|
|
38
|
+
static async load(path, options) {
|
|
39
|
+
if (options?.disabled) {
|
|
40
|
+
return { context: new Context(), reflector: null };
|
|
41
|
+
}
|
|
42
|
+
const exists = await new File(path).exists();
|
|
43
|
+
return exists
|
|
44
|
+
? await FileContextReflect.read(path)
|
|
45
|
+
: await FileContextReflect.create(path);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventManager } from "./utils/event-manager.js";
|
|
2
|
+
import { JobId } from "./dto/job-id.js";
|
|
3
|
+
import { Job } from "./dto/job.js";
|
|
4
|
+
import { Manifest } from "./dto/manifest.js";
|
|
5
|
+
import { CBApp } from "./dto/cb-app.js";
|
|
6
|
+
export declare class Context {
|
|
7
|
+
readonly manifest: Manifest;
|
|
8
|
+
readonly events: EventManager<{
|
|
9
|
+
job_created: (jobId: string) => void;
|
|
10
|
+
job_loaded: (jobId: string) => void;
|
|
11
|
+
job_started: (jobId: string) => void;
|
|
12
|
+
job_done: (jobId: string) => void;
|
|
13
|
+
job_error: (jobId: string, error: any) => void;
|
|
14
|
+
job_retry: (jobId: string, retryCount: number) => void;
|
|
15
|
+
app_registered: (appId: string) => void;
|
|
16
|
+
manifest_stored: (path: string | URL) => void;
|
|
17
|
+
manifest_updated: () => void;
|
|
18
|
+
}>;
|
|
19
|
+
private appsDefinitions;
|
|
20
|
+
constructor(manifest?: Manifest);
|
|
21
|
+
configs: {
|
|
22
|
+
enableLoadJob: boolean;
|
|
23
|
+
};
|
|
24
|
+
jobs: {
|
|
25
|
+
get: (jobId: JobId) => Job;
|
|
26
|
+
load: (job: Job) => Job;
|
|
27
|
+
error: (jobOrId: JobId | Job, err: any) => void;
|
|
28
|
+
start: (jobOrId: string | Job, startedAt?: Date) => void;
|
|
29
|
+
done: (jobOrId: string | Job, output: Record<string, any>, completedAt?: Date) => void;
|
|
30
|
+
};
|
|
31
|
+
apps: {
|
|
32
|
+
register: (appId: string, inputDef: Record<string, any>, cb: CBApp, credentials?: {
|
|
33
|
+
name: string;
|
|
34
|
+
}[]) => void;
|
|
35
|
+
get: (appId: string) => Promise<{
|
|
36
|
+
inputDef: Record<string, any>;
|
|
37
|
+
cb: CBApp;
|
|
38
|
+
credentials?: {
|
|
39
|
+
name: string;
|
|
40
|
+
}[];
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { processingJobContext } from "./worker_ctx.js";
|
|
2
|
+
import { EventManager } from "./utils/event-manager.js";
|
|
3
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
4
|
+
export class Context {
|
|
5
|
+
manifest;
|
|
6
|
+
events = new EventManager();
|
|
7
|
+
appsDefinitions = new Map();
|
|
8
|
+
constructor(manifest = {
|
|
9
|
+
jobs: {},
|
|
10
|
+
}) {
|
|
11
|
+
this.manifest = manifest;
|
|
12
|
+
}
|
|
13
|
+
configs = {
|
|
14
|
+
enableLoadJob: true,
|
|
15
|
+
};
|
|
16
|
+
jobs = {
|
|
17
|
+
get: (jobId) => {
|
|
18
|
+
return this.manifest.jobs[jobId] ?? null;
|
|
19
|
+
},
|
|
20
|
+
load: (job) => {
|
|
21
|
+
const prevJob = this.jobs.get(job.jobId);
|
|
22
|
+
const storedJob = prevJob ?? job;
|
|
23
|
+
const isNew = !prevJob;
|
|
24
|
+
this.manifest.jobs[job.jobId] = storedJob;
|
|
25
|
+
if (isNew) {
|
|
26
|
+
this.events.emit("job_created", job.jobId);
|
|
27
|
+
this.events.emit("manifest_updated");
|
|
28
|
+
}
|
|
29
|
+
this.events.emit("job_loaded", job.jobId);
|
|
30
|
+
return storedJob;
|
|
31
|
+
},
|
|
32
|
+
error: (jobOrId, err) => {
|
|
33
|
+
const jobId = typeof jobOrId === "string" ? jobOrId : jobOrId.jobId;
|
|
34
|
+
if (!this.manifest.jobs[jobId]) {
|
|
35
|
+
if (typeof jobOrId === "string")
|
|
36
|
+
throw new Error(`Job ${jobId} not found`);
|
|
37
|
+
this.manifest.jobs[jobId] = jobOrId;
|
|
38
|
+
}
|
|
39
|
+
const job = this.manifest.jobs[jobId];
|
|
40
|
+
const maxRetries = job?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
41
|
+
job.error =
|
|
42
|
+
err instanceof Error
|
|
43
|
+
? { name: err.name, message: err.message, stack: err.stack }
|
|
44
|
+
: err;
|
|
45
|
+
job.retries ??= 0;
|
|
46
|
+
this.events.emit("job_error", jobId, err);
|
|
47
|
+
if (job.retries < maxRetries) {
|
|
48
|
+
job.retries += 1;
|
|
49
|
+
this.events.emit("job_retry", jobId, job.retries);
|
|
50
|
+
}
|
|
51
|
+
this.events.emit("manifest_updated");
|
|
52
|
+
},
|
|
53
|
+
start: (jobOrId, startedAt = new Date()) => {
|
|
54
|
+
const jobId = typeof jobOrId === "string" ? jobOrId : jobOrId.jobId;
|
|
55
|
+
if (!this.manifest.jobs[jobId]) {
|
|
56
|
+
if (typeof jobOrId === "string")
|
|
57
|
+
throw new Error(`Job ${jobId} not found`);
|
|
58
|
+
this.manifest.jobs[jobId] = jobOrId;
|
|
59
|
+
}
|
|
60
|
+
const job = this.manifest.jobs[jobId];
|
|
61
|
+
job.startedAt = startedAt;
|
|
62
|
+
job.workerId = processingJobContext.getStore()?.processingId ?? null;
|
|
63
|
+
this.events.emit("job_started", jobId);
|
|
64
|
+
this.events.emit("manifest_updated");
|
|
65
|
+
},
|
|
66
|
+
done: (jobOrId, output, completedAt = new Date()) => {
|
|
67
|
+
const jobId = typeof jobOrId === "string" ? jobOrId : jobOrId.jobId;
|
|
68
|
+
if (!this.manifest.jobs[jobId]) {
|
|
69
|
+
if (typeof jobOrId === "string")
|
|
70
|
+
throw new Error(`Job ${jobId} not found`);
|
|
71
|
+
this.manifest.jobs[jobId] = jobOrId;
|
|
72
|
+
}
|
|
73
|
+
const job = this.manifest.jobs[jobId];
|
|
74
|
+
job.completedAt = completedAt;
|
|
75
|
+
job.output = output;
|
|
76
|
+
this.events.emit("job_done", jobId);
|
|
77
|
+
this.events.emit("manifest_updated");
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
apps = {
|
|
81
|
+
register: (appId, inputDef, cb, credentials) => {
|
|
82
|
+
this.appsDefinitions.set(appId, { inputDef, cb, credentials });
|
|
83
|
+
this.events.emit("app_registered", appId);
|
|
84
|
+
},
|
|
85
|
+
get: async (appId) => {
|
|
86
|
+
const app = this.appsDefinitions.get(appId);
|
|
87
|
+
if (app)
|
|
88
|
+
return app;
|
|
89
|
+
const waitRegister = Promise.withResolvers();
|
|
90
|
+
const listener = (registeredAppId) => {
|
|
91
|
+
if (registeredAppId === appId) {
|
|
92
|
+
waitRegister.resolve(true);
|
|
93
|
+
this.events.removeEventListener("app_registered", listener);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.events.addEventListener("app_registered", listener);
|
|
97
|
+
await waitRegister.promise;
|
|
98
|
+
const registeredApp = this.appsDefinitions.get(appId);
|
|
99
|
+
if (!registeredApp)
|
|
100
|
+
throw new Error(`App ${appId} not found after waiting`);
|
|
101
|
+
return registeredApp;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const currentContext = { current: null };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type JobId = string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JobId } from "./job-id.js";
|
|
2
|
+
export type Job = {
|
|
3
|
+
parentJobId: JobId | null;
|
|
4
|
+
jobId: JobId;
|
|
5
|
+
appId: string;
|
|
6
|
+
credentials?: {
|
|
7
|
+
name: string;
|
|
8
|
+
}[];
|
|
9
|
+
startedAt: Date | null;
|
|
10
|
+
completedAt: Date | null;
|
|
11
|
+
input: Record<string, any>;
|
|
12
|
+
output: null | Record<string, any>;
|
|
13
|
+
error?: any;
|
|
14
|
+
retries?: number;
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
workerId: string | null;
|
|
17
|
+
};
|
|
18
|
+
export declare const isJob: (value: any) => value is Job;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|