lightnode-sdk 0.4.9 → 0.5.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/dist/worker.js ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * SDK-level worker preflight + watch.
3
+ *
4
+ * The desktop app already has local worker-health parsers (parseWorkerHealth,
5
+ * parseSpeedTest) that need shell access to the running container. Those are
6
+ * great for operators ON the worker box, but useless for a builder watching
7
+ * the network from anywhere else.
8
+ *
9
+ * This module fills the remote-only gap:
10
+ * - `preflight()` runs ONE real test inference against the live network
11
+ * and returns a verdict (works / over-deadline / failed). This is the
12
+ * "test job before joining a worker pool" the community has been asking
13
+ * for; works from any machine with a funded wallet.
14
+ * - `watch()` polls a worker's on-chain + indexer status on a fixed
15
+ * interval and yields an event each time the status meaningfully
16
+ * changes (registered <-> deregistered, last-seen went stale, completion
17
+ * rate dropped). Suitable for a CI gate or a `cron` job.
18
+ *
19
+ * Both are pure SDK calls. No Docker, no SSH, no privileged access.
20
+ */
21
+ import { runInferenceWithKey } from "./inference.js";
22
+ import { isStalledWorker } from "./errors.js";
23
+ const DEFAULT_PROMPT = "Reply with the single word OK.";
24
+ /**
25
+ * Run one real encrypted inference against the live network and classify
26
+ * the result. Useful as a CI gate ("did the wallet survive a real call this
27
+ * deploy?") or as a pre-join check for a worker operator who wants to test
28
+ * the protocol path before staking.
29
+ */
30
+ export async function preflight(args) {
31
+ const deadlineMs = args.deadlineMs ?? 60000;
32
+ const prompt = args.prompt ?? DEFAULT_PROMPT;
33
+ const t0 = Date.now();
34
+ try {
35
+ const r = await runInferenceWithKey({ ...args, prompt });
36
+ const elapsedMs = Date.now() - t0;
37
+ const verdict = elapsedMs > deadlineMs ? "over-deadline" : "ok";
38
+ return {
39
+ verdict,
40
+ elapsedMs,
41
+ summary: verdict === "ok"
42
+ ? `OK in ${(elapsedMs / 1000).toFixed(1)}s. Worker ${r.worker} replied with ${r.answer.length} chars.`
43
+ : `Answer arrived but took ${(elapsedMs / 1000).toFixed(1)}s, over the ${(deadlineMs / 1000).toFixed(0)}s deadline.`,
44
+ answer: r.answer,
45
+ worker: r.worker,
46
+ txs: r.txs,
47
+ error: null,
48
+ };
49
+ }
50
+ catch (e) {
51
+ const elapsedMs = Date.now() - t0;
52
+ if (isStalledWorker(e)) {
53
+ return {
54
+ verdict: "stalled",
55
+ elapsedMs,
56
+ summary: "All retry attempts stalled (workers never produced an answer). Protocol refunds the fees automatically.",
57
+ answer: "",
58
+ worker: null,
59
+ txs: { createSession: null, submitJob: null, jobCompleted: null },
60
+ error: e.message,
61
+ };
62
+ }
63
+ return {
64
+ verdict: "failed",
65
+ elapsedMs,
66
+ summary: `Test inference failed: ${e.message}`,
67
+ answer: "",
68
+ worker: null,
69
+ txs: { createSession: null, submitJob: null, jobCompleted: null },
70
+ error: e.message,
71
+ };
72
+ }
73
+ }
74
+ /**
75
+ * Poll one worker's on-chain + indexer status and yield an event each time
76
+ * something meaningful changes (registration, staleness, completed-jobs
77
+ * counter, earnings). Runs until `handle.stop()` is called or `maxEvents`
78
+ * is reached.
79
+ */
80
+ export function watch(ln, address, opts = {}) {
81
+ const intervalMs = opts.intervalMs ?? 30000;
82
+ const staleSecs = opts.staleSecs ?? 90;
83
+ const maxEvents = opts.maxEvents ?? Number.POSITIVE_INFINITY;
84
+ const networkId = ln.network.label.toLowerCase().includes("mainnet") ? "mainnet" : "testnet";
85
+ const queue = [];
86
+ const waiters = [];
87
+ let stopped = false;
88
+ let eventCount = 0;
89
+ let prevReg = null;
90
+ let prevStale = null;
91
+ let prevJobs = null;
92
+ let prevEarn = null;
93
+ let timer = null;
94
+ const push = (kind, state) => {
95
+ const event = { kind, at: Date.now(), worker: address, network: networkId, state };
96
+ eventCount++;
97
+ if (waiters.length > 0) {
98
+ const resolve = waiters.shift();
99
+ if (resolve)
100
+ resolve({ value: event, done: false });
101
+ }
102
+ else {
103
+ queue.push(event);
104
+ }
105
+ if (eventCount >= maxEvents)
106
+ stop();
107
+ };
108
+ const stop = () => {
109
+ if (stopped)
110
+ return;
111
+ stopped = true;
112
+ if (timer)
113
+ clearTimeout(timer);
114
+ timer = null;
115
+ while (waiters.length > 0) {
116
+ const resolve = waiters.shift();
117
+ if (resolve)
118
+ resolve({ value: undefined, done: true });
119
+ }
120
+ };
121
+ const poll = async () => {
122
+ if (stopped)
123
+ return;
124
+ try {
125
+ const [reg, worker] = await Promise.all([ln.isRegistered(address), ln.getWorker(address)]);
126
+ const now = Math.floor(Date.now() / 1000);
127
+ const lastSeenSecsAgo = worker?.last_seen_at != null ? Math.max(0, now - worker.last_seen_at) : null;
128
+ const isStale = lastSeenSecsAgo == null ? false : lastSeenSecsAgo > staleSecs;
129
+ const jobs = worker?.jobs_completed ?? null;
130
+ const earn = worker ? Number(BigInt(worker.total_earned ?? "0")) / 1e18 : 0;
131
+ const state = {
132
+ registered: reg,
133
+ lastSeenSecsAgo,
134
+ jobsCompleted: jobs,
135
+ earningsLcai: earn,
136
+ activeJobs: worker?.active_job_count ?? null,
137
+ isStale,
138
+ };
139
+ // First poll: emit a snapshot so the caller always sees an initial event.
140
+ if (prevReg === null && prevStale === null && prevJobs === null && prevEarn === null) {
141
+ push("snapshot", state);
142
+ }
143
+ else {
144
+ if (prevReg === false && reg === true)
145
+ push("registered", state);
146
+ if (prevReg === true && reg === false)
147
+ push("deregistered", state);
148
+ if (prevStale === false && isStale)
149
+ push("went-stale", state);
150
+ if (prevStale === true && !isStale)
151
+ push("back-online", state);
152
+ if (jobs != null && prevJobs != null && jobs > prevJobs)
153
+ push("jobs-completed", state);
154
+ if (earn > (prevEarn ?? 0) + 1e-9)
155
+ push("earnings-up", state);
156
+ }
157
+ prevReg = reg;
158
+ prevStale = isStale;
159
+ prevJobs = jobs;
160
+ prevEarn = earn;
161
+ }
162
+ catch {
163
+ // Transient indexer / RPC error - do nothing, the next poll will retry.
164
+ }
165
+ if (!stopped)
166
+ timer = setTimeout(poll, intervalMs);
167
+ };
168
+ // Kick off the first poll immediately.
169
+ void poll();
170
+ return {
171
+ events: {
172
+ [Symbol.asyncIterator]() {
173
+ return {
174
+ async next() {
175
+ if (queue.length > 0)
176
+ return { value: queue.shift(), done: false };
177
+ if (stopped)
178
+ return { value: undefined, done: true };
179
+ return new Promise((resolve) => waiters.push(resolve));
180
+ },
181
+ };
182
+ },
183
+ },
184
+ stop,
185
+ };
186
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.4.9",
3
+ "version": "0.5.0",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",