@ic-reactor/core 3.0.1-beta.0 → 3.0.1
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 +6 -4
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +48 -4
- package/dist/client.js.map +1 -1
- package/dist/display/types.d.ts +2 -2
- package/dist/display/types.d.ts.map +1 -1
- package/dist/display/visitor.d.ts +2 -1
- package/dist/display/visitor.d.ts.map +1 -1
- package/dist/display/visitor.js +146 -121
- package/dist/display/visitor.js.map +1 -1
- package/dist/display-reactor.d.ts +9 -92
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +5 -41
- package/dist/display-reactor.js.map +1 -1
- package/dist/reactor.d.ts +36 -4
- package/dist/reactor.d.ts.map +1 -1
- package/dist/reactor.js +101 -61
- package/dist/reactor.js.map +1 -1
- package/dist/types/client.d.ts +11 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/display-reactor.d.ts +54 -0
- package/dist/types/display-reactor.d.ts.map +1 -0
- package/dist/types/display-reactor.js +5 -0
- package/dist/types/display-reactor.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/reactor.d.ts +10 -15
- package/dist/types/reactor.d.ts.map +1 -1
- package/dist/types/transform.d.ts +1 -1
- package/dist/types/transform.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/helper.d.ts +20 -1
- package/dist/utils/helper.d.ts.map +1 -1
- package/dist/utils/helper.js +37 -6
- package/dist/utils/helper.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/zod.d.ts +34 -0
- package/dist/utils/zod.d.ts.map +1 -0
- package/dist/utils/zod.js +39 -0
- package/dist/utils/zod.js.map +1 -0
- package/package.json +7 -6
- package/src/client.ts +571 -0
- package/src/display/helper.ts +92 -0
- package/src/display/index.ts +3 -0
- package/src/display/types.ts +91 -0
- package/src/display/visitor.ts +415 -0
- package/src/display-reactor.ts +361 -0
- package/src/errors/index.ts +246 -0
- package/src/index.ts +8 -0
- package/src/reactor.ts +461 -0
- package/src/types/client.ts +110 -0
- package/src/types/display-reactor.ts +73 -0
- package/src/types/index.ts +6 -0
- package/src/types/reactor.ts +188 -0
- package/src/types/result.ts +50 -0
- package/src/types/transform.ts +29 -0
- package/src/types/variant.ts +39 -0
- package/src/utils/agent.ts +201 -0
- package/src/utils/candid.ts +112 -0
- package/src/utils/constants.ts +12 -0
- package/src/utils/helper.ts +155 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/polling.ts +330 -0
- package/src/utils/zod.ts +56 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { LOCAL_HOSTS, REMOTE_HOSTS } from "./constants"
|
|
2
|
+
import { CanisterError } from "../errors"
|
|
3
|
+
import { OkResult } from "../types"
|
|
4
|
+
|
|
5
|
+
export const generateKey = (args: any[]) => {
|
|
6
|
+
return JSON.stringify(args, (_, v) =>
|
|
7
|
+
typeof v === "bigint" ? v.toString() : v
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks if the current environment is local or development.
|
|
13
|
+
*
|
|
14
|
+
* @returns `true` if running in a local or development environment, otherwise `false`.
|
|
15
|
+
*/
|
|
16
|
+
export const isInLocalOrDevelopment = () => {
|
|
17
|
+
return typeof process !== "undefined" && process.env.DFX_NETWORK === "local"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Retrieves the network from the process environment variables.
|
|
22
|
+
*
|
|
23
|
+
* @returns The network name, defaulting to "ic" if not specified.
|
|
24
|
+
*/
|
|
25
|
+
export const getProcessEnvNetwork = () => {
|
|
26
|
+
if (typeof process === "undefined") return "ic"
|
|
27
|
+
else return process.env.DFX_NETWORK ?? "ic"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect whether the runtime should be considered *development*.
|
|
32
|
+
*
|
|
33
|
+
* Checks in order:
|
|
34
|
+
* - `import.meta.env?.DEV` (Vite / ESM environments)
|
|
35
|
+
* - `process.env.NODE_ENV === 'development'` (Node)
|
|
36
|
+
* - `process.env.DFX_NETWORK === 'local'` (local IC replica)
|
|
37
|
+
*/
|
|
38
|
+
export const isDev = (): boolean => {
|
|
39
|
+
const importMetaDev =
|
|
40
|
+
typeof import.meta !== "undefined" && (import.meta as any).env?.DEV
|
|
41
|
+
const nodeDev =
|
|
42
|
+
typeof process !== "undefined" &&
|
|
43
|
+
(process.env.NODE_ENV === "development" ||
|
|
44
|
+
process.env.DFX_NETWORK === "local")
|
|
45
|
+
|
|
46
|
+
return Boolean(importMetaDev || nodeDev)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determines the network type based on the provided hostname.
|
|
51
|
+
*
|
|
52
|
+
* @param hostname - The hostname to evaluate.
|
|
53
|
+
* @returns A string indicating the network type: "local", "remote", or "ic".
|
|
54
|
+
*/
|
|
55
|
+
export function getNetworkByHostname(
|
|
56
|
+
hostname: string
|
|
57
|
+
): "local" | "remote" | "ic" {
|
|
58
|
+
if (LOCAL_HOSTS.some((host) => hostname.endsWith(host))) {
|
|
59
|
+
return "local"
|
|
60
|
+
} else if (REMOTE_HOSTS.some((host) => hostname.endsWith(host))) {
|
|
61
|
+
return "remote"
|
|
62
|
+
} else {
|
|
63
|
+
return "ic"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Helper function for extracting the value from a compiled result { Ok: T } or throw a CanisterError if { Err: E }
|
|
69
|
+
* Supports both uppercase (Ok/Err - Rust) and lowercase (ok/err - Motoko) conventions.
|
|
70
|
+
*
|
|
71
|
+
* @param result - The compiled result to extract from.
|
|
72
|
+
* @returns The extracted value from the compiled result.
|
|
73
|
+
* @throws CanisterError with the typed error value if result is { Err: E } or { err: E }
|
|
74
|
+
*/
|
|
75
|
+
export function extractOkResult<T>(result: T): OkResult<T> {
|
|
76
|
+
if (!result || typeof result !== "object") {
|
|
77
|
+
// Non-object, return as-is
|
|
78
|
+
return result as OkResult<T>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle { Ok: T } (Rust convention)
|
|
82
|
+
if ("Ok" in result) {
|
|
83
|
+
return result.Ok as OkResult<T>
|
|
84
|
+
}
|
|
85
|
+
// Handle { ok: T } (Motoko convention)
|
|
86
|
+
if ("ok" in result) {
|
|
87
|
+
return result.ok as OkResult<T>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Handle { Err: E } (Rust convention) - throw CanisterError
|
|
91
|
+
if ("Err" in result) {
|
|
92
|
+
throw new CanisterError(result.Err)
|
|
93
|
+
}
|
|
94
|
+
// Handle { err: E } (Motoko convention) - throw CanisterError
|
|
95
|
+
if ("err" in result) {
|
|
96
|
+
throw new CanisterError(result.err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Non-Result type, return as-is
|
|
100
|
+
return result as OkResult<T>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const isNullish = (value: unknown): value is null | undefined =>
|
|
104
|
+
value === null || value === undefined
|
|
105
|
+
|
|
106
|
+
export const nonNullish = <T>(value: T | null | undefined): value is T =>
|
|
107
|
+
value !== null && value !== undefined
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converts a Uint8Array or number array to a hex string (without 0x prefix)
|
|
111
|
+
*/
|
|
112
|
+
export const uint8ArrayToHex = (bytes: Uint8Array | number[]): string => {
|
|
113
|
+
return Array.from(bytes)
|
|
114
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
115
|
+
.join("")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Converts a hex string to Uint8Array (accepts with or without 0x prefix)
|
|
120
|
+
*/
|
|
121
|
+
export const hexToUint8Array = (hex: string): Uint8Array<ArrayBuffer> => {
|
|
122
|
+
// Normalize: remove 0x prefix if present and filter invalid chars
|
|
123
|
+
const normalized = hex
|
|
124
|
+
.replace(/^0x/i, "")
|
|
125
|
+
.replace(/[^0-9a-f]/gi, "")
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
|
|
128
|
+
// Handle odd-length hex strings by padding with leading zero
|
|
129
|
+
const paddedHex = normalized.length % 2 ? `0${normalized}` : normalized
|
|
130
|
+
|
|
131
|
+
return new Uint8Array(
|
|
132
|
+
paddedHex.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) ?? []
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Formats hex string with 0x prefix for display purposes
|
|
138
|
+
*/
|
|
139
|
+
export const formatHexDisplay = (hex: string): `0x${string}` => {
|
|
140
|
+
const normalized = hex.replace(/^0x/i, "")
|
|
141
|
+
return `0x${normalized}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Converts a JSON-serializable value to a string, handling BigInt values.
|
|
146
|
+
* @param value - The value to convert
|
|
147
|
+
* @returns A string representation of the value
|
|
148
|
+
*/
|
|
149
|
+
export const jsonToString = (value: any): string => {
|
|
150
|
+
return JSON.stringify(
|
|
151
|
+
value,
|
|
152
|
+
(_, v) => (typeof v === "bigint" ? v.toString() : v),
|
|
153
|
+
2
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module polling
|
|
3
|
+
* @description
|
|
4
|
+
* Polling strategy for Internet Computer (Dfinity) agent update calls.
|
|
5
|
+
*
|
|
6
|
+
* This module provides a configurable, intelligent polling mechanism that:
|
|
7
|
+
* - Starts with rapid polling for quick responses (fast phase)
|
|
8
|
+
* - Gradually increases delay intervals (ramp phase)
|
|
9
|
+
* - Settles into steady-state polling (plateau phase)
|
|
10
|
+
* - Adds jitter to prevent thundering herd problems
|
|
11
|
+
* - Provides structured logging with elapsed time and status
|
|
12
|
+
* - Supports graceful cancellation via AbortSignal
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const strategy = createPollingStrategy({
|
|
17
|
+
* context: "signer-creation",
|
|
18
|
+
* fastAttempts: 10,
|
|
19
|
+
* plateauDelayMs: 5000
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const result = await actor.createSigner(params, {
|
|
23
|
+
* pollingOptions: { strategy }
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
PollStrategy,
|
|
30
|
+
RequestId,
|
|
31
|
+
RequestStatusResponseStatus,
|
|
32
|
+
} from "@icp-sdk/core/agent"
|
|
33
|
+
import { Principal } from "@icp-sdk/core/principal"
|
|
34
|
+
|
|
35
|
+
export interface PollingConfig {
|
|
36
|
+
/**
|
|
37
|
+
* Logical operation name used in log messages for identifying the polling context.
|
|
38
|
+
*
|
|
39
|
+
* Used to distinguish between different polling operations in console output.
|
|
40
|
+
* Choose descriptive names that help with debugging and monitoring.
|
|
41
|
+
*
|
|
42
|
+
* @default "operation"
|
|
43
|
+
* @example "sign-transaction", "document-upload"
|
|
44
|
+
*/
|
|
45
|
+
context?: string
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom prefix prepended to all log messages from this polling strategy.
|
|
49
|
+
*
|
|
50
|
+
* Allows filtering and identifying logs specific to this polling instance.
|
|
51
|
+
* Useful when running multiple polling operations concurrently.
|
|
52
|
+
*
|
|
53
|
+
* @default "[Polling]"
|
|
54
|
+
* @example "[PaymentPolling]", "[TransactionPolling]", "[SignerPolling]"
|
|
55
|
+
*/
|
|
56
|
+
logPrefix?: string
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Number of initial rapid polling attempts before transitioning to ramp phase.
|
|
60
|
+
*
|
|
61
|
+
* During fast phase, polls occur at `fastDelayMs` intervals (plus jitter).
|
|
62
|
+
* Higher values = more aggressive initial polling for fast responses.
|
|
63
|
+
* Lower values = faster transition to exponential backoff.
|
|
64
|
+
*
|
|
65
|
+
* @default 10
|
|
66
|
+
* @example 5 (for slower operations), 15 (for very fast operations)
|
|
67
|
+
*/
|
|
68
|
+
fastAttempts?: number
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Base delay in milliseconds between polls during the fast phase.
|
|
72
|
+
*
|
|
73
|
+
* This is the baseline interval before jitter is applied.
|
|
74
|
+
* Actual delay will vary by ±`jitterRatio` percentage.
|
|
75
|
+
* Lower values = more aggressive polling, higher network load.
|
|
76
|
+
*
|
|
77
|
+
* @default 100
|
|
78
|
+
* @example 50 (aggressive), 200 (conservative)
|
|
79
|
+
*/
|
|
80
|
+
fastDelayMs?: number
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Duration threshold in milliseconds for the ramp phase.
|
|
84
|
+
*
|
|
85
|
+
* While elapsed time < rampUntilMs, delay grows exponentially from
|
|
86
|
+
* `fastDelayMs` up to `plateauDelayMs` using a power curve (0.7 exponent).
|
|
87
|
+
* After this duration, polling enters plateau phase with constant delays.
|
|
88
|
+
*
|
|
89
|
+
* @default 20000 (20 seconds)
|
|
90
|
+
* @example 10_000 (faster transition), 30_000 (longer ramp for slow ops)
|
|
91
|
+
*/
|
|
92
|
+
rampUntilMs?: number
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Steady-state polling delay in milliseconds during plateau phase.
|
|
96
|
+
*
|
|
97
|
+
* Once ramp phase completes, all subsequent polls use this interval (plus jitter).
|
|
98
|
+
* This is the "cruise speed" for long-running operations.
|
|
99
|
+
* Balance between responsiveness and network/resource efficiency.
|
|
100
|
+
*
|
|
101
|
+
* @default 5000 (5 seconds)
|
|
102
|
+
* @example 2_000 (more responsive), 10_000 (more efficient)
|
|
103
|
+
*/
|
|
104
|
+
plateauDelayMs?: number
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Randomization ratio (0-1) for adding jitter to prevent thundering herd.
|
|
108
|
+
*
|
|
109
|
+
* Jitter adds ±(ratio * delay) randomness to each polling interval.
|
|
110
|
+
* Higher values = more randomization, better distribution across time.
|
|
111
|
+
* Lower values = more predictable intervals, less variance.
|
|
112
|
+
* Prevents synchronized polling when multiple clients start simultaneously.
|
|
113
|
+
*
|
|
114
|
+
* @default 0.4 (±40% randomization)
|
|
115
|
+
* @example 0.2 (±20%, less jitter), 0.5 (±50%, more jitter)
|
|
116
|
+
*/
|
|
117
|
+
jitterRatio?: number
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Maximum time in milliseconds between log outputs (heartbeat interval).
|
|
121
|
+
*
|
|
122
|
+
* Forces a log message if this much time passes without logging,
|
|
123
|
+
* even if normal log throttling would suppress it.
|
|
124
|
+
* Prevents "silent" long-running operations; ensures monitoring visibility.
|
|
125
|
+
*
|
|
126
|
+
* @default 15000 (15 seconds)
|
|
127
|
+
* @example 10_000 (more frequent heartbeats), 30_000 (less verbose)
|
|
128
|
+
*/
|
|
129
|
+
maxLogIntervalMs?: number
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* External abort signal for graceful cancellation of polling operation.
|
|
133
|
+
*
|
|
134
|
+
* When the signal is aborted, the polling strategy throws an error
|
|
135
|
+
* on the next poll attempt. Use AbortController to create signals.
|
|
136
|
+
* Allows coordinated cancellation across multiple async operations.
|
|
137
|
+
*
|
|
138
|
+
* @default undefined (no external cancellation)
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const controller = new AbortController();
|
|
142
|
+
* const strategy = createPollingStrategy({
|
|
143
|
+
* abortSignal: controller.signal
|
|
144
|
+
* });
|
|
145
|
+
* // Later: controller.abort();
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
abortSignal?: AbortSignal
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates an polling strategy for Internet Computer agent update calls.
|
|
153
|
+
*
|
|
154
|
+
* The strategy implements three phases:
|
|
155
|
+
* 1. **Fast Phase**: Initial rapid polling (default: 10 attempts @ 100ms intervals)
|
|
156
|
+
* 2. **Ramp Phase**: Exponential backoff growth (default: up to 20s elapsed)
|
|
157
|
+
* 3. **Plateau Phase**: Steady-state polling (default: 5s intervals)
|
|
158
|
+
*
|
|
159
|
+
* The strategy continues polling while request status is RECEIVED/PROCESSING,
|
|
160
|
+
* and only terminates on REPLIED/REJECTED/DONE status or when aborted.
|
|
161
|
+
*
|
|
162
|
+
* @param {PollingConfig} [cfg={}] - Configuration options
|
|
163
|
+
* @returns {PollStrategy} - Async strategy function compatible with agent pollingOptions.strategy
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* // Basic usage
|
|
168
|
+
* const strategy = createPollingStrategy();
|
|
169
|
+
*
|
|
170
|
+
* // Custom configuration for long-running operations
|
|
171
|
+
* const strategy = createPollingStrategy({
|
|
172
|
+
* context: "blockchain-sync",
|
|
173
|
+
* fastAttempts: 5,
|
|
174
|
+
* fastDelayMs: 200,
|
|
175
|
+
* rampUntilMs: 30_000,
|
|
176
|
+
* plateauDelayMs: 10_000,
|
|
177
|
+
* jitterRatio: 0.3
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* // With abort signal
|
|
181
|
+
* const controller = new AbortController();
|
|
182
|
+
* const strategy = createPollingStrategy({
|
|
183
|
+
* context: "transaction-signing",
|
|
184
|
+
* abortSignal: controller.signal
|
|
185
|
+
* });
|
|
186
|
+
* // Later: controller.abort();
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @throws {Error} When abortSignal is triggered during polling
|
|
190
|
+
*/
|
|
191
|
+
export function createPollingStrategy(cfg: PollingConfig = {}): PollStrategy {
|
|
192
|
+
const {
|
|
193
|
+
context = "operation",
|
|
194
|
+
logPrefix = "[Polling]",
|
|
195
|
+
fastAttempts = 10,
|
|
196
|
+
fastDelayMs = 100,
|
|
197
|
+
rampUntilMs = 20_000,
|
|
198
|
+
plateauDelayMs = 5_000,
|
|
199
|
+
jitterRatio = 0.4,
|
|
200
|
+
maxLogIntervalMs = 15_000,
|
|
201
|
+
abortSignal,
|
|
202
|
+
} = cfg
|
|
203
|
+
|
|
204
|
+
let attempt = 0
|
|
205
|
+
const start = Date.now()
|
|
206
|
+
let lastLog = 0
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Structured logging function that outputs polling state information.
|
|
210
|
+
* Implements intelligent log throttling to prevent console spam while ensuring
|
|
211
|
+
* periodic heartbeat logs for long-running operations.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} phase - Current polling phase ("fast", "ramp", or "plateau")
|
|
214
|
+
* @param {string | undefined} statusKind - Request status from IC agent
|
|
215
|
+
* @param {number} nextDelay - Calculated delay until next poll (ms)
|
|
216
|
+
*/
|
|
217
|
+
const log = (
|
|
218
|
+
phase: string,
|
|
219
|
+
statusKind: string | undefined,
|
|
220
|
+
nextDelay: number
|
|
221
|
+
) => {
|
|
222
|
+
const now = Date.now()
|
|
223
|
+
// Suppress ultra-noisy logs: skip if < 1s since last log and not in fast phase
|
|
224
|
+
if (now - lastLog < 1_000 && phase !== "fast" && nextDelay < 1_000) return
|
|
225
|
+
|
|
226
|
+
// Force a heartbeat log if too much time has passed (prevents silent operations)
|
|
227
|
+
if (now - lastLog > maxLogIntervalMs) {
|
|
228
|
+
phase += "+heartbeat"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lastLog = now
|
|
232
|
+
// eslint-disable-next-line no-console
|
|
233
|
+
console.info(
|
|
234
|
+
`${logPrefix} ${context} attempt=${attempt} elapsed=${now - start}ms status=${statusKind} phase=${phase} nextDelay=${Math.round(nextDelay)}ms`
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Computes the next polling delay based on elapsed time and attempt count.
|
|
240
|
+
* Implements three-phase strategy:
|
|
241
|
+
* - Fast: Initial rapid polling
|
|
242
|
+
* - Ramp: Exponential growth with power curve
|
|
243
|
+
* - Plateau: Steady-state polling
|
|
244
|
+
*
|
|
245
|
+
* @param {number} elapsed - Milliseconds elapsed since start
|
|
246
|
+
* @param {number} a - Current attempt number
|
|
247
|
+
* @returns {{ delay: number; phase: string }} - Calculated delay and phase name
|
|
248
|
+
*/
|
|
249
|
+
const computeDelay = (
|
|
250
|
+
elapsed: number,
|
|
251
|
+
a: number
|
|
252
|
+
): { delay: number; phase: string } => {
|
|
253
|
+
// Phase 1: Fast initial polling
|
|
254
|
+
if (a < fastAttempts) {
|
|
255
|
+
return { delay: withJitter(fastDelayMs), phase: "fast" }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Phase 2: Ramp with exponential growth (power curve 0.7 for smooth acceleration)
|
|
259
|
+
if (elapsed < rampUntilMs) {
|
|
260
|
+
const progress = elapsed / rampUntilMs // Normalized progress: 0..1
|
|
261
|
+
const base =
|
|
262
|
+
fastDelayMs + (plateauDelayMs - fastDelayMs) * Math.pow(progress, 0.7)
|
|
263
|
+
return { delay: withJitter(base), phase: "ramp" }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Phase 3: Plateau - steady-state polling
|
|
267
|
+
return { delay: withJitter(plateauDelayMs), phase: "plateau" }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Applies random jitter to prevent synchronized polling across multiple clients.
|
|
272
|
+
* Uses configured jitterRatio to add ±N% randomness to the base delay.
|
|
273
|
+
*
|
|
274
|
+
* @param {number} base - Base delay in milliseconds
|
|
275
|
+
* @returns {number} - Jittered delay, minimum 50ms
|
|
276
|
+
*/
|
|
277
|
+
const withJitter = (base: number): number => {
|
|
278
|
+
const spread = base * jitterRatio
|
|
279
|
+
return Math.max(50, base - spread + Math.random() * spread * 2)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Async strategy function invoked by the IC agent on each polling cycle.
|
|
284
|
+
* Determines whether to continue waiting based on request status.
|
|
285
|
+
*
|
|
286
|
+
* @param {Principal} _canisterId - Target canister principal (unused but required by interface)
|
|
287
|
+
* @param {RequestId} _requestId - Request identifier (unused but required by interface)
|
|
288
|
+
* @param {RequestStatusResponseStatus} [rawStatus] - Current request status from IC
|
|
289
|
+
* @returns {Promise<void>} - Resolves after calculated delay, or immediately for terminal states
|
|
290
|
+
* @throws {Error} - If abortSignal is triggered
|
|
291
|
+
*/
|
|
292
|
+
return async function strategy(
|
|
293
|
+
_canisterId: Principal,
|
|
294
|
+
_requestId: RequestId,
|
|
295
|
+
rawStatus?: RequestStatusResponseStatus
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
// Check for external cancellation
|
|
298
|
+
if (abortSignal?.aborted) {
|
|
299
|
+
throw new Error(`${context} polling aborted`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
attempt++
|
|
303
|
+
const statusKind = rawStatus
|
|
304
|
+
|
|
305
|
+
// Terminal states: Stop polling immediately
|
|
306
|
+
// - Replied: Request completed successfully
|
|
307
|
+
// - Rejected: Request was rejected by canister
|
|
308
|
+
// - Done: Request processing complete
|
|
309
|
+
// Note: Agent typically stops before we reach here, but we handle defensively
|
|
310
|
+
if (
|
|
311
|
+
statusKind === RequestStatusResponseStatus.Replied ||
|
|
312
|
+
statusKind === RequestStatusResponseStatus.Rejected ||
|
|
313
|
+
statusKind === RequestStatusResponseStatus.Done
|
|
314
|
+
) {
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Continue polling for:
|
|
319
|
+
// - Received: Request received but not yet processed
|
|
320
|
+
// - Processing: Request is being processed
|
|
321
|
+
// - Unknown: Status not yet available
|
|
322
|
+
// - undefined: No status information yet
|
|
323
|
+
const elapsed = Date.now() - start
|
|
324
|
+
const { delay, phase } = computeDelay(elapsed, attempt)
|
|
325
|
+
log(phase, statusKind, delay)
|
|
326
|
+
|
|
327
|
+
// Sleep for calculated delay before next poll
|
|
328
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/utils/zod.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Zod Integration Helper
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { ValidationIssue } from "../errors"
|
|
6
|
+
import { ValidationResult, Validator } from "../types"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a validator from a Zod schema.
|
|
10
|
+
* This is a utility function to easily integrate Zod schemas as validators.
|
|
11
|
+
*
|
|
12
|
+
* @param schema - A Zod schema to validate against
|
|
13
|
+
* @returns A Validator function compatible with DisplayReactor
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { z } from "zod"
|
|
18
|
+
* import { fromZodSchema } from "@ic-reactor/core"
|
|
19
|
+
*
|
|
20
|
+
* const transferSchema = z.object({
|
|
21
|
+
* to: z.string().min(1, "Recipient is required"),
|
|
22
|
+
* amount: z.string().regex(/^\d+$/, "Must be a valid number"),
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* reactor.registerValidator("transfer", fromZodSchema(transferSchema))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function fromZodSchema<T>(schema: {
|
|
29
|
+
safeParse: (data: unknown) => {
|
|
30
|
+
success: boolean
|
|
31
|
+
error?: {
|
|
32
|
+
issues: Array<{
|
|
33
|
+
path: (string | number)[]
|
|
34
|
+
message: string
|
|
35
|
+
code?: string
|
|
36
|
+
}>
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}): Validator<T[]> {
|
|
40
|
+
return (args: T[]): ValidationResult => {
|
|
41
|
+
// Validate the first argument (common IC pattern)
|
|
42
|
+
const result = schema.safeParse(args[0])
|
|
43
|
+
|
|
44
|
+
if (result.success) {
|
|
45
|
+
return { success: true }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const issues: ValidationIssue[] = result.error!.issues.map((issue) => ({
|
|
49
|
+
path: issue.path,
|
|
50
|
+
message: issue.message,
|
|
51
|
+
code: issue.code,
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
return { success: false, issues }
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/version.ts
ADDED