@openfn/ws-worker 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # ws-worker
2
+
3
+ ## 0.1.0
4
+
5
+ First release of the websocket worker, which handles comm between Lightning and the multi-threaded engine.
6
+
7
+ Features:
8
+
9
+ - Websocket integration with JWT auth
10
+ - Eventing between Lightning and the Worker
11
+ - Eventing between the Worker and the Engine
12
+ - Placeholder exit reasons
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Websocket Worker
2
+
3
+ The Websocket Worker `ws-worker` provides a Websocket interface between Lightning and a Runtime Engine.
4
+
5
+ It is a fairly thin layer between the two systems, designed to transport messages and convert Lightning data structures into runtime-friendly ones.
6
+
7
+ This package contains:
8
+
9
+ - A server which connects Lightning to an Engine (exposing dev APIs to http and node.js)
10
+ - A mock runtime engine implementation
11
+
12
+ The mock services allow lightweight and controlled testing of the interfaces between them.
13
+
14
+ ## Getting started
15
+
16
+ To use this server:
17
+
18
+ - Start a lightning instance (you can use the mock if you like, see `../lightning-mock`)
19
+ - Start the worker server with `pnpm start`
20
+
21
+ The worker will use the WORKER_SECRET env var (which you should have set for Lightning already). Check WORKERS.md in Lightning and run this in Lightning if you haven't already:
22
+
23
+ ```
24
+ mix lightning.gen_worker_keys
25
+ ```
26
+
27
+ ### WS Server
28
+
29
+ To start a `ws-worker` server, run:
30
+
31
+ ```
32
+ pnpm start
33
+ ```
34
+
35
+ You may want to add `--log debug` or disable the work loop, see below.
36
+
37
+ The default settings will try and connect to lightning at `localhost:4000`.
38
+
39
+ Pass a custom lightining url with `-l ws://localhost:1234`. You need to include the websocket endpoint, which at the time of writing is `/worker`.
40
+
41
+ Use `-l mock` to connect to a lightning mock server (on the default port).
42
+
43
+ ## Watched Server
44
+
45
+ You can start a dev server (which rebuilds on save) by running:
46
+
47
+ ```
48
+ pnpm start:watch
49
+ ```
50
+
51
+ This will wrap a real runtime engine into the server (?). It will rebuild when the Worker Engine code changes (although you'll have to `pnpm build:watch` in `runtime-manager`). This will use the repo at `ENGINE_REPO_DIR` or `/tmp/openfn/repo`.
52
+
53
+ ### Disabling auto-fetch
54
+
55
+ When working in dev it is convinient to disable the workloop. To switch it off, run:
56
+
57
+ ```
58
+ pnpm start --no-loop
59
+ ```
60
+
61
+ To manually trigger a claim, post to `/claim`:
62
+
63
+ ```
64
+ curl -X POST http://localhost:2222/claim
65
+ ```
66
+
67
+ ## Architecture
68
+
69
+ Lightning is expected to maintain a queue of attempts. The Worker pulls those attempts from the queue, via websocket, and sends them off to the Engine for execution.
70
+
71
+ While the engine executes it may need to request more information (like credentials and dataclips) and may feedback status (such as logging and runs). The Worker satisifies both these requirements.
72
+
73
+ The ws-worker server is designed for zero persistence. It does not have any database, does not use the file system. Should the server crash, tracking of any active jobs will be lost (Lightning is expected to time these runs out).
@@ -0,0 +1,30 @@
1
+ import Koa from 'koa';
2
+ import { Logger } from '@openfn/logger';
3
+ import { RuntimeEngine } from '@openfn/engine-multi';
4
+
5
+ declare type CLAIM_ATTEMPT = {
6
+ id: string;
7
+ token: string;
8
+ };
9
+
10
+ declare type ServerOptions = {
11
+ backoff?: number;
12
+ maxBackoff?: number;
13
+ maxWorkflows?: number;
14
+ port?: number;
15
+ lightning?: string;
16
+ logger?: Logger;
17
+ noLoop?: boolean;
18
+ secret?: string;
19
+ };
20
+ interface ServerApp extends Koa {
21
+ id: string;
22
+ socket: any;
23
+ channel: any;
24
+ execute: ({ id, token }: CLAIM_ATTEMPT) => Promise<void>;
25
+ destroy: () => void;
26
+ killWorkloop: () => void;
27
+ }
28
+ declare function createServer(engine: RuntimeEngine, options?: ServerOptions): ServerApp;
29
+
30
+ export { createServer as default };
package/dist/index.js ADDED
@@ -0,0 +1,524 @@
1
+ // src/server.ts
2
+ import Koa from "koa";
3
+ import bodyParser from "koa-bodyparser";
4
+ import koaLogger from "koa-logger";
5
+ import Router from "@koa/router";
6
+ import { humanId } from "human-id";
7
+ import { createMockLogger as createMockLogger2 } from "@openfn/logger";
8
+
9
+ // src/util/try-with-backoff.ts
10
+ var MAX_BACKOFF = 1e3 * 30;
11
+ var tryWithBackoff = (fn, opts = {}) => {
12
+ if (!opts.timeout) {
13
+ opts.timeout = 100;
14
+ }
15
+ if (!opts.attempts) {
16
+ opts.attempts = 1;
17
+ }
18
+ if (!opts.maxBackoff) {
19
+ opts.maxBackoff = MAX_BACKOFF;
20
+ }
21
+ let { timeout, attempts, maxAttempts } = opts;
22
+ timeout = timeout;
23
+ attempts = attempts;
24
+ let cancelled = false;
25
+ if (!opts.isCancelled) {
26
+ opts.isCancelled = () => cancelled;
27
+ }
28
+ const promise = new Promise(async (resolve, reject) => {
29
+ try {
30
+ await fn();
31
+ resolve();
32
+ } catch (e) {
33
+ if (opts.isCancelled()) {
34
+ return resolve();
35
+ }
36
+ if (!isNaN(maxAttempts) && attempts >= maxAttempts) {
37
+ return reject(new Error("max attempts exceeded"));
38
+ }
39
+ setTimeout(() => {
40
+ if (opts.isCancelled()) {
41
+ return resolve();
42
+ }
43
+ const nextOpts = {
44
+ maxAttempts,
45
+ attempts: attempts + 1,
46
+ timeout: Math.min(opts.maxBackoff, timeout * 1.2),
47
+ isCancelled: opts.isCancelled
48
+ };
49
+ tryWithBackoff(fn, nextOpts).then(resolve).catch(reject);
50
+ }, timeout);
51
+ }
52
+ });
53
+ promise.cancel = () => {
54
+ cancelled = true;
55
+ };
56
+ return promise;
57
+ };
58
+ var try_with_backoff_default = tryWithBackoff;
59
+
60
+ // src/api/claim.ts
61
+ import { createMockLogger } from "@openfn/logger";
62
+
63
+ // src/events.ts
64
+ var CLAIM = "claim";
65
+ var GET_ATTEMPT = "fetch:attempt";
66
+ var GET_CREDENTIAL = "fetch:credential";
67
+ var GET_DATACLIP = "fetch:dataclip";
68
+ var ATTEMPT_START = "attempt:start";
69
+ var ATTEMPT_COMPLETE = "attempt:complete";
70
+ var ATTEMPT_LOG = "attempt:log";
71
+ var RUN_START = "run:start";
72
+ var RUN_COMPLETE = "run:complete";
73
+
74
+ // src/api/claim.ts
75
+ var mockLogger = createMockLogger();
76
+ var claim = (channel, execute2, logger = mockLogger) => {
77
+ return new Promise((resolve, reject) => {
78
+ logger.debug("requesting attempt...");
79
+ channel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
80
+ logger.debug(`pulled ${attempts.length} attempts`);
81
+ if (!attempts?.length) {
82
+ return reject(new Error("claim failed"));
83
+ }
84
+ attempts.forEach((attempt) => {
85
+ logger.debug("starting attempt", attempt.id);
86
+ execute2(attempt);
87
+ resolve();
88
+ });
89
+ });
90
+ });
91
+ };
92
+ var claim_default = claim;
93
+
94
+ // src/api/workloop.ts
95
+ var startWorkloop = (channel, execute2, logger, options = {}) => {
96
+ let promise;
97
+ let cancelled = false;
98
+ const workLoop = () => {
99
+ if (!cancelled) {
100
+ promise = try_with_backoff_default(() => claim_default(channel, execute2, logger), {
101
+ timeout: options.timeout,
102
+ maxBackoff: options.maxBackoff
103
+ });
104
+ promise.then(() => {
105
+ if (!cancelled) {
106
+ workLoop();
107
+ }
108
+ });
109
+ }
110
+ };
111
+ workLoop();
112
+ return () => {
113
+ logger.debug("cancelling workloop");
114
+ cancelled = true;
115
+ promise.cancel();
116
+ };
117
+ };
118
+ var workloop_default = startWorkloop;
119
+
120
+ // src/api/execute.ts
121
+ import crypto2 from "node:crypto";
122
+
123
+ // src/util/convert-attempt.ts
124
+ import crypto from "node:crypto";
125
+ var convert_attempt_default = (attempt) => {
126
+ const options = attempt.options || {};
127
+ const plan = {
128
+ id: attempt.id
129
+ };
130
+ if (attempt.dataclip_id) {
131
+ plan.initialState = attempt.dataclip_id;
132
+ }
133
+ if (attempt.starting_node_id) {
134
+ plan.start = attempt.starting_node_id;
135
+ }
136
+ const nodes = {};
137
+ const edges = attempt.edges ?? [];
138
+ if (attempt.triggers?.length) {
139
+ attempt.triggers.forEach((trigger) => {
140
+ const id = trigger.id || "trigger";
141
+ nodes[id] = {
142
+ id
143
+ };
144
+ const connectedEdges = edges.filter((e) => e.source_trigger_id === id);
145
+ if (connectedEdges.length) {
146
+ nodes[id].next = connectedEdges.reduce((obj, edge) => {
147
+ if (edge.enabled !== false) {
148
+ obj[edge.target_job_id] = true;
149
+ }
150
+ return obj;
151
+ }, {});
152
+ } else {
153
+ }
154
+ });
155
+ }
156
+ if (attempt.jobs?.length) {
157
+ attempt.jobs.forEach((job) => {
158
+ const id = job.id || crypto.randomUUID();
159
+ nodes[id] = {
160
+ id,
161
+ configuration: job.credential,
162
+ expression: job.body,
163
+ adaptor: job.adaptor
164
+ };
165
+ if (job.state) {
166
+ nodes[id].state = job.state;
167
+ }
168
+ const next = edges.filter((e) => e.source_job_id === id).reduce((obj, edge) => {
169
+ const newEdge = {};
170
+ if (edge.condition) {
171
+ newEdge.condition = edge.condition;
172
+ }
173
+ if (edge.enabled === false) {
174
+ newEdge.disabled = true;
175
+ }
176
+ obj[edge.target_job_id] = Object.keys(newEdge).length ? newEdge : true;
177
+ return obj;
178
+ }, {});
179
+ if (Object.keys(next).length) {
180
+ nodes[id].next = next;
181
+ }
182
+ });
183
+ }
184
+ plan.jobs = Object.values(nodes);
185
+ return {
186
+ plan,
187
+ options
188
+ };
189
+ };
190
+
191
+ // src/util/get-with-reply.ts
192
+ var get_with_reply_default = (channel, event, payload) => new Promise((resolve) => {
193
+ channel.push(event, payload).receive("ok", (evt) => {
194
+ resolve(evt);
195
+ });
196
+ });
197
+
198
+ // src/util/stringify.ts
199
+ import stringify from "fast-safe-stringify";
200
+ var stringify_default = (obj) => stringify(obj, (_key, value) => {
201
+ if (value instanceof Uint8Array) {
202
+ return Array.from(value);
203
+ }
204
+ return value;
205
+ });
206
+
207
+ // src/api/execute.ts
208
+ var enc = new TextDecoder("utf-8");
209
+ var eventMap = {
210
+ "workflow-start": ATTEMPT_START,
211
+ "job-start": RUN_START,
212
+ "job-complete": RUN_COMPLETE,
213
+ log: ATTEMPT_LOG,
214
+ "workflow-complete": ATTEMPT_COMPLETE
215
+ };
216
+ function execute(channel, engine, logger, plan, options = {}) {
217
+ return new Promise(async (resolve, reject) => {
218
+ logger.info("execute...");
219
+ const state = {
220
+ plan,
221
+ lastDataclipId: plan.initialState,
222
+ dataclips: {},
223
+ options
224
+ };
225
+ const context = { channel, state, logger, onComplete: resolve };
226
+ const addEvent = (eventName, handler) => {
227
+ const wrappedFn = async (event) => {
228
+ const lightningEvent = eventMap[eventName];
229
+ try {
230
+ await handler(context, event);
231
+ logger.info(`${plan.id} :: ${lightningEvent} :: OK`);
232
+ } catch (e) {
233
+ logger.error(
234
+ `${plan.id} :: ${lightningEvent} :: ERR: ${e.message || e.toString()}`
235
+ );
236
+ logger.error(e);
237
+ }
238
+ };
239
+ return {
240
+ [eventName]: wrappedFn
241
+ };
242
+ };
243
+ const listeners = Object.assign(
244
+ {},
245
+ addEvent("workflow-start", onWorkflowStart),
246
+ addEvent("job-start", onJobStart),
247
+ addEvent("job-complete", onJobComplete),
248
+ addEvent("log", onJobLog),
249
+ addEvent("workflow-complete", onWorkflowComplete),
250
+ addEvent("workflow-error", onWorkflowError)
251
+ );
252
+ engine.listen(plan.id, listeners);
253
+ const resolvers = {
254
+ credential: (id) => loadCredential(channel, id)
255
+ };
256
+ if (typeof plan.initialState === "string") {
257
+ logger.debug("loading dataclip", plan.initialState);
258
+ plan.initialState = await loadDataclip(channel, plan.initialState);
259
+ logger.success("dataclip loaded");
260
+ logger.debug(plan.initialState);
261
+ }
262
+ try {
263
+ engine.execute(plan, { resolvers, ...options });
264
+ } catch (e) {
265
+ onWorkflowError(context, { workflowId: plan.id, message: e.message });
266
+ reject(e);
267
+ }
268
+ });
269
+ }
270
+ var sendEvent = (channel, event, payload) => new Promise((resolve, reject) => {
271
+ channel.push(event, payload).receive("error", reject).receive("timeout", () => reject(new Error("timeout"))).receive("ok", resolve);
272
+ });
273
+ function onJobStart({ channel, state }, event) {
274
+ state.activeRun = crypto2.randomUUID();
275
+ state.activeJob = event.jobId;
276
+ return sendEvent(channel, RUN_START, {
277
+ run_id: state.activeRun,
278
+ job_id: state.activeJob,
279
+ input_dataclip_id: state.lastDataclipId
280
+ });
281
+ }
282
+ function onJobComplete({ channel, state }, event) {
283
+ const dataclipId = crypto2.randomUUID();
284
+ const run_id = state.activeRun;
285
+ const job_id = state.activeJob;
286
+ if (!state.dataclips) {
287
+ state.dataclips = {};
288
+ }
289
+ state.dataclips[dataclipId] = event.state;
290
+ state.lastDataclipId = dataclipId;
291
+ delete state.activeRun;
292
+ delete state.activeJob;
293
+ return sendEvent(channel, RUN_COMPLETE, {
294
+ run_id,
295
+ job_id,
296
+ output_dataclip_id: dataclipId,
297
+ output_dataclip: stringify_default(event.state),
298
+ reason: "success"
299
+ });
300
+ }
301
+ function onWorkflowStart({ channel }, _event) {
302
+ return sendEvent(channel, ATTEMPT_START);
303
+ }
304
+ async function onWorkflowComplete({ state, channel, onComplete }, _event) {
305
+ const result = state.dataclips[state.lastDataclipId];
306
+ await sendEvent(channel, ATTEMPT_COMPLETE, {
307
+ final_dataclip_id: state.lastDataclipId,
308
+ status: "success",
309
+ reason: "ok"
310
+ });
311
+ onComplete(result);
312
+ }
313
+ async function onWorkflowError({ state, channel, onComplete }, event) {
314
+ await sendEvent(channel, ATTEMPT_COMPLETE, {
315
+ reason: "fail",
316
+ final_dataclip_id: state.lastDataclipId,
317
+ message: event.message
318
+ });
319
+ onComplete({});
320
+ }
321
+ function onJobLog({ channel, state }, event) {
322
+ const log = {
323
+ attempt_id: state.plan.id,
324
+ message: event.message,
325
+ source: event.name,
326
+ level: event.level,
327
+ timestamp: event.time || Date.now()
328
+ };
329
+ if (state.activeRun) {
330
+ log.run_id = state.activeRun;
331
+ }
332
+ return sendEvent(channel, ATTEMPT_LOG, log);
333
+ }
334
+ async function loadDataclip(channel, stateId) {
335
+ const result = await get_with_reply_default(channel, GET_DATACLIP, {
336
+ id: stateId
337
+ });
338
+ const str = enc.decode(new Uint8Array(result));
339
+ return JSON.parse(str);
340
+ }
341
+ async function loadCredential(channel, credentialId) {
342
+ return get_with_reply_default(channel, GET_CREDENTIAL, { id: credentialId });
343
+ }
344
+
345
+ // src/channels/attempt.ts
346
+ var joinAttemptChannel = (socket, token, attemptId, logger) => {
347
+ return new Promise((resolve, reject) => {
348
+ let didReceiveOk = false;
349
+ const channelName = `attempt:${attemptId}`;
350
+ logger.debug("connecting to ", channelName);
351
+ const channel = socket.channel(channelName, { token });
352
+ channel.join().receive("ok", async (e) => {
353
+ if (!didReceiveOk) {
354
+ didReceiveOk = true;
355
+ logger.success(`connected to ${channelName}`, e);
356
+ const { plan, options } = await loadAttempt(channel);
357
+ logger.debug("converted attempt as execution plan:", plan);
358
+ resolve({ channel, plan, options });
359
+ }
360
+ }).receive("error", (err) => {
361
+ logger.error(`error connecting to ${channelName}`, err);
362
+ reject(err);
363
+ });
364
+ });
365
+ };
366
+ var attempt_default = joinAttemptChannel;
367
+ async function loadAttempt(channel) {
368
+ const attemptBody = await get_with_reply_default(channel, GET_ATTEMPT);
369
+ return convert_attempt_default(attemptBody);
370
+ }
371
+
372
+ // src/channels/worker-queue.ts
373
+ import { Socket as PhxSocket } from "phoenix";
374
+ import { WebSocket } from "ws";
375
+
376
+ // src/util/worker-token.ts
377
+ import * as jose from "jose";
378
+ var alg = "HS256";
379
+ var generateWorkerToken = async (secret, workerId) => {
380
+ if (!secret) {
381
+ console.warn();
382
+ console.warn("WARNING: Worker Secret not provided!");
383
+ console.warn(
384
+ "This worker will attempt to connect to Lightning with default secret"
385
+ );
386
+ console.warn();
387
+ }
388
+ const encodedSecret = new TextEncoder().encode(secret || "<secret>");
389
+ const claims = {
390
+ worker_id: workerId
391
+ };
392
+ const jwt = await new jose.SignJWT(claims).setProtectedHeader({ alg }).setIssuedAt().setIssuer("urn:example:issuer").setAudience("urn:example:audience").sign(encodedSecret);
393
+ return jwt;
394
+ };
395
+ var worker_token_default = generateWorkerToken;
396
+
397
+ // src/channels/worker-queue.ts
398
+ var connectToWorkerQueue = (endpoint, serverId, secret, SocketConstructor = PhxSocket) => {
399
+ return new Promise(async (done, reject) => {
400
+ const token = await worker_token_default(secret, serverId);
401
+ const socket = new SocketConstructor(endpoint, {
402
+ params: { token },
403
+ transport: WebSocket
404
+ });
405
+ let didOpen = false;
406
+ socket.onOpen(() => {
407
+ didOpen = true;
408
+ const channel = socket.channel("worker:queue");
409
+ channel.join().receive("ok", () => {
410
+ done({ socket, channel });
411
+ }).receive("error", (e) => {
412
+ console.log("ERROR", e);
413
+ }).receive("timeout", (e) => {
414
+ console.log("TIMEOUT", e);
415
+ });
416
+ });
417
+ socket.onError((e) => {
418
+ if (!didOpen) {
419
+ reject(e);
420
+ }
421
+ });
422
+ socket.connect();
423
+ });
424
+ };
425
+ var worker_queue_default = connectToWorkerQueue;
426
+
427
+ // src/server.ts
428
+ var DEFAULT_PORT = 1234;
429
+ function connect(app, engine, logger, options = {}) {
430
+ logger.debug("Connecting to Lightning at", options.lightning);
431
+ worker_queue_default(options.lightning, app.id, options.secret).then(({ socket, channel }) => {
432
+ logger.success("Connected to Lightning at", options.lightning);
433
+ app.socket = socket;
434
+ app.channel = channel;
435
+ if (!options.noLoop) {
436
+ logger.info("Starting workloop");
437
+ app.killWorkloop = workloop_default(channel, app.execute, logger, {
438
+ maxBackoff: options.maxBackoff
439
+ });
440
+ } else {
441
+ logger.break();
442
+ logger.warn("Workloop not starting");
443
+ logger.info("This server will not auto-pull work from lightning.");
444
+ logger.info("You can manually claim by posting to /claim, eg:");
445
+ logger.info(
446
+ ` curl -X POST http://locahost:${options.port || DEFAULT_PORT}/claim`
447
+ );
448
+ logger.break();
449
+ }
450
+ }).catch((e) => {
451
+ logger.error(
452
+ "CRITICAL ERROR: could not connect to lightning at",
453
+ options.lightning
454
+ );
455
+ logger.debug(e);
456
+ app.killWorkloop?.();
457
+ setTimeout(() => {
458
+ connect(app, engine, logger, options);
459
+ }, 1e4);
460
+ });
461
+ }
462
+ function createServer(engine, options = {}) {
463
+ const logger = options.logger || createMockLogger2();
464
+ const port = options.port || DEFAULT_PORT;
465
+ logger.debug("Starting server");
466
+ const app = new Koa();
467
+ app.id = humanId({ separator: "-", capitalize: false });
468
+ const router = new Router();
469
+ app.use(bodyParser());
470
+ app.use(
471
+ koaLogger((str, _args) => {
472
+ logger.debug(str);
473
+ })
474
+ );
475
+ const server = app.listen(port);
476
+ logger.success(`ws-worker ${app.id} listening on ${port}`);
477
+ app.execute = async ({ id, token }) => {
478
+ if (app.socket) {
479
+ const {
480
+ channel: attemptChannel,
481
+ plan,
482
+ options: options2
483
+ } = await attempt_default(app.socket, token, id, logger);
484
+ execute(attemptChannel, engine, logger, plan, options2);
485
+ } else {
486
+ logger.error("No lightning socket established");
487
+ }
488
+ };
489
+ router.post("/claim", async (ctx) => {
490
+ logger.info("triggering claim from POST request");
491
+ return claim_default(app.channel, app.execute, logger).then(() => {
492
+ logger.info("claim complete: 1 attempt claimed");
493
+ ctx.body = "complete";
494
+ ctx.status = 200;
495
+ }).catch(() => {
496
+ logger.info("claim complete: no attempts");
497
+ ctx.body = "no attempts";
498
+ ctx.status = 204;
499
+ });
500
+ });
501
+ app.destroy = () => {
502
+ logger.info("Closing server...");
503
+ server.close();
504
+ app.killWorkloop?.();
505
+ logger.success("Server closed");
506
+ };
507
+ app.use(router.routes());
508
+ if (options.lightning) {
509
+ connect(app, engine, logger, options);
510
+ } else {
511
+ logger.warn("No lightning URL provided");
512
+ }
513
+ app.on = (...args) => {
514
+ return engine.on(...args);
515
+ };
516
+ return app;
517
+ }
518
+ var server_default = createServer;
519
+
520
+ // src/index.ts
521
+ var src_default = server_default;
522
+ export {
523
+ src_default as default
524
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@openfn/ws-worker",
3
+ "version": "0.1.0",
4
+ "description": "A Websocket Worker to connect Lightning to a Runtime Engine",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "author": "Open Function Group <admin@openfn.org>",
8
+ "license": "ISC",
9
+ "dependencies": {
10
+ "@koa/router": "^12.0.0",
11
+ "@types/koa-logger": "^3.1.2",
12
+ "@types/ws": "^8.5.6",
13
+ "fast-safe-stringify": "^2.1.1",
14
+ "human-id": "^4.1.0",
15
+ "jose": "^4.14.6",
16
+ "koa": "^2.13.4",
17
+ "koa-bodyparser": "^4.4.0",
18
+ "koa-logger": "^3.2.1",
19
+ "phoenix": "^1.7.7",
20
+ "ws": "^8.14.1",
21
+ "@openfn/engine-multi": "0.1.0",
22
+ "@openfn/logger": "0.0.18",
23
+ "@openfn/runtime": "0.0.32"
24
+ },
25
+ "devDependencies": {
26
+ "@types/koa": "^2.13.5",
27
+ "@types/koa-bodyparser": "^4.3.10",
28
+ "@types/koa__router": "^12.0.1",
29
+ "@types/node": "^18.15.3",
30
+ "@types/nodemon": "1.19.3",
31
+ "@types/phoenix": "^1.6.2",
32
+ "@types/yargs": "^17.0.12",
33
+ "ava": "5.1.0",
34
+ "nodemon": "3.0.1",
35
+ "ts-node": "^10.9.1",
36
+ "tslib": "^2.4.0",
37
+ "tsup": "^6.2.3",
38
+ "typescript": "^4.6.4",
39
+ "yargs": "^17.6.2",
40
+ "@openfn/lightning-mock": "1.0.1"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "README.md",
45
+ "CHANGELOG.md"
46
+ ],
47
+ "scripts": {
48
+ "test": "pnpm ava --serial",
49
+ "test:types": "pnpm tsc --noEmit --project tsconfig.json",
50
+ "build": "tsup --config ../../tsup.config.js src/index.ts src/mock-worker.ts --no-splitting",
51
+ "build:watch": "pnpm build --watch",
52
+ "start": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" ts-node-esm --transpile-only src/start.ts",
53
+ "start:watch": "nodemon -e ts,js --watch ../runtime-manager/dist --watch ./src --exec 'pnpm start'",
54
+ "pack": "pnpm pack --pack-destination ../../dist"
55
+ }
56
+ }