@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.
Files changed (74) hide show
  1. package/README.md +6 -4
  2. package/dist/client.d.ts +3 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +48 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/display/types.d.ts +2 -2
  7. package/dist/display/types.d.ts.map +1 -1
  8. package/dist/display/visitor.d.ts +2 -1
  9. package/dist/display/visitor.d.ts.map +1 -1
  10. package/dist/display/visitor.js +146 -121
  11. package/dist/display/visitor.js.map +1 -1
  12. package/dist/display-reactor.d.ts +9 -92
  13. package/dist/display-reactor.d.ts.map +1 -1
  14. package/dist/display-reactor.js +5 -41
  15. package/dist/display-reactor.js.map +1 -1
  16. package/dist/reactor.d.ts +36 -4
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +101 -61
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types/client.d.ts +11 -0
  21. package/dist/types/client.d.ts.map +1 -1
  22. package/dist/types/display-reactor.d.ts +54 -0
  23. package/dist/types/display-reactor.d.ts.map +1 -0
  24. package/dist/types/display-reactor.js +5 -0
  25. package/dist/types/display-reactor.js.map +1 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/index.js +1 -0
  29. package/dist/types/index.js.map +1 -1
  30. package/dist/types/reactor.d.ts +10 -15
  31. package/dist/types/reactor.d.ts.map +1 -1
  32. package/dist/types/transform.d.ts +1 -1
  33. package/dist/types/transform.d.ts.map +1 -1
  34. package/dist/utils/constants.d.ts +1 -1
  35. package/dist/utils/constants.d.ts.map +1 -1
  36. package/dist/utils/constants.js +1 -1
  37. package/dist/utils/constants.js.map +1 -1
  38. package/dist/utils/helper.d.ts +20 -1
  39. package/dist/utils/helper.d.ts.map +1 -1
  40. package/dist/utils/helper.js +37 -6
  41. package/dist/utils/helper.js.map +1 -1
  42. package/dist/utils/index.d.ts +1 -0
  43. package/dist/utils/index.d.ts.map +1 -1
  44. package/dist/utils/index.js +1 -0
  45. package/dist/utils/index.js.map +1 -1
  46. package/dist/utils/zod.d.ts +34 -0
  47. package/dist/utils/zod.d.ts.map +1 -0
  48. package/dist/utils/zod.js +39 -0
  49. package/dist/utils/zod.js.map +1 -0
  50. package/package.json +7 -6
  51. package/src/client.ts +571 -0
  52. package/src/display/helper.ts +92 -0
  53. package/src/display/index.ts +3 -0
  54. package/src/display/types.ts +91 -0
  55. package/src/display/visitor.ts +415 -0
  56. package/src/display-reactor.ts +361 -0
  57. package/src/errors/index.ts +246 -0
  58. package/src/index.ts +8 -0
  59. package/src/reactor.ts +461 -0
  60. package/src/types/client.ts +110 -0
  61. package/src/types/display-reactor.ts +73 -0
  62. package/src/types/index.ts +6 -0
  63. package/src/types/reactor.ts +188 -0
  64. package/src/types/result.ts +50 -0
  65. package/src/types/transform.ts +29 -0
  66. package/src/types/variant.ts +39 -0
  67. package/src/utils/agent.ts +201 -0
  68. package/src/utils/candid.ts +112 -0
  69. package/src/utils/constants.ts +12 -0
  70. package/src/utils/helper.ts +155 -0
  71. package/src/utils/index.ts +5 -0
  72. package/src/utils/polling.ts +330 -0
  73. package/src/utils/zod.ts +56 -0
  74. package/src/version.ts +4 -0
package/src/reactor.ts ADDED
@@ -0,0 +1,461 @@
1
+ import type {
2
+ CallConfig,
3
+ PollingOptions,
4
+ ReadStateOptions,
5
+ } from "@icp-sdk/core/agent"
6
+ import type { ClientManager } from "./client"
7
+ import type { QueryKey, FetchQueryOptions } from "@tanstack/query-core"
8
+ import type {
9
+ ReactorParameters,
10
+ BaseActor,
11
+ ActorMethodParameters,
12
+ ActorMethodReturnType,
13
+ FunctionName,
14
+ TransformKey,
15
+ ReactorArgs,
16
+ ReactorReturnOk,
17
+ ReactorQueryParams,
18
+ ReactorCallParams,
19
+ CanisterId,
20
+ } from "./types/reactor"
21
+
22
+ import { DEFAULT_POLLING_OPTIONS } from "@icp-sdk/core/agent"
23
+ import { IDL } from "@icp-sdk/core/candid"
24
+ import { Principal } from "@icp-sdk/core/principal"
25
+ import { generateKey, extractOkResult } from "./utils/helper"
26
+ import {
27
+ processQueryCallResponse,
28
+ processUpdateCallResponse,
29
+ } from "./utils/agent"
30
+ import { CallError, CanisterError, ValidationError } from "./errors"
31
+ import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"
32
+
33
+ /**
34
+ * Reactor class for interacting with IC canisters.
35
+ *
36
+ * This class provides core functionality for:
37
+ * - Direct agent calls using agent.call() and agent.query()
38
+ * - Query caching with TanStack Query integration
39
+ * - Method calls with result unwrapping
40
+ *
41
+ * @typeParam A - The actor service type
42
+ * @typeParam T - The type transformation to apply (default: candid = raw Candid types)
43
+ */
44
+ export class Reactor<A = BaseActor, T extends TransformKey = "candid"> {
45
+ /** Phantom type brand for inference - never assigned at runtime */
46
+ declare readonly _actor: A
47
+ public readonly transform: TransformKey = "candid"
48
+ public clientManager: ClientManager
49
+ public name: string
50
+ public canisterId: Principal
51
+ public service: IDL.ServiceClass
52
+ public pollingOptions: PollingOptions
53
+
54
+ constructor(config: ReactorParameters) {
55
+ this.clientManager = config.clientManager
56
+ this.name = config.name
57
+ this.pollingOptions =
58
+ "pollingOptions" in config && config.pollingOptions
59
+ ? config.pollingOptions
60
+ : DEFAULT_POLLING_OPTIONS
61
+
62
+ const { idlFactory } = config
63
+ if (!idlFactory) {
64
+ throw new Error(`[ic-reactor] idlFactory is missing for ${this.name}`)
65
+ }
66
+
67
+ let canisterId = config.canisterId
68
+
69
+ if (!canisterId) {
70
+ const env = safeGetCanisterEnv()
71
+ const key = `PUBLIC_CANISTER_ID:${this.name}`
72
+ canisterId = env?.[key]
73
+
74
+ if (!canisterId) {
75
+ console.warn(
76
+ `[ic-reactor] ${this.name} canister ID not found in ic_env cookie`
77
+ )
78
+ canisterId = "aaaaa-aa" // Fallback
79
+ }
80
+ }
81
+
82
+ this.canisterId = Principal.from(canisterId)
83
+ this.service = idlFactory({ IDL })
84
+
85
+ // Register this canister ID for delegation during login
86
+ this.clientManager.registerCanisterId(this.canisterId.toString(), this.name)
87
+ }
88
+
89
+ /**
90
+ * Set the canister ID for this reactor.
91
+ * Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).
92
+ *
93
+ * @param canisterId - The new canister ID (as string or Principal)
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * // Switch to a different ledger canister
98
+ * ledgerReactor.setCanisterId("ryjl3-tyaaa-aaaaa-aaaba-cai")
99
+ *
100
+ * // Then use queries/mutations as normal
101
+ * const { data } = icrc1NameQuery.useQuery()
102
+ * ```
103
+ */
104
+ public setCanisterId(canisterId: CanisterId): void {
105
+ this.canisterId = Principal.from(canisterId)
106
+ // Register the new canister ID for delegation
107
+ this.clientManager.registerCanisterId(this.canisterId.toString(), this.name)
108
+ }
109
+
110
+ /**
111
+ * Set the canister name for this reactor.
112
+ * Useful for dynamically switching between canisters of the same type (e.g., multiple ICRC tokens).
113
+ *
114
+ * @param name - The new canister name
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * // Switch to a different ledger canister
119
+ * ledgerReactor.setCanisterName("icrc1")
120
+ *
121
+ * // Then use queries/mutations as normal
122
+ * const { data } = icrc1NameQuery.useQuery()
123
+ * ```
124
+ */
125
+ public setCanisterName(name: string): void {
126
+ this.name = name
127
+ }
128
+
129
+ protected verifyCanister() {
130
+ // Optional: add any verification logic here
131
+ }
132
+
133
+ /**
134
+ * Get the service interface (IDL.ServiceClass) for this reactor.
135
+ * Useful for introspection and codec generation.
136
+ * @returns The service interface
137
+ */
138
+ public getServiceInterface(): IDL.ServiceClass {
139
+ return this.service
140
+ }
141
+
142
+ /**
143
+ * Get the function class for a specific method.
144
+ * @param methodName - The name of the method
145
+ * @returns The function class or null if not found
146
+ */
147
+ protected getFuncClass<M extends FunctionName<A>>(
148
+ methodName: M
149
+ ): IDL.FuncClass | null {
150
+ const field = this.service._fields.find(([name]) => name === methodName)
151
+ return field ? field[1] : null
152
+ }
153
+
154
+ /**
155
+ * Check if a method is a query method (query or composite_query).
156
+ */
157
+ public isQueryMethod<M extends FunctionName<A>>(methodName: M): boolean {
158
+ const func = this.getFuncClass(methodName)
159
+ if (!func) return false
160
+ return (
161
+ func.annotations.includes("query") ||
162
+ func.annotations.includes("composite_query")
163
+ )
164
+ }
165
+
166
+ // ══════════════════════════════════════════════════════════════════════
167
+ // TRANSFORMATION METHODS
168
+ // ══════════════════════════════════════════════════════════════════════
169
+
170
+ /**
171
+ * Transform arguments before calling the method.
172
+ * Default implementation returns arguments as-is.
173
+ */
174
+ protected transformArgs<M extends FunctionName<A>>(
175
+ _methodName: M,
176
+ args?: ReactorArgs<A, M, T>
177
+ ): ActorMethodParameters<A[M]> {
178
+ if (!args) {
179
+ return [] as unknown as ActorMethodParameters<A[M]>
180
+ }
181
+ return args as ActorMethodParameters<A[M]>
182
+ }
183
+
184
+ /**
185
+ * Transform the result after calling the method.
186
+ * Default implementation extracts Ok value from Result types.
187
+ */
188
+ protected transformResult<M extends FunctionName<A>>(
189
+ _methodName: M,
190
+ result: ActorMethodReturnType<A[M]>
191
+ ): ReactorReturnOk<A, M, T> {
192
+ return extractOkResult(result) as ReactorReturnOk<A, M, T>
193
+ }
194
+
195
+ // ══════════════════════════════════════════════════════════════════════
196
+ // QUERY KEY GENERATION
197
+ // ══════════════════════════════════════════════════════════════════════
198
+
199
+ public generateQueryKey<M extends FunctionName<A>>(
200
+ params: ReactorQueryParams<A, M, T>
201
+ ): QueryKey {
202
+ const queryKeys: any[] = [this.canisterId.toString(), params.functionName]
203
+
204
+ if (params.args) {
205
+ const argKey = generateKey(params.args)
206
+ queryKeys.push(argKey)
207
+ }
208
+ if (params.queryKey) {
209
+ queryKeys.push(...params.queryKey)
210
+ }
211
+
212
+ return queryKeys
213
+ }
214
+
215
+ // ══════════════════════════════════════════════════════════════════════
216
+ // QUERY OPTIONS
217
+ // ══════════════════════════════════════════════════════════════════════
218
+
219
+ public getQueryOptions<M extends FunctionName<A>>(
220
+ params: ReactorCallParams<A, M, T>
221
+ ): FetchQueryOptions<ReactorReturnOk<A, M, T>> {
222
+ return {
223
+ queryKey: this.generateQueryKey(params),
224
+ queryFn: () => this.callMethod(params),
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Invalidate cached queries for this canister.
230
+ * This will mark matching queries as stale and trigger a refetch for any active queries.
231
+ *
232
+ * @param params - Optional parameters to filter the invalidation
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * // Invalidate all queries for this canister
237
+ * reactor.invalidateQueries()
238
+ *
239
+ * // Invalidate only 'getUser' queries
240
+ * reactor.invalidateQueries({ functionName: 'getUser' })
241
+ *
242
+ * // Invalidate 'getUser' query for specific user
243
+ * reactor.invalidateQueries({ functionName: 'getUser', args: ['user-1'] })
244
+ * ```
245
+ */
246
+ public invalidateQueries<M extends FunctionName<A>>(
247
+ params?: Partial<ReactorQueryParams<A, M, T>>
248
+ ) {
249
+ const queryKey = params
250
+ ? this.generateQueryKey({
251
+ functionName: params.functionName as M,
252
+ args: params.args,
253
+ queryKey: params.queryKey,
254
+ })
255
+ : [this.canisterId.toString()]
256
+
257
+ this.queryClient.invalidateQueries({
258
+ queryKey,
259
+ })
260
+ }
261
+
262
+ // ══════════════════════════════════════════════════════════════════════
263
+ // METHOD CALLS - Using agent.call() and agent.query() directly
264
+ // ══════════════════════════════════════════════════════════════════════
265
+
266
+ /**
267
+ * Call a canister method directly using agent.call() or agent.query().
268
+ * This is the recommended approach for interacting with canisters.
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * // Query method
273
+ * const result = await reactor.callMethod({
274
+ * functionName: 'greet',
275
+ * args: ['world'],
276
+ * });
277
+ *
278
+ * // Update method with options
279
+ * const result = await reactor.callMethod({
280
+ * functionName: 'transfer',
281
+ * args: [{ to: principal, amount: 100n }],
282
+ * callConfig: { effectiveCanisterId: principal },
283
+ * });
284
+ * ```
285
+ */
286
+ public async callMethod<M extends FunctionName<A>>(
287
+ params: Omit<ReactorCallParams<A, M, T>, "queryKey">
288
+ ): Promise<ReactorReturnOk<A, M, T>> {
289
+ try {
290
+ const func = this.getFuncClass(params.functionName)
291
+ if (!func) {
292
+ throw new Error(`Method ${String(params.functionName)} not found`)
293
+ }
294
+
295
+ // Transform args
296
+ const transformedArgs = this.transformArgs(
297
+ params.functionName,
298
+ params.args
299
+ )
300
+
301
+ // Encode arguments using Candid
302
+ const arg = IDL.encode(func.argTypes, transformedArgs)
303
+
304
+ // Determine if this is a query or update call
305
+ const isQuery =
306
+ func.annotations.includes("query") ||
307
+ func.annotations.includes("composite_query")
308
+
309
+ // Execute the call
310
+ let rawResponse: Uint8Array
311
+ if (isQuery) {
312
+ rawResponse = await this.executeQuery(
313
+ String(params.functionName),
314
+ arg,
315
+ params.callConfig
316
+ )
317
+ } else {
318
+ rawResponse = await this.executeCall(
319
+ String(params.functionName),
320
+ arg,
321
+ params.callConfig
322
+ )
323
+ }
324
+
325
+ // Decode the result
326
+ const decoded = IDL.decode(func.retTypes, rawResponse)
327
+
328
+ // Handle single, zero, and multiple return values appropriately
329
+ const response = (
330
+ decoded.length === 0
331
+ ? undefined
332
+ : decoded.length === 1
333
+ ? decoded[0]
334
+ : decoded
335
+ ) as ActorMethodReturnType<A[M]>
336
+
337
+ return this.transformResult(params.functionName, response)
338
+ } catch (error) {
339
+ // Re-throw CanisterError as-is (business logic error from canister)
340
+ if (error instanceof CanisterError || error instanceof ValidationError) {
341
+ throw error
342
+ }
343
+
344
+ const message = `Failed to call method "${String(params.functionName)}": `
345
+
346
+ // Wrap other errors in CallError (network/agent issues)
347
+ if (error instanceof Error) {
348
+ throw new CallError(message + error.message, error)
349
+ }
350
+
351
+ throw new CallError(message + String(error), error)
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Fetch data from the canister and cache it using React Query.
357
+ * This method ensures the data is in the cache and returns it.
358
+ */
359
+ public async fetchQuery<M extends FunctionName<A>>(
360
+ params: ReactorCallParams<A, M, T>
361
+ ): Promise<ReactorReturnOk<A, M, T>> {
362
+ const options = this.getQueryOptions(params)
363
+ return this.queryClient.ensureQueryData<ReactorReturnOk<A, M, T>>(options)
364
+ }
365
+
366
+ /**
367
+ * Get the current data from the cache without fetching.
368
+ */
369
+ public getQueryData<M extends FunctionName<A>>(
370
+ params: ReactorQueryParams<A, M, T>
371
+ ): ReactorReturnOk<A, M, T> | undefined {
372
+ const queryKey = this.generateQueryKey(params)
373
+ return this.queryClient.getQueryData<ReactorReturnOk<A, M, T>>(queryKey)
374
+ }
375
+
376
+ /**
377
+ * Execute a query call using agent.query()
378
+ */
379
+ protected async executeQuery(
380
+ methodName: string,
381
+ arg: Uint8Array,
382
+ callConfig?: CallConfig
383
+ ): Promise<Uint8Array> {
384
+ const agent = this.clientManager.agent
385
+ const effectiveCanisterId =
386
+ callConfig?.effectiveCanisterId ?? this.canisterId
387
+
388
+ const response = await agent.query(this.canisterId, {
389
+ methodName,
390
+ arg,
391
+ effectiveCanisterId,
392
+ })
393
+
394
+ return processQueryCallResponse(response, this.canisterId, methodName)
395
+ }
396
+
397
+ /**
398
+ * Execute an update call using agent.call()
399
+ */
400
+ protected async executeCall(
401
+ methodName: string,
402
+ arg: Uint8Array,
403
+ callConfig?: CallConfig
404
+ ): Promise<Uint8Array> {
405
+ const agent = this.clientManager.agent
406
+
407
+ const response = await agent.call(this.canisterId, {
408
+ methodName,
409
+ arg,
410
+ effectiveCanisterId: callConfig?.effectiveCanisterId,
411
+ nonce: callConfig?.nonce,
412
+ })
413
+
414
+ return await processUpdateCallResponse(
415
+ response,
416
+ this.canisterId,
417
+ methodName,
418
+ agent,
419
+ this.pollingOptions
420
+ )
421
+ }
422
+
423
+ // ══════════════════════════════════════════════════════════════════════
424
+ // SUBNET
425
+ // ══════════════════════════════════════════════════════════════════════
426
+
427
+ /**
428
+ * Get the subnet ID for this canister.
429
+ */
430
+ public async subnetId() {
431
+ return this.clientManager.agent.getSubnetIdFromCanister(this.canisterId)
432
+ }
433
+
434
+ /**
435
+ * Get the subnet state for this canister.
436
+ */
437
+ public async subnetState(options: ReadStateOptions) {
438
+ const subnetId = await this.subnetId()
439
+ return this.clientManager.agent.readSubnetState(subnetId, options)
440
+ }
441
+
442
+ // ══════════════════════════════════════════════════════════════════════
443
+ // GETTERS
444
+ // ══════════════════════════════════════════════════════════════════════
445
+
446
+ /**
447
+ * Get the query client from clientManager.
448
+ * This is the recommended way to access the query client for direct queries.
449
+ */
450
+ get queryClient() {
451
+ return this.clientManager.queryClient
452
+ }
453
+
454
+ /**
455
+ * Get the agent from clientManager.
456
+ * This is the recommended way to access the agent for direct calls.
457
+ */
458
+ get agent() {
459
+ return this.clientManager.agent
460
+ }
461
+ }
@@ -0,0 +1,110 @@
1
+ import type { HttpAgent, HttpAgentOptions, Identity } from "@icp-sdk/core/agent"
2
+ import type { AuthClient } from "@icp-sdk/auth/client"
3
+ import type { QueryClient } from "@tanstack/query-core"
4
+
5
+ /**
6
+ * Parameters for configuring a ClientManager instance.
7
+ *
8
+ * @property {QueryClient} queryClient - The TanStack QueryClient used for caching and state management.
9
+ * @property {number} [port] - The port used for the local IC replica (default is 4943).
10
+ * @property {HttpAgentOptions} [agentOptions] - Optional configuration for the underlying HttpAgent.
11
+ * @property {boolean} [withLocalEnv] - If true, configures the agent for a local environment.
12
+ * @property {boolean} [withProcessEnv] - If true, auto-configures the agent based on process.env settings.
13
+ */
14
+ export interface ClientManagerParameters {
15
+ /**
16
+ * The TanStack QueryClient used for caching and state management.
17
+ */
18
+ queryClient: QueryClient
19
+ /**
20
+ * Optional configuration for the underlying HttpAgent.
21
+ */
22
+ agentOptions?: HttpAgentOptions
23
+ /**
24
+ * The port used for the local IC replica (default is 4943).
25
+ */
26
+ port?: number
27
+ /**
28
+ * If true, configures the agent for a local environment.
29
+ */
30
+ withLocalEnv?: boolean
31
+ /**
32
+ * If true, auto-configures the agent based on process.env settings.
33
+ */
34
+ withProcessEnv?: boolean
35
+ /**
36
+ * Optional pre-initialized AuthClient instance.
37
+ * If provided, the manager will use this instance instead of dynamically importing
38
+ * and creating a new one from `@icp-sdk/auth`.
39
+ * This is useful for environments where dynamic imports are not supported or
40
+ * when you want to share an AuthClient instance across multiple managers.
41
+ */
42
+ authClient?: AuthClient
43
+ /**
44
+ * **EXPERIMENTAL** - If true, uses the canister environment from `@icp-sdk/core/agent/canister-env`
45
+ * to automatically configure the agent host and root key based on the `ic_env` cookie.
46
+ *
47
+ * ⚠️ This feature is experimental and may cause issues with update calls on localhost development.
48
+ * Use with caution and only when you need automatic environment detection from the IC SDK.
49
+ *
50
+ * @experimental
51
+ * @default false
52
+ */
53
+ withCanisterEnv?: boolean
54
+ }
55
+
56
+ /**
57
+ * Represents the state of an agent.
58
+ */
59
+ export interface AgentState {
60
+ /**
61
+ * Indicates whether the agent has been initialized.
62
+ */
63
+ isInitialized: boolean
64
+
65
+ /**
66
+ * Indicates whether the agent is in the process of initializing.
67
+ */
68
+ isInitializing: boolean
69
+
70
+ /**
71
+ * Represents an error associated with the agent, if any.
72
+ */
73
+ error: Error | undefined
74
+
75
+ /**
76
+ * Represents the network associated with the agent, if any.
77
+ */
78
+ network: string | undefined
79
+
80
+ /**
81
+ * Indicates whether the agent is connected to a local network.
82
+ */
83
+ isLocalhost: boolean
84
+ }
85
+
86
+ /**
87
+ * Represents the authentication state of an agent.
88
+ */
89
+ export interface AuthState {
90
+ identity: Identity | null
91
+
92
+ /**
93
+ * Indicates whether the authentication process is ongoing.
94
+ */
95
+ isAuthenticating: boolean
96
+
97
+ /**
98
+ * Indicates whether the agent is authenticated.
99
+ */
100
+ isAuthenticated: boolean
101
+
102
+ /**
103
+ * Represents any error that occurred during authentication.
104
+ */
105
+ error: Error | undefined
106
+ }
107
+
108
+ export interface UpdateAgentParameters extends HttpAgentOptions {
109
+ agent?: HttpAgent
110
+ }
@@ -0,0 +1,73 @@
1
+ // ============================================================================
2
+ // Validation Types
3
+ // ============================================================================
4
+
5
+ import { ValidationIssue } from "../errors"
6
+ import {
7
+ BaseActor,
8
+ FunctionName,
9
+ ReactorArgs,
10
+ ReactorParameters,
11
+ } from "./reactor"
12
+
13
+ /**
14
+ * Validation result returned by a validator function.
15
+ * Either success (true) or failure with issues.
16
+ */
17
+ export type ValidationResult =
18
+ | { success: true }
19
+ | { success: false; issues: ValidationIssue[] }
20
+
21
+ /**
22
+ * A validator function that validates method arguments.
23
+ * Receives display types (strings for Principal, bigint, etc.).
24
+ *
25
+ * @param args - The display-type arguments to validate
26
+ * @returns ValidationResult indicating success or failure with issues
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * // Validator receives display types
31
+ * reactor.registerValidator("transfer", ([input]) => {
32
+ * const issues = []
33
+ *
34
+ * // input.to is string (not Principal)
35
+ * if (!input.to) {
36
+ * issues.push({ path: ["to"], message: "Recipient is required" })
37
+ * }
38
+ *
39
+ * // input.amount is string (not bigint)
40
+ * if (!/^\d+$/.test(input.amount)) {
41
+ * issues.push({ path: ["amount"], message: "Must be a valid number" })
42
+ * }
43
+ *
44
+ * return issues.length > 0 ? { success: false, issues } : { success: true }
45
+ * })
46
+ * ```
47
+ */
48
+ export type Validator<Args = unknown[]> = (
49
+ args: Args
50
+ ) => ValidationResult | Promise<ValidationResult>
51
+
52
+ /**
53
+ * Validator that receives display types for a specific method.
54
+ */
55
+ export type DisplayValidator<A, M extends FunctionName<A>> = Validator<
56
+ ReactorArgs<A, M, "display">
57
+ >
58
+
59
+ // ============================================================================
60
+ // DisplayReactor Parameters
61
+ // ============================================================================
62
+
63
+ export interface DisplayReactorParameters<
64
+ A = BaseActor,
65
+ > extends ReactorParameters {
66
+ /**
67
+ * Optional initial validators to register.
68
+ * Validators receive display types (strings for Principal, bigint, etc.)
69
+ */
70
+ validators?: Partial<{
71
+ [M in FunctionName<A>]: DisplayValidator<A, M>
72
+ }>
73
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./reactor"
2
+ export * from "./client"
3
+ export * from "./result"
4
+ export * from "./transform"
5
+ export * from "./variant"
6
+ export * from "./display-reactor"