@sokel-dev/plugin-sdk 0.3.0 → 0.4.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/README.md +61 -46
- package/README.zh-CN.md +95 -0
- package/dist/src/auth.d.ts +12 -8
- package/dist/src/auth.js +2 -9
- package/dist/src/auth.js.map +1 -1
- package/dist/src/contract.d.ts +13 -9
- package/dist/src/contract.js +8 -10
- package/dist/src/contract.js.map +1 -1
- package/dist/src/env.d.ts +1 -6
- package/dist/src/env.js +6 -3
- package/dist/src/env.js.map +1 -1
- package/dist/src/events.d.ts +32 -23
- package/dist/src/events.js +32 -31
- package/dist/src/events.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +6 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/nats.d.ts +13 -15
- package/dist/src/nats.js +70 -54
- package/dist/src/nats.js.map +1 -1
- package/dist/src/plugin.d.ts +24 -19
- package/dist/src/plugin.js +42 -29
- package/dist/src/plugin.js.map +1 -1
- package/dist/src/runtime.d.ts +30 -23
- package/dist/src/runtime.js +28 -25
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/webhook.d.ts +15 -10
- package/dist/src/webhook.js +8 -12
- package/dist/src/webhook.js.map +1 -1
- package/package.json +1 -1
package/dist/src/events.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 与操作的区别:操作是 request/reply(平台调插件),事件是 fire-and-forget(插件推平台)。
|
|
5
|
-
*
|
|
6
|
-
* 多 bot 单实例(协议 v1.3):平台每次注册/心跳下发「分配给本副本的凭证子集」,
|
|
7
|
-
* supervisor 按它 reconcile —— 每个凭证一套源实例,凭证被移除就取消,字段变了就重启。
|
|
8
|
-
*/
|
|
1
|
+
// Copyright 2026 The Sokel Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
9
3
|
import { toVars } from "./runtime.js";
|
|
10
4
|
export const TRIGGER_SUBJECT = "sokel.trigger";
|
|
11
5
|
export const CREDENTIAL_UPDATE_SUBJECT = "sokel.credential.update";
|
|
12
|
-
/**
|
|
6
|
+
/** One entry of the registration reply's credentials list — a bot identity assigned here. */
|
|
13
7
|
export class CredEntry {
|
|
14
8
|
id;
|
|
15
9
|
fields;
|
|
@@ -17,7 +11,7 @@ export class CredEntry {
|
|
|
17
11
|
this.id = id;
|
|
18
12
|
this.fields = fields;
|
|
19
13
|
}
|
|
20
|
-
/**
|
|
14
|
+
/** A stable signature of the fields; reconcile uses it to decide "fields changed -> restart". */
|
|
21
15
|
sig() {
|
|
22
16
|
return Object.keys(this.fields)
|
|
23
17
|
.sort()
|
|
@@ -25,7 +19,8 @@ export class CredEntry {
|
|
|
25
19
|
.join("\n");
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
|
-
/**
|
|
22
|
+
/** Runtime state per source × credential. Reported with each registration/heartbeat so the panel
|
|
23
|
+
* can show every bot. */
|
|
29
24
|
export class StateBoard {
|
|
30
25
|
now;
|
|
31
26
|
m = new Map();
|
|
@@ -41,10 +36,11 @@ export class StateBoard {
|
|
|
41
36
|
this.m.set(`${sourceId}|${credId}`, st);
|
|
42
37
|
}
|
|
43
38
|
/**
|
|
44
|
-
*
|
|
39
|
+
* Overwrite only while the instance is still `running`.
|
|
45
40
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
41
|
+
* A source that reported its own status (auth_required, say) and then returned normally should not
|
|
42
|
+
* have that sentence overwritten on the way out: all the panel would show is "exited", and *why*
|
|
43
|
+
* it exited is the half that matters.
|
|
48
44
|
*/
|
|
49
45
|
setIfRunning(sourceId, credId, status, error = "") {
|
|
50
46
|
const cur = this.m.get(`${sourceId}|${credId}`);
|
|
@@ -63,12 +59,13 @@ export class StateBoard {
|
|
|
63
59
|
: a.source_id.localeCompare(b.source_id));
|
|
64
60
|
}
|
|
65
61
|
}
|
|
66
|
-
/**
|
|
62
|
+
/** The context for a long-running source or a webhook: push events, read and write back
|
|
63
|
+
* credentials, upload attachments, report state. */
|
|
67
64
|
export class SourceCtx {
|
|
68
65
|
credential;
|
|
69
66
|
credentialId;
|
|
70
67
|
sourceId;
|
|
71
|
-
/**
|
|
68
|
+
/** Set to true when reconcile stops this instance. Long-poll loops run `while (!ctx.stopped)`. */
|
|
72
69
|
stopped = false;
|
|
73
70
|
token;
|
|
74
71
|
publish;
|
|
@@ -85,20 +82,21 @@ export class SourceCtx {
|
|
|
85
82
|
this.board = opts.board;
|
|
86
83
|
this.files = opts.files;
|
|
87
84
|
}
|
|
88
|
-
/**
|
|
85
|
+
/** Read the credential into a typed shape (same as Ctx.credentialAs on the operation side). */
|
|
89
86
|
credentialAs() {
|
|
90
87
|
return this.credential;
|
|
91
88
|
}
|
|
92
89
|
/**
|
|
93
|
-
*
|
|
90
|
+
* Push one event (fire-and-forget).
|
|
94
91
|
*
|
|
95
|
-
* event
|
|
96
|
-
*
|
|
97
|
-
*
|
|
92
|
+
* `event` must be a declared event id: a typo fails here rather than turning into a message nobody
|
|
93
|
+
* on the platform claims. That failure mode has no symptoms — the plugin log looks fine and the
|
|
94
|
+
* workflow simply never starts. `eventId` is the idempotency key; the platform deduplicates on
|
|
95
|
+
* (plugin, event, eventId).
|
|
98
96
|
*/
|
|
99
97
|
async trigger(event, eventId, payload) {
|
|
100
98
|
if (this.validEvents.size > 0 && !this.validEvents.has(event)) {
|
|
101
|
-
throw new Error(
|
|
99
|
+
throw new Error(`undeclared event "${event}" — declare it under events in sokel.yaml`);
|
|
102
100
|
}
|
|
103
101
|
const msg = {
|
|
104
102
|
token: this.token,
|
|
@@ -112,18 +110,20 @@ export class SourceCtx {
|
|
|
112
110
|
await this.publish(TRIGGER_SUBJECT, encode(msg));
|
|
113
111
|
}
|
|
114
112
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* Write a patch back to the credential bound to this instance (how a session-style credential
|
|
114
|
+
* refreshes itself while running).
|
|
115
|
+
* The platform is the only store for credentials; a plugin never persists them locally.
|
|
117
116
|
*/
|
|
118
117
|
async updateCredential(patch) {
|
|
119
118
|
if (!this.credentialId)
|
|
120
|
-
throw new Error("
|
|
119
|
+
throw new Error("this source instance has no bound credential to write back to");
|
|
121
120
|
if (Object.keys(patch).length === 0)
|
|
122
121
|
return;
|
|
123
122
|
Object.assign(this.credential, patch);
|
|
124
123
|
await this.publish(CREDENTIAL_UPDATE_SUBJECT, encode({ token: this.token, credential_id: this.credentialId, patch }));
|
|
125
124
|
}
|
|
126
|
-
/**
|
|
125
|
+
/** Report state (an expired session becomes auth_required); it rides the heartbeat and lights up
|
|
126
|
+
* "needs login" in the panel. */
|
|
127
127
|
reportStatus(status, msg = "") {
|
|
128
128
|
this.board?.set(this.sourceId, this.credentialId, status, msg);
|
|
129
129
|
}
|
|
@@ -136,14 +136,14 @@ export class SourceCtx {
|
|
|
136
136
|
if (f.data)
|
|
137
137
|
return f.data;
|
|
138
138
|
if (!this.files)
|
|
139
|
-
throw new Error("
|
|
139
|
+
throw new Error("file runtime not ready");
|
|
140
140
|
return this.files.fetch(f);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
function encode(v) {
|
|
144
144
|
return new TextEncoder().encode(JSON.stringify(v));
|
|
145
145
|
}
|
|
146
|
-
/**
|
|
146
|
+
/** Per-credential supervisor: starts, stops and restarts instances to match the assigned set. */
|
|
147
147
|
export class SourceSupervisor {
|
|
148
148
|
start;
|
|
149
149
|
running = new Map();
|
|
@@ -152,7 +152,7 @@ export class SourceSupervisor {
|
|
|
152
152
|
}
|
|
153
153
|
reconcile(desired) {
|
|
154
154
|
const want = new Map(desired.map((c) => [c.id, c]));
|
|
155
|
-
//
|
|
155
|
+
// Stop: not in the desired set, or its fields changed (stop then start = restart)
|
|
156
156
|
for (const [id, r] of [...this.running]) {
|
|
157
157
|
const c = want.get(id);
|
|
158
158
|
if (c && c.sig() === r.sig)
|
|
@@ -160,7 +160,7 @@ export class SourceSupervisor {
|
|
|
160
160
|
r.stop();
|
|
161
161
|
this.running.delete(id);
|
|
162
162
|
}
|
|
163
|
-
//
|
|
163
|
+
// Start: desired but not running
|
|
164
164
|
for (const [id, c] of want) {
|
|
165
165
|
if (this.running.has(id))
|
|
166
166
|
continue;
|
|
@@ -173,7 +173,8 @@ export class SourceSupervisor {
|
|
|
173
173
|
this.running.clear();
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
/**
|
|
176
|
+
/** Empty (a plugin with no credentials) becomes one bare instance, so both cases take the same
|
|
177
|
+
* code path. */
|
|
177
178
|
export function desiredSourceCreds(creds) {
|
|
178
179
|
return creds.length > 0 ? creds : [new CredEntry()];
|
|
179
180
|
}
|
package/dist/src/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sokel plugin SDK for Node.js / TypeScript.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The contract is declared in a language-neutral sokel.yaml; `sokel-gen generate` turns it into
|
|
5
|
+
* typed interfaces and registration functions. This package is the runtime: registration handshake,
|
|
6
|
+
* heartbeat, call dispatch, chunked file transfer, event triggering, webhooks and collaborative
|
|
7
|
+
* authentication.
|
|
6
8
|
*
|
|
7
9
|
* ```ts
|
|
8
10
|
* import { Plugin } from "@sokel-dev/plugin-sdk";
|
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
// Copyright 2026 The Sokel Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Sokel plugin SDK for Node.js / TypeScript.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
+
* The contract is declared in a language-neutral sokel.yaml; `sokel-gen generate` turns it into
|
|
7
|
+
* typed interfaces and registration functions. This package is the runtime: registration handshake,
|
|
8
|
+
* heartbeat, call dispatch, chunked file transfer, event triggering, webhooks and collaborative
|
|
9
|
+
* authentication.
|
|
6
10
|
*
|
|
7
11
|
* ```ts
|
|
8
12
|
* import { Plugin } from "@sokel-dev/plugin-sdk";
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAErG,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/src/nats.d.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NATS 承载:插件**出站**连 broker(无入站端口、无公网 IP、无防火墙洞)。
|
|
3
|
-
*
|
|
4
|
-
* 流程与 Go / Python SDK 逐条对齐(协议 §1-§7):发现 → 连接 → 注册(上报契约)→
|
|
5
|
-
* 订阅(队列组)→ 心跳续约 → 分发调用 → 优雅下线。事件源插件另有 per-credential supervisor。
|
|
6
|
-
*/
|
|
7
1
|
import type { NatsConnection } from "nats";
|
|
8
2
|
import type { Plugin } from "./plugin.js";
|
|
9
3
|
import type { FileRuntime, SokelFile } from "./runtime.js";
|
|
10
|
-
/**
|
|
4
|
+
/** Replicas of a group share one queue: each call goes to exactly one of them. */
|
|
11
5
|
export declare const QUEUE_GROUP = "sokel-workers";
|
|
12
|
-
/**
|
|
6
|
+
/** Replies and stream frames report which replica answered. */
|
|
13
7
|
export declare const INSTANCE_HEADER = "Sokel-Instance";
|
|
14
|
-
/**
|
|
8
|
+
/** Exchange file bytes with the platform over the same NATS connection. The plugin never needs HTTP
|
|
9
|
+
* access to the platform, so a plugin behind NAT works the same way. */
|
|
15
10
|
export declare class NatsFiles implements FileRuntime {
|
|
16
11
|
private readonly nc;
|
|
17
12
|
private readonly token;
|
|
18
13
|
constructor(nc: NatsConnection, token: string);
|
|
19
14
|
fetch(f: SokelFile): Promise<Uint8Array>;
|
|
20
|
-
/**
|
|
15
|
+
/** Whole bytes. Delegates to storeStream: the chunking protocol should have one implementation. */
|
|
21
16
|
store(name: string, mime: string, data: Uint8Array): Promise<SokelFile>;
|
|
22
|
-
/**
|
|
17
|
+
/** **Stream while reading**; memory stays at one chunk. The platform already writes into blob
|
|
18
|
+
* storage chunk by chunk. */
|
|
23
19
|
storeStream(name: string, mime: string, src: AsyncIterable<Uint8Array>): Promise<SokelFile>;
|
|
24
20
|
private putChunk;
|
|
25
21
|
}
|
|
@@ -28,12 +24,14 @@ export declare class NatsTransport {
|
|
|
28
24
|
private dispatch;
|
|
29
25
|
}
|
|
30
26
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* A single https endpoint becomes the real transport address via the platform's /connect-info.
|
|
28
|
+
* A literal nats:// or tls:// URL skips discovery (local development, offline setups).
|
|
33
29
|
*/
|
|
34
30
|
export declare function discover(endpoint: string, token: string): Promise<string>;
|
|
35
31
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* A stable replica identity, reused across restarts. Without it every restart leaves the platform
|
|
33
|
+
* holding another ghost row that stays offline forever.
|
|
34
|
+
* In order: 1) SOKEL_INSTANCE_ID; 2) a file in the working directory named after the token's
|
|
35
|
+
* fingerprint; 3) if writing that file fails, host-pid.
|
|
38
36
|
*/
|
|
39
37
|
export declare function stableInstanceId(token: string): string;
|
package/dist/src/nats.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
// Copyright 2026 The Sokel Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
|
-
* NATS
|
|
4
|
+
* The NATS transport: the plugin **dials out** to the broker (no inbound port, no public IP, no
|
|
5
|
+
* firewall hole).
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
7
|
+
* The flow matches the Go and Python SDKs step for step (protocol §1-§7): discover, connect,
|
|
8
|
+
* register (reporting the contract), subscribe (as a queue group), renew by heartbeat, dispatch
|
|
9
|
+
* calls, shut down gracefully. Event-source plugins additionally run a per-credential supervisor.
|
|
6
10
|
*/
|
|
7
11
|
import { connect, headers as natsHeaders } from "nats";
|
|
8
12
|
import { hostname } from "node:os";
|
|
@@ -11,11 +15,11 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
11
15
|
import { OP_WEBHOOK } from "./contract.js";
|
|
12
16
|
import { env } from "./env.js";
|
|
13
17
|
import { CredEntry, SourceCtx, SourceSupervisor, desiredSourceCreds } from "./events.js";
|
|
14
|
-
/**
|
|
18
|
+
/** Replicas of a group share one queue: each call goes to exactly one of them. */
|
|
15
19
|
export const QUEUE_GROUP = "sokel-workers";
|
|
16
|
-
/**
|
|
20
|
+
/** Replies and stream frames report which replica answered. */
|
|
17
21
|
export const INSTANCE_HEADER = "Sokel-Instance";
|
|
18
|
-
/** 1 MiB
|
|
22
|
+
/** 1 MiB. Bytes never ride the operation reply (max_payload); they go through the chunk channel. */
|
|
19
23
|
const FILE_CHUNK = 1 << 20;
|
|
20
24
|
const HEARTBEAT_MS = 20_000;
|
|
21
25
|
const RETRY_MS = 8_000;
|
|
@@ -23,7 +27,8 @@ const REQUEST_TIMEOUT_MS = 8_000;
|
|
|
23
27
|
const FILE_TIMEOUT_MS = 30_000;
|
|
24
28
|
const enc = new TextEncoder();
|
|
25
29
|
const dec = new TextDecoder();
|
|
26
|
-
/**
|
|
30
|
+
/** Exchange file bytes with the platform over the same NATS connection. The plugin never needs HTTP
|
|
31
|
+
* access to the platform, so a plugin behind NAT works the same way. */
|
|
27
32
|
export class NatsFiles {
|
|
28
33
|
nc;
|
|
29
34
|
token;
|
|
@@ -34,7 +39,7 @@ export class NatsFiles {
|
|
|
34
39
|
async fetch(f) {
|
|
35
40
|
const id = f.id || (f.url ? f.url.split("/").pop() : "");
|
|
36
41
|
if (!id)
|
|
37
|
-
throw new Error("
|
|
42
|
+
throw new Error("the file reference has neither id nor url");
|
|
38
43
|
const chunks = [];
|
|
39
44
|
for (let seq = 0;; seq++) {
|
|
40
45
|
const resp = await this.nc.request("sokel.file.get", enc.encode(JSON.stringify({ token: this.token, id, seq })), { timeout: FILE_TIMEOUT_MS });
|
|
@@ -46,19 +51,20 @@ export class NatsFiles {
|
|
|
46
51
|
return Buffer.concat(chunks);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
/**
|
|
54
|
+
/** Whole bytes. Delegates to storeStream: the chunking protocol should have one implementation. */
|
|
50
55
|
async store(name, mime, data) {
|
|
51
56
|
return this.storeStream(name, mime, (async function* () { yield data; })());
|
|
52
57
|
}
|
|
53
|
-
/**
|
|
58
|
+
/** **Stream while reading**; memory stays at one chunk. The platform already writes into blob
|
|
59
|
+
* storage chunk by chunk. */
|
|
54
60
|
async storeStream(name, mime, src) {
|
|
55
61
|
let uploadId = "";
|
|
56
62
|
let seq = 0;
|
|
57
63
|
let pending = Buffer.alloc(0);
|
|
58
64
|
let done = false;
|
|
59
65
|
const it = src[Symbol.asyncIterator]();
|
|
60
|
-
//
|
|
61
|
-
//
|
|
66
|
+
// Fill a whole chunk before sending: the source decides its own slice size (a fs stream gives
|
|
67
|
+
// 64KB by default), and forwarding those as-is would multiply the round trips more than tenfold.
|
|
62
68
|
while (!done) {
|
|
63
69
|
while (pending.length < FILE_CHUNK) {
|
|
64
70
|
const r = await it.next();
|
|
@@ -76,12 +82,12 @@ export class NatsFiles {
|
|
|
76
82
|
uploadId = f.uploadId;
|
|
77
83
|
if (last) {
|
|
78
84
|
if (!f.file)
|
|
79
|
-
throw new Error("
|
|
85
|
+
throw new Error("the platform returned no file reference");
|
|
80
86
|
return f.file;
|
|
81
87
|
}
|
|
82
88
|
seq += 1;
|
|
83
89
|
}
|
|
84
|
-
throw new Error("
|
|
90
|
+
throw new Error("the upload never got a final-chunk reply (should not happen)");
|
|
85
91
|
}
|
|
86
92
|
async putChunk(name, mime, uploadId, seq, last, chunk) {
|
|
87
93
|
{
|
|
@@ -106,16 +112,17 @@ export class NatsFiles {
|
|
|
106
112
|
export class NatsTransport {
|
|
107
113
|
async run(p) {
|
|
108
114
|
const target = await discover(p.endpoint, p.token);
|
|
109
|
-
//
|
|
115
|
+
// Transport-level auth prefers SOKEL_NATS_TOKEN and falls back to the access token; a broker
|
|
116
|
+
// without auth ignores it either way.
|
|
110
117
|
const token = env("NATS_TOKEN") || p.token;
|
|
111
118
|
const ca = env("NATS_CA");
|
|
112
119
|
const nc = await connectForever({
|
|
113
120
|
servers: [target],
|
|
114
121
|
name: p.name,
|
|
115
122
|
token: token || undefined,
|
|
116
|
-
maxReconnectAttempts: -1, //
|
|
123
|
+
maxReconnectAttempts: -1, // reconnect forever; subscriptions restore themselves
|
|
117
124
|
reconnectTimeWait: 2_000,
|
|
118
|
-
waitOnFirstConnect: true, // broker
|
|
125
|
+
waitOnFirstConnect: true, // wait for a broker that is not up yet instead of exiting
|
|
119
126
|
...(ca ? { tls: { caFile: ca } } : {}),
|
|
120
127
|
});
|
|
121
128
|
const host = hostname();
|
|
@@ -130,17 +137,18 @@ export class NatsTransport {
|
|
|
130
137
|
});
|
|
131
138
|
const reg = JSON.parse(dec.decode(resp.data));
|
|
132
139
|
if (!reg.ok || !reg.subject)
|
|
133
|
-
throw new Error(
|
|
140
|
+
throw new Error(`registration refused: ${reg.error ?? "the platform returned no subject"}`);
|
|
134
141
|
if (reg.notify_subject)
|
|
135
142
|
notifySubject = reg.notify_subject;
|
|
136
143
|
let creds = (reg.credentials ?? []).map((c) => new CredEntry(c.id ?? "", c.fields ?? {}));
|
|
137
|
-
//
|
|
144
|
+
// An older platform only sends the singular form; fold it into a one-element set.
|
|
138
145
|
if (creds.length === 0 && (reg.credential_id || reg.credential)) {
|
|
139
146
|
creds = [new CredEntry(reg.credential_id ?? "", reg.credential ?? {})];
|
|
140
147
|
}
|
|
141
148
|
return { subject: reg.subject, name: reg.name || p.name, creds };
|
|
142
149
|
};
|
|
143
|
-
//
|
|
150
|
+
// A failed first registration is not fatal (broker or platform may still be starting): retry at
|
|
151
|
+
// a fixed interval until it succeeds.
|
|
144
152
|
let first;
|
|
145
153
|
for (;;) {
|
|
146
154
|
try {
|
|
@@ -148,7 +156,7 @@ export class NatsTransport {
|
|
|
148
156
|
break;
|
|
149
157
|
}
|
|
150
158
|
catch (e) {
|
|
151
|
-
console.warn(`[sokel]
|
|
159
|
+
console.warn(`[sokel] registration failed (${errText(e)}), retrying in ${RETRY_MS / 1000}s…`);
|
|
152
160
|
await sleep(RETRY_MS);
|
|
153
161
|
}
|
|
154
162
|
}
|
|
@@ -158,20 +166,21 @@ export class NatsTransport {
|
|
|
158
166
|
void this.dispatch(p, nc, msg, files, instanceId);
|
|
159
167
|
}
|
|
160
168
|
})();
|
|
161
|
-
console.log(`[sokel]
|
|
169
|
+
console.log(`[sokel] connected: plugin "${first.name}" ready, replica ${instanceId} listening on ${first.subject}`);
|
|
162
170
|
let supervisor;
|
|
163
171
|
if (p.sources.length > 0) {
|
|
164
172
|
supervisor = makeSupervisor(p, nc, files);
|
|
165
173
|
supervisor.reconcile(desiredSourceCreds(first.creds));
|
|
166
174
|
if (notifySubject) {
|
|
167
|
-
//
|
|
175
|
+
// Credential-change notifications use a plain subscription (not a queue group): every
|
|
176
|
+
// replica in the group must hear it, since any assignment may have changed.
|
|
168
177
|
const debounced = debounce(300, async () => {
|
|
169
178
|
try {
|
|
170
179
|
const { creds } = await register();
|
|
171
180
|
supervisor.reconcile(desiredSourceCreds(creds));
|
|
172
181
|
}
|
|
173
182
|
catch (e) {
|
|
174
|
-
console.warn(`[sokel]
|
|
183
|
+
console.warn(`[sokel] re-register after a credential change failed: ${errText(e)}`);
|
|
175
184
|
}
|
|
176
185
|
});
|
|
177
186
|
const notifySub = nc.subscribe(notifySubject);
|
|
@@ -181,17 +190,19 @@ export class NatsTransport {
|
|
|
181
190
|
})();
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
|
-
//
|
|
185
|
-
//
|
|
193
|
+
// The heartbeat keeps the replica online. On SIGINT/SIGTERM (docker stop, Ctrl-C) it shuts down
|
|
194
|
+
// gracefully: tell the platform to mark this replica offline right away (seconds), instead of
|
|
195
|
+
// waiting for the heartbeat sweep (45s+).
|
|
186
196
|
await new Promise((resolve) => {
|
|
187
197
|
const timer = setInterval(async () => {
|
|
188
198
|
try {
|
|
189
199
|
const { creds } = await register();
|
|
190
|
-
//
|
|
200
|
+
// Reconcile against the latest assignment each tick: shard moves, credentials added or
|
|
201
|
+
// removed, fields refreshed.
|
|
191
202
|
supervisor?.reconcile(desiredSourceCreds(creds));
|
|
192
203
|
}
|
|
193
204
|
catch (e) {
|
|
194
|
-
console.warn(`[sokel]
|
|
205
|
+
console.warn(`[sokel] heartbeat renewal failed: ${errText(e)}`);
|
|
195
206
|
}
|
|
196
207
|
}, HEARTBEAT_MS);
|
|
197
208
|
const bye = (sig) => {
|
|
@@ -199,11 +210,11 @@ export class NatsTransport {
|
|
|
199
210
|
supervisor?.stopAll();
|
|
200
211
|
nc.publish("sokel.unregister", enc.encode(JSON.stringify({ token: p.token, instance_id: instanceId })));
|
|
201
212
|
void nc
|
|
202
|
-
.flush() //
|
|
213
|
+
.flush() // make sure the goodbye lands before the connection drops
|
|
203
214
|
.then(() => nc.drain())
|
|
204
215
|
.catch(() => undefined)
|
|
205
216
|
.then(() => {
|
|
206
|
-
console.log(`[sokel]
|
|
217
|
+
console.log(`[sokel] got ${sig}, told the platform we are going offline, exiting`);
|
|
207
218
|
resolve();
|
|
208
219
|
});
|
|
209
220
|
};
|
|
@@ -219,15 +230,16 @@ export class NatsTransport {
|
|
|
219
230
|
call = JSON.parse(dec.decode(msg.data));
|
|
220
231
|
}
|
|
221
232
|
catch {
|
|
222
|
-
nc.publish(msg.reply, enc.encode(JSON.stringify({ error: "
|
|
233
|
+
nc.publish(msg.reply, enc.encode(JSON.stringify({ error: "could not parse the call frame" })));
|
|
223
234
|
return;
|
|
224
235
|
}
|
|
225
236
|
const op = call.operation ?? "";
|
|
226
237
|
const tag = traceTag(call.trace ?? {});
|
|
227
|
-
//
|
|
228
|
-
//
|
|
238
|
+
// The platform-relayed webhook frame is intercepted before dispatch. An older SDK without this
|
|
239
|
+
// branch falls through to "unknown operation", which the platform translates into "the plugin
|
|
240
|
+
// registered no webhook handler".
|
|
229
241
|
if (op === OP_WEBHOOK) {
|
|
230
|
-
console.log(`[sokel] ← webhook
|
|
242
|
+
console.log(`[sokel] ← webhook inbound${tag}`);
|
|
231
243
|
const sctx = new SourceCtx({
|
|
232
244
|
token: p.token,
|
|
233
245
|
publish: (subject, data) => nc.publish(subject, data),
|
|
@@ -244,16 +256,16 @@ export class NatsTransport {
|
|
|
244
256
|
const h = natsHeaders();
|
|
245
257
|
h.set(INSTANCE_HEADER, instanceId);
|
|
246
258
|
const started = Date.now();
|
|
247
|
-
console.log(`[sokel] ← ${op}
|
|
259
|
+
console.log(`[sokel] ← ${op} started${tag}`);
|
|
248
260
|
if (p.contract.isStream(op)) {
|
|
249
|
-
//
|
|
261
|
+
// Streaming: publish frame by frame to the reply subject; the end frame is mandatory.
|
|
250
262
|
const publishFrame = (f) => nc.publish(msg.reply, enc.encode(JSON.stringify(f)), { headers: h });
|
|
251
263
|
try {
|
|
252
264
|
await p.dispatch(call, publishFrame, files);
|
|
253
|
-
console.log(`[sokel] ✓ ${op}
|
|
265
|
+
console.log(`[sokel] ✓ ${op} done (${Date.now() - started}ms)${tag}`);
|
|
254
266
|
}
|
|
255
267
|
catch (e) {
|
|
256
|
-
console.warn(`[sokel] ✗ ${op}
|
|
268
|
+
console.warn(`[sokel] ✗ ${op} failed (${Date.now() - started}ms)${tag}: ${errText(e)}`);
|
|
257
269
|
publishFrame({ kind: "error", text: errText(e) });
|
|
258
270
|
}
|
|
259
271
|
publishFrame({ kind: "end" });
|
|
@@ -261,16 +273,17 @@ export class NatsTransport {
|
|
|
261
273
|
}
|
|
262
274
|
try {
|
|
263
275
|
const vars = await p.dispatchBuffered(call, files);
|
|
264
|
-
console.log(`[sokel] ✓ ${op}
|
|
276
|
+
console.log(`[sokel] ✓ ${op} done (${Date.now() - started}ms)${tag}`);
|
|
265
277
|
nc.publish(msg.reply, enc.encode(JSON.stringify(vars)), { headers: h });
|
|
266
278
|
}
|
|
267
279
|
catch (e) {
|
|
268
|
-
console.warn(`[sokel] ✗ ${op}
|
|
280
|
+
console.warn(`[sokel] ✗ ${op} failed (${Date.now() - started}ms)${tag}: ${errText(e)}`);
|
|
269
281
|
nc.publish(msg.reply, enc.encode(JSON.stringify({ error: errText(e) })), { headers: h });
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
}
|
|
273
|
-
/**
|
|
285
|
+
/** One source instance per credential: its ctx is bound to that credential, and trigger carries the
|
|
286
|
+
* credential_id back automatically. */
|
|
274
287
|
function makeSupervisor(p, nc, files) {
|
|
275
288
|
return new SourceSupervisor((cred) => {
|
|
276
289
|
const ctxs = [];
|
|
@@ -286,7 +299,7 @@ function makeSupervisor(p, nc, files) {
|
|
|
286
299
|
files,
|
|
287
300
|
});
|
|
288
301
|
ctxs.push(sctx);
|
|
289
|
-
console.log(`[sokel]
|
|
302
|
+
console.log(`[sokel] event source "${src.id}" started (credential=${cred.id || "(none)"})`);
|
|
290
303
|
p.board.set(src.id, cred.id, "running");
|
|
291
304
|
void src
|
|
292
305
|
.fn(sctx)
|
|
@@ -296,13 +309,14 @@ function makeSupervisor(p, nc, files) {
|
|
|
296
309
|
})
|
|
297
310
|
.catch((e) => {
|
|
298
311
|
if (sctx.stopped)
|
|
299
|
-
return; // reconcile
|
|
300
|
-
console.warn(`[sokel]
|
|
312
|
+
return; // stopped by reconcile: stop() already removed the state
|
|
313
|
+
console.warn(`[sokel] event source "${src.id}" exited (credential=${cred.id || "(none)"}): ${errText(e)}`);
|
|
301
314
|
p.board.set(src.id, cred.id, "error", errText(e));
|
|
302
315
|
});
|
|
303
316
|
}
|
|
304
317
|
return () => {
|
|
305
|
-
// JS
|
|
318
|
+
// JS has no task cancellation: by convention the source loop watches ctx.stopped and exits
|
|
319
|
+
// (a long poll notices on its next tick).
|
|
306
320
|
for (const c of ctxs)
|
|
307
321
|
c.stopped = true;
|
|
308
322
|
p.board.removeCred(cred.id);
|
|
@@ -310,29 +324,31 @@ function makeSupervisor(p, nc, files) {
|
|
|
310
324
|
});
|
|
311
325
|
}
|
|
312
326
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
327
|
+
* A single https endpoint becomes the real transport address via the platform's /connect-info.
|
|
328
|
+
* A literal nats:// or tls:// URL skips discovery (local development, offline setups).
|
|
315
329
|
*/
|
|
316
330
|
export async function discover(endpoint, token) {
|
|
317
331
|
const ep = (endpoint ?? "").trim();
|
|
318
332
|
if (ep.startsWith("nats://") || ep.startsWith("tls://"))
|
|
319
333
|
return ep;
|
|
320
334
|
if (!ep.startsWith("http://") && !ep.startsWith("https://")) {
|
|
321
|
-
throw new Error(
|
|
335
|
+
throw new Error(`invalid endpoint "${endpoint}": expected a platform URL (https://…) or nats://`);
|
|
322
336
|
}
|
|
323
337
|
const url = ep.replace(/\/+$/, "") + "/api/v1/connect-info";
|
|
324
338
|
const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
325
339
|
if (!resp.ok)
|
|
326
|
-
throw new Error(
|
|
340
|
+
throw new Error(`discovery failed at ${url}: HTTP ${resp.status}`);
|
|
327
341
|
const info = (await resp.json());
|
|
328
342
|
const target = info.transports?.nats;
|
|
329
343
|
if (!target)
|
|
330
|
-
throw new Error("
|
|
344
|
+
throw new Error("the platform offers no transport (connect-info.transports is empty)");
|
|
331
345
|
return target;
|
|
332
346
|
}
|
|
333
347
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
348
|
+
* A stable replica identity, reused across restarts. Without it every restart leaves the platform
|
|
349
|
+
* holding another ghost row that stays offline forever.
|
|
350
|
+
* In order: 1) SOKEL_INSTANCE_ID; 2) a file in the working directory named after the token's
|
|
351
|
+
* fingerprint; 3) if writing that file fails, host-pid.
|
|
336
352
|
*/
|
|
337
353
|
export function stableInstanceId(token) {
|
|
338
354
|
const explicit = env("INSTANCE_ID");
|
|
@@ -347,7 +363,7 @@ export function stableInstanceId(token) {
|
|
|
347
363
|
return existing;
|
|
348
364
|
}
|
|
349
365
|
catch {
|
|
350
|
-
/*
|
|
366
|
+
/* first run: fall through, generate one and write it out */
|
|
351
367
|
}
|
|
352
368
|
const id = `${hostname()}-${randomBytes(4).toString("hex")}`;
|
|
353
369
|
try {
|
|
@@ -364,7 +380,7 @@ async function connectForever(opts) {
|
|
|
364
380
|
return await connect(opts);
|
|
365
381
|
}
|
|
366
382
|
catch (e) {
|
|
367
|
-
console.warn(`[sokel]
|
|
383
|
+
console.warn(`[sokel] could not connect to the platform (${errText(e)}), retrying in ${RETRY_MS / 1000}s…`);
|
|
368
384
|
await sleep(RETRY_MS);
|
|
369
385
|
}
|
|
370
386
|
}
|