@quark-fw/plugin-threads 0.1.26 → 0.1.27

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/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @quark-fw/plugin-threads
2
+
3
+ Worker processes for Quark apps: every `src/**/*.quark.ts` file is forked as a child process with a light quark context (core + env + redis), and `quark.ee` is bridged between processes over Redis pub/sub, so an event emitted in the web process is received in the workers and vice versa. Design spec: `docs/superpowers/specs/2026-08-22-quark-threads-design.md`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i @quark-fw/plugin-threads
9
+ ```
10
+
11
+ Peer: `@quark-fw/core`. Requires `@quark-fw/plugin-redis` (`dependsOn: ["redis"]`); `@quark-fw/plugin-env` and `@quark-fw/plugin-redis` are loaded inside each worker.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // src/index.ts
17
+ import "@quark-fw/plugin-redis";
18
+ import "@quark-fw/plugin-threads"; // provides quark.threads; workers start in onReady
19
+
20
+ // src/ping.quark.ts — a worker file; runs in its own process
21
+ export const config = { instances: 1 };
22
+
23
+ quark.ee.value.on("ping:send", (n) => {
24
+ quark.ee.value.emit("ping:pong", n * 2); // reaches the web process over Redis
25
+ });
26
+
27
+ // main process
28
+ quark.threads.list.value; // [{ file, instance, pid }]
29
+ await quark.threads.spawn("src/ping.quark.ts");
30
+ await quark.threads.stop("src/ping.quark.ts");
31
+ ```
32
+
33
+ How it works: in `onReady` the plugin globs `src/**/*.quark.ts` (skipping `node_modules` and `frontend`), attaches the event bridge only if at least one file exists (no workers, no Redis requirement), and forks `worker-entry.ts` for each file with `--import tsx`. The worker sets `quark.thread = { file, instance, instances }` before importing the file, reads `export const config = { instances }` and reports it back over IPC (`quark:online`); instance 0 tells the parent how many instances to spawn, so `instances > 1` is a broadcast — every instance receives every event and splits work by `quark.thread.instance`. The bridge publishes each `emit` to the Redis channel `quark:ee` as `{ origin, event, args }` and re-emits incoming messages locally; `lifecycle:init`, `plugin:init`, `atom:update` and non-serializable payloads stay local. The bus also carries `thread:online`, `thread:exit` and `thread:restart`.
34
+
35
+ Supervision: a crashed instance is restarted with backoff `min(1s * 2^n, 30s)`, the counter resets after 60 s of uptime. Shutdown sends `quark:shutdown` over IPC (the worker runs `quark.destroy()`), then `SIGTERM` after 5 s and `SIGKILL` after another 5 s. Outside production a chokidar watcher on `src` restarts the workers of a changed `.quark.ts`, spawns new files and stops deleted ones.
36
+
37
+ Env: `NODE_ENV` — the dev watcher runs unless it is `production`. Redis address comes from `@quark-fw/plugin-redis` (`REDIS_URL`).
38
+
39
+ Part of the [Quark framework](https://www.npmjs.com/package/@quark-fw/core).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quark-fw/plugin-threads",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "main": "src/index.ts",
5
5
  "types": "src/index.ts",
6
6
  "type": "module",
@@ -21,16 +21,16 @@
21
21
  "access": "public"
22
22
  },
23
23
  "dependencies": {
24
- "@quark-fw/atom": "0.1.26",
25
- "@quark-fw/plugin": "0.1.26",
26
- "@quark-fw/plugin-env": "0.1.26",
27
- "@quark-fw/plugin-redis": "0.1.26",
24
+ "@quark-fw/atom": "0.1.27",
25
+ "@quark-fw/plugin": "0.1.27",
26
+ "@quark-fw/plugin-env": "0.1.27",
27
+ "@quark-fw/plugin-redis": "0.1.27",
28
28
  "chokidar": "^5.0.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "ioredis": "^5.9.3"
32
32
  },
33
33
  "peerDependencies": {
34
- "@quark-fw/core": "0.1.26"
34
+ "@quark-fw/core": "0.1.27"
35
35
  }
36
36
  }
package/src/bridge.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import { quitClient } from "./helpers.js";
3
+ import { log as rootLog } from "@quark-fw/core/src/log.js";
4
+
5
+ const log = rootLog.child({ scope: "threads" });
3
6
 
4
7
  const CHANNEL = "quark:ee";
5
8
 
@@ -44,7 +47,7 @@ export async function attachEventBridge(
44
47
  if (msg.origin === origin) return; // эхо собственного publish
45
48
  originalEmit(msg.event, ...msg.args);
46
49
  } catch (e) {
47
- console.error("[threads] bad bridge message", e);
50
+ log.error("bad bridge message", { err: e });
48
51
  }
49
52
  });
50
53
 
@@ -60,7 +63,7 @@ export async function attachEventBridge(
60
63
  args,
61
64
  } satisfies BridgeMessage);
62
65
  void pub.publish(CHANNEL, payload).catch((e) => {
63
- console.error("[threads] publish failed", e);
66
+ log.error("publish failed", { err: e });
64
67
  });
65
68
  } catch {
66
69
  // несериализуемые аргументы — событие остаётся локальным
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@ import { fileURLToPath } from "node:url";
7
7
  import chokidar, { FSWatcher } from "chokidar";
8
8
  import { attachEventBridge, EventBridge } from "./bridge.js";
9
9
  import { ThreadInfo } from "@quark-fw/plugin-threads/src/global.js";
10
+ import { log as rootLog } from "@quark-fw/core/src/log.js";
11
+
12
+ const log = rootLog.child({ scope: "threads" });
10
13
 
11
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
15
  const workerEntry = path.join(__dirname, "worker-entry.ts");
@@ -80,9 +83,12 @@ await quark.registerPlugin({
80
83
  "message",
81
84
  (msg: { type?: string; config?: { instances?: number } }) => {
82
85
  if (msg?.type !== "quark:online") return;
83
- console.log(
84
- `[threads] ${path.relative(process.cwd(), file)} #${instance}/${Number(msg.config?.instances ?? 1)} online (pid ${child.pid})`,
85
- );
86
+ log.info("worker online", {
87
+ file: path.relative(process.cwd(), file),
88
+ instance,
89
+ instances: Number(msg.config?.instances ?? 1),
90
+ pid: child.pid,
91
+ });
86
92
  quark.ee.value.emit("thread:online", file, instance);
87
93
  restarts.delete(`${file}#${instance}`);
88
94
 
@@ -6,6 +6,9 @@ import "@quark-fw/plugin-redis";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { attachEventBridge } from "./bridge.js";
8
8
  import {} from "@quark-fw/plugin-threads/src/global.js";
9
+ import { log as rootLog } from "@quark-fw/core/src/log.js";
10
+
11
+ const log = rootLog.child({ scope: "threads" });
9
12
 
10
13
  const file = process.argv[2]!;
11
14
  const instance = Number(process.argv[3] ?? 0);
@@ -18,7 +21,9 @@ const bridge = await attachEventBridge(quark);
18
21
  process.on("message", (msg) => {
19
22
  if ((msg as { type?: string })?.type === "quark:shutdown") {
20
23
  void (async () => {
21
- await quark.destroy().catch(console.error);
24
+ await quark
25
+ .destroy()
26
+ .catch((e) => log.error("destroy failed", { err: e }));
22
27
  await bridge.close().catch(() => {});
23
28
  process.exit(0);
24
29
  })();