@fiber-pay/runtime 0.1.0-rc.1 → 0.1.0-rc.3

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 CHANGED
@@ -9,6 +9,31 @@ fiber-pay runtime start --daemon --json
9
9
  fiber-pay runtime status --json
10
10
  ```
11
11
 
12
+ ## Single-instance low-CPU defaults
13
+
14
+ Runtime defaults are tuned for lower idle CPU on one node:
15
+
16
+ - `channelPollIntervalMs=5000`
17
+ - `invoicePollIntervalMs=3000`
18
+ - `paymentPollIntervalMs=2000`
19
+ - `peerPollIntervalMs=15000`
20
+ - `healthPollIntervalMs=10000`
21
+ - `includeClosedChannels=false`
22
+ - `jobs.schedulerIntervalMs=1000`
23
+
24
+ If your machine is still busy when idle, start with slower polling explicitly:
25
+
26
+ ```bash
27
+ fiber-pay runtime start \
28
+ --channel-poll-ms 8000 \
29
+ --invoice-poll-ms 5000 \
30
+ --payment-poll-ms 3000 \
31
+ --peer-poll-ms 20000 \
32
+ --health-poll-ms 15000 \
33
+ --include-closed false \
34
+ --json
35
+ ```
36
+
12
37
  ## Manual payment flow (runtime jobs)
13
38
 
14
39
  前置:`jq`、`curl`、两边 profile 已有资金(示例 `rt-a` / `rt-b`)。
package/dist/index.js CHANGED
@@ -375,12 +375,12 @@ function computeRetryDelay(retryCount, policy) {
375
375
  // src/config.ts
376
376
  var defaultRuntimeConfig = {
377
377
  fiberRpcUrl: "http://127.0.0.1:8227",
378
- channelPollIntervalMs: 3e3,
379
- invoicePollIntervalMs: 2e3,
380
- paymentPollIntervalMs: 1e3,
381
- peerPollIntervalMs: 1e4,
382
- healthPollIntervalMs: 5e3,
383
- includeClosedChannels: true,
378
+ channelPollIntervalMs: 5e3,
379
+ invoicePollIntervalMs: 3e3,
380
+ paymentPollIntervalMs: 2e3,
381
+ peerPollIntervalMs: 15e3,
382
+ healthPollIntervalMs: 1e4,
383
+ includeClosedChannels: false,
384
384
  completedItemTtlSeconds: 86400,
385
385
  requestTimeoutMs: 1e4,
386
386
  alerts: [{ type: "stdout" }],
@@ -397,28 +397,41 @@ var defaultRuntimeConfig = {
397
397
  enabled: true,
398
398
  dbPath: resolve(process.cwd(), ".fiber-pay-jobs.db"),
399
399
  maxConcurrentJobs: 5,
400
- schedulerIntervalMs: 500,
400
+ schedulerIntervalMs: 1e3,
401
401
  retryPolicy: defaultPaymentRetryPolicy
402
402
  }
403
403
  };
404
404
  function createRuntimeConfig(input = {}) {
405
405
  const config = {
406
- ...defaultRuntimeConfig,
407
- ...input,
406
+ fiberRpcUrl: input.fiberRpcUrl ?? defaultRuntimeConfig.fiberRpcUrl,
407
+ channelPollIntervalMs: input.channelPollIntervalMs ?? defaultRuntimeConfig.channelPollIntervalMs,
408
+ invoicePollIntervalMs: input.invoicePollIntervalMs ?? defaultRuntimeConfig.invoicePollIntervalMs,
409
+ paymentPollIntervalMs: input.paymentPollIntervalMs ?? defaultRuntimeConfig.paymentPollIntervalMs,
410
+ peerPollIntervalMs: input.peerPollIntervalMs ?? defaultRuntimeConfig.peerPollIntervalMs,
411
+ healthPollIntervalMs: input.healthPollIntervalMs ?? defaultRuntimeConfig.healthPollIntervalMs,
412
+ includeClosedChannels: input.includeClosedChannels ?? defaultRuntimeConfig.includeClosedChannels,
413
+ completedItemTtlSeconds: input.completedItemTtlSeconds ?? defaultRuntimeConfig.completedItemTtlSeconds,
414
+ requestTimeoutMs: input.requestTimeoutMs ?? defaultRuntimeConfig.requestTimeoutMs,
408
415
  proxy: {
409
- ...defaultRuntimeConfig.proxy,
410
- ...input.proxy
416
+ enabled: input.proxy?.enabled ?? defaultRuntimeConfig.proxy.enabled,
417
+ listen: input.proxy?.listen ?? defaultRuntimeConfig.proxy.listen
411
418
  },
412
419
  storage: {
413
- ...defaultRuntimeConfig.storage,
414
- ...input.storage
420
+ stateFilePath: input.storage?.stateFilePath ?? defaultRuntimeConfig.storage.stateFilePath,
421
+ flushIntervalMs: input.storage?.flushIntervalMs ?? defaultRuntimeConfig.storage.flushIntervalMs,
422
+ maxAlertHistory: input.storage?.maxAlertHistory ?? defaultRuntimeConfig.storage.maxAlertHistory
415
423
  },
416
424
  jobs: {
417
- ...defaultRuntimeConfig.jobs,
418
- ...input.jobs,
425
+ enabled: input.jobs?.enabled ?? defaultRuntimeConfig.jobs.enabled,
426
+ dbPath: input.jobs?.dbPath ?? defaultRuntimeConfig.jobs.dbPath,
427
+ maxConcurrentJobs: input.jobs?.maxConcurrentJobs ?? defaultRuntimeConfig.jobs.maxConcurrentJobs,
428
+ schedulerIntervalMs: input.jobs?.schedulerIntervalMs ?? defaultRuntimeConfig.jobs.schedulerIntervalMs,
419
429
  retryPolicy: {
420
- ...defaultRuntimeConfig.jobs.retryPolicy,
421
- ...input.jobs?.retryPolicy
430
+ maxRetries: input.jobs?.retryPolicy?.maxRetries ?? defaultRuntimeConfig.jobs.retryPolicy.maxRetries,
431
+ baseDelayMs: input.jobs?.retryPolicy?.baseDelayMs ?? defaultRuntimeConfig.jobs.retryPolicy.baseDelayMs,
432
+ maxDelayMs: input.jobs?.retryPolicy?.maxDelayMs ?? defaultRuntimeConfig.jobs.retryPolicy.maxDelayMs,
433
+ backoffMultiplier: input.jobs?.retryPolicy?.backoffMultiplier ?? defaultRuntimeConfig.jobs.retryPolicy.backoffMultiplier,
434
+ jitterMs: input.jobs?.retryPolicy?.jitterMs ?? defaultRuntimeConfig.jobs.retryPolicy.jitterMs
422
435
  }
423
436
  },
424
437
  alerts: input.alerts ?? defaultRuntimeConfig.alerts
@@ -757,6 +770,9 @@ var InvoiceTracker = class extends BaseMonitor {
757
770
  async poll() {
758
771
  const tracked = this.store.listTrackedInvoices();
759
772
  for (const invoice of tracked) {
773
+ if (isTerminalInvoiceStatusString(invoice.status)) {
774
+ continue;
775
+ }
760
776
  try {
761
777
  const next = await this.client.getInvoice({ payment_hash: invoice.paymentHash });
762
778
  const previousStatus = invoice.status;
@@ -813,6 +829,9 @@ var InvoiceTracker = class extends BaseMonitor {
813
829
  this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
814
830
  }
815
831
  };
832
+ function isTerminalInvoiceStatusString(status) {
833
+ return status === "Cancelled" || status === "Expired" || status === "Paid";
834
+ }
816
835
 
817
836
  // src/monitors/payment-tracker.ts
818
837
  var PaymentTracker = class extends BaseMonitor {
@@ -833,6 +852,9 @@ var PaymentTracker = class extends BaseMonitor {
833
852
  async poll() {
834
853
  const tracked = this.store.listTrackedPayments();
835
854
  for (const payment of tracked) {
855
+ if (isTerminalPaymentStatus(payment.status)) {
856
+ continue;
857
+ }
836
858
  try {
837
859
  const next = await this.client.getPayment({ payment_hash: payment.paymentHash });
838
860
  const previousStatus = payment.status;
@@ -876,6 +898,9 @@ var PaymentTracker = class extends BaseMonitor {
876
898
  this.store.pruneCompleted(this.config.completedItemTtlSeconds * 1e3);
877
899
  }
878
900
  };
901
+ function isTerminalPaymentStatus(status) {
902
+ return status === "Success" || status === "Failed";
903
+ }
879
904
 
880
905
  // src/diff/peer-diff.ts
881
906
  function diffPeers(previous, current) {
@@ -1332,6 +1357,7 @@ var RpcMonitorProxy = class {
1332
1357
  if (this.server) {
1333
1358
  return;
1334
1359
  }
1360
+ assertNoProxySelfLoop(this.config.listen, this.config.targetUrl);
1335
1361
  this.server = http2.createServer((req, res) => {
1336
1362
  void this.handleRequest(req, res);
1337
1363
  });
@@ -1422,6 +1448,29 @@ var RpcMonitorProxy = class {
1422
1448
  res.end(responseText);
1423
1449
  }
1424
1450
  };
1451
+ function assertNoProxySelfLoop(listen, targetUrl) {
1452
+ const { host, port } = parseListenAddress(listen);
1453
+ let parsed;
1454
+ try {
1455
+ parsed = new URL(targetUrl);
1456
+ } catch {
1457
+ throw new Error(`Invalid proxy targetUrl: ${targetUrl}`);
1458
+ }
1459
+ const targetHost = normalizeHost(parsed.hostname);
1460
+ const listenHost = normalizeHost(host);
1461
+ const targetPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
1462
+ if (targetHost === listenHost && targetPort === String(port)) {
1463
+ throw new Error(
1464
+ `Invalid proxy configuration: targetUrl (${targetUrl}) points to proxy listen address (${listen})`
1465
+ );
1466
+ }
1467
+ }
1468
+ function normalizeHost(host) {
1469
+ if (host === "localhost" || host === "::1") {
1470
+ return "127.0.0.1";
1471
+ }
1472
+ return host;
1473
+ }
1425
1474
 
1426
1475
  // src/storage/memory-store.ts
1427
1476
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -1566,13 +1615,13 @@ var MemoryStore = class {
1566
1615
  ...existing,
1567
1616
  status,
1568
1617
  updatedAt: now,
1569
- completedAt: isTerminalPaymentStatus(status) ? existing.completedAt ?? now : void 0
1618
+ completedAt: isTerminalPaymentStatus2(status) ? existing.completedAt ?? now : void 0
1570
1619
  } : {
1571
1620
  paymentHash: hash,
1572
1621
  status,
1573
1622
  trackedAt: now,
1574
1623
  updatedAt: now,
1575
- completedAt: isTerminalPaymentStatus(status) ? now : void 0
1624
+ completedAt: isTerminalPaymentStatus2(status) ? now : void 0
1576
1625
  };
1577
1626
  this.trackedPayments.set(hash, next);
1578
1627
  }
@@ -1607,7 +1656,7 @@ var MemoryStore = class {
1607
1656
  function isTerminalInvoiceStatus(status) {
1608
1657
  return status === "Paid" || status === "Cancelled" || status === "Expired";
1609
1658
  }
1610
- function isTerminalPaymentStatus(status) {
1659
+ function isTerminalPaymentStatus2(status) {
1611
1660
  return status === "Success" || status === "Failed";
1612
1661
  }
1613
1662
 
@@ -1899,6 +1948,7 @@ var PATTERNS = [
1899
1948
  // Peer / connectivity
1900
1949
  { pattern: /peer.*offline|peer.*unreachable|peer.*disconnect/i, category: "peer_offline", retryable: true },
1901
1950
  { pattern: /connection.*refused|connection.*reset/i, category: "peer_offline", retryable: true },
1951
+ { pattern: /fetch failed/i, category: "peer_offline", retryable: true },
1902
1952
  { pattern: /peer.*feature not found|waiting for peer to send init message/i, category: "peer_offline", retryable: true },
1903
1953
  { pattern: /channel.*already.*exist|duplicat(e|ed).*channel/i, category: "temporary_failure", retryable: true },
1904
1954
  // Timeout
@@ -2051,8 +2101,12 @@ function transitionJobState(job, machine, event, options) {
2051
2101
  }
2052
2102
  function applyRetryOrFail(job, classifiedError, policy, options) {
2053
2103
  const now = options?.now ?? Date.now();
2054
- if (shouldRetry(classifiedError, job.retryCount, policy)) {
2055
- const delay = computeRetryDelay(job.retryCount, policy);
2104
+ const effectivePolicy = {
2105
+ ...policy,
2106
+ maxRetries: job.maxRetries
2107
+ };
2108
+ if (shouldRetry(classifiedError, job.retryCount, effectivePolicy)) {
2109
+ const delay = computeRetryDelay(job.retryCount, effectivePolicy);
2056
2110
  const retryTransition = options?.machine && options.retryEvent ? transitionJobState(job, options.machine, options.retryEvent, { now }) : { ...job, state: "waiting_retry", updatedAt: now };
2057
2111
  return {
2058
2112
  ...retryTransition,
@@ -2237,6 +2291,15 @@ async function* runInvoiceJob(job, rpc, policy, signal) {
2237
2291
  yield current;
2238
2292
  }
2239
2293
  if (current.state === "waiting_retry") {
2294
+ const delay = current.nextRetryAt ? Math.max(0, current.nextRetryAt - Date.now()) : 0;
2295
+ if (delay > 0) {
2296
+ await sleep(delay, signal);
2297
+ if (signal.aborted) {
2298
+ current = transitionJobState(current, invoiceStateMachine, "cancel");
2299
+ yield current;
2300
+ return;
2301
+ }
2302
+ }
2240
2303
  current = transitionJobState(current, invoiceStateMachine, "retry_delay_elapsed", {
2241
2304
  patch: { nextRetryAt: void 0 }
2242
2305
  });
@@ -2443,6 +2506,15 @@ async function* runChannelJob(job, rpc, policy, signal) {
2443
2506
  yield current;
2444
2507
  }
2445
2508
  if (current.state === "waiting_retry") {
2509
+ const delay = current.nextRetryAt ? Math.max(0, current.nextRetryAt - Date.now()) : 0;
2510
+ if (delay > 0) {
2511
+ await sleep(delay, signal);
2512
+ if (signal.aborted) {
2513
+ current = transitionJobState(current, channelStateMachine, "cancel");
2514
+ yield current;
2515
+ return;
2516
+ }
2517
+ }
2446
2518
  current = transitionJobState(current, channelStateMachine, "retry_delay_elapsed", {
2447
2519
  patch: { nextRetryAt: void 0 }
2448
2520
  });
@@ -2656,7 +2728,7 @@ async function* runChannelJob(job, rpc, policy, signal) {
2656
2728
  }
2657
2729
  throw new Error(`Unsupported channel action: ${current.params.action}`);
2658
2730
  } catch (error) {
2659
- const classified = classifyRpcError(error);
2731
+ const classified = classifyChannelError(error);
2660
2732
  current = applyRetryOrFail(current, classified, policy, {
2661
2733
  machine: channelStateMachine,
2662
2734
  retryEvent: "payment_failed_retryable",
@@ -2665,6 +2737,32 @@ async function* runChannelJob(job, rpc, policy, signal) {
2665
2737
  yield current;
2666
2738
  }
2667
2739
  }
2740
+ function classifyChannelError(error) {
2741
+ const base = classifyRpcError(error);
2742
+ if (base.retryable) {
2743
+ return base;
2744
+ }
2745
+ const raw = base.rawError ?? base.message;
2746
+ if (/channel\s+not\s+found|no\s+channel\s+with\s+.*\s+found|no\s+channel\s+.*\s+found/i.test(
2747
+ raw
2748
+ )) {
2749
+ return {
2750
+ ...base,
2751
+ category: "temporary_failure",
2752
+ retryable: true,
2753
+ rawError: raw
2754
+ };
2755
+ }
2756
+ if (/invalid\s+state|negotiatingfunding|cannot\s+.*\s+in\s+.*state/i.test(raw)) {
2757
+ return {
2758
+ ...base,
2759
+ category: "temporary_failure",
2760
+ retryable: true,
2761
+ rawError: raw
2762
+ };
2763
+ }
2764
+ return base;
2765
+ }
2668
2766
  async function findTargetChannel(rpc, peerId, channelId) {
2669
2767
  const channels = await rpc.listChannels({
2670
2768
  peer_id: peerId,
@@ -2710,7 +2808,7 @@ var JobManager = class extends EventEmitter {
2710
2808
  this.rpc = rpc;
2711
2809
  this.store = store;
2712
2810
  this.retryPolicy = config.retryPolicy ?? defaultPaymentRetryPolicy;
2713
- this.schedulerIntervalMs = config.schedulerIntervalMs ?? 500;
2811
+ this.schedulerIntervalMs = config.schedulerIntervalMs ?? 1e3;
2714
2812
  this.maxConcurrentJobs = config.maxConcurrentJobs ?? 5;
2715
2813
  }
2716
2814
  async ensurePayment(params, options = {}) {