@ic-reactor/core 3.0.3-beta.4 → 3.0.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.
Files changed (65) hide show
  1. package/README.md +6 -4
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +2 -3
  4. package/dist/client.js.map +1 -1
  5. package/dist/display/types.d.ts +4 -2
  6. package/dist/display/types.d.ts.map +1 -1
  7. package/dist/display/visitor.d.ts +2 -1
  8. package/dist/display/visitor.d.ts.map +1 -1
  9. package/dist/display/visitor.js +146 -121
  10. package/dist/display/visitor.js.map +1 -1
  11. package/dist/display-reactor.d.ts +9 -41
  12. package/dist/display-reactor.d.ts.map +1 -1
  13. package/dist/display-reactor.js +5 -41
  14. package/dist/display-reactor.js.map +1 -1
  15. package/dist/reactor.d.ts +17 -1
  16. package/dist/reactor.d.ts.map +1 -1
  17. package/dist/reactor.js +60 -44
  18. package/dist/reactor.js.map +1 -1
  19. package/dist/types/display-reactor.d.ts +2 -2
  20. package/dist/types/display-reactor.d.ts.map +1 -1
  21. package/dist/types/reactor.d.ts +8 -9
  22. package/dist/types/reactor.d.ts.map +1 -1
  23. package/dist/types/transform.d.ts +1 -1
  24. package/dist/types/transform.d.ts.map +1 -1
  25. package/dist/utils/helper.d.ts +20 -1
  26. package/dist/utils/helper.d.ts.map +1 -1
  27. package/dist/utils/helper.js +37 -6
  28. package/dist/utils/helper.js.map +1 -1
  29. package/dist/utils/index.d.ts +1 -0
  30. package/dist/utils/index.d.ts.map +1 -1
  31. package/dist/utils/index.js +1 -0
  32. package/dist/utils/index.js.map +1 -1
  33. package/dist/utils/zod.d.ts +34 -0
  34. package/dist/utils/zod.d.ts.map +1 -0
  35. package/dist/utils/zod.js +39 -0
  36. package/dist/utils/zod.js.map +1 -0
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.d.ts.map +1 -1
  39. package/dist/version.js +2 -1
  40. package/dist/version.js.map +1 -1
  41. package/package.json +7 -6
  42. package/src/client.ts +571 -0
  43. package/src/display/helper.ts +92 -0
  44. package/src/display/index.ts +3 -0
  45. package/src/display/types.ts +91 -0
  46. package/src/display/visitor.ts +415 -0
  47. package/src/display-reactor.ts +361 -0
  48. package/src/errors/index.ts +246 -0
  49. package/src/index.ts +8 -0
  50. package/src/reactor.ts +461 -0
  51. package/src/types/client.ts +110 -0
  52. package/src/types/display-reactor.ts +73 -0
  53. package/src/types/index.ts +6 -0
  54. package/src/types/reactor.ts +188 -0
  55. package/src/types/result.ts +50 -0
  56. package/src/types/transform.ts +29 -0
  57. package/src/types/variant.ts +39 -0
  58. package/src/utils/agent.ts +201 -0
  59. package/src/utils/candid.ts +112 -0
  60. package/src/utils/constants.ts +12 -0
  61. package/src/utils/helper.ts +155 -0
  62. package/src/utils/index.ts +5 -0
  63. package/src/utils/polling.ts +330 -0
  64. package/src/utils/zod.ts +56 -0
  65. package/src/version.ts +5 -0
@@ -0,0 +1,361 @@
1
+ import { Reactor } from "./reactor"
2
+ import {
3
+ didToDisplayCodec,
4
+ transformArgsWithCodec,
5
+ transformResultWithCodec,
6
+ didTypeFromArray,
7
+ ActorDisplayCodec,
8
+ } from "./display"
9
+ import {
10
+ ActorMethodParameters,
11
+ ActorMethodReturnType,
12
+ FunctionName,
13
+ ReactorArgs,
14
+ ReactorReturnOk,
15
+ ActorMethodCodecs,
16
+ BaseActor,
17
+ TransformKey,
18
+ } from "./types/reactor"
19
+ import { extractOkResult } from "./utils/helper"
20
+ import { ValidationError } from "./errors"
21
+ import {
22
+ DisplayReactorParameters,
23
+ DisplayValidator,
24
+ ValidationResult,
25
+ Validator,
26
+ } from "./types/display-reactor"
27
+
28
+ // ============================================================================
29
+ // DisplayReactor
30
+ // ============================================================================
31
+
32
+ /**
33
+ * DisplayReactor provides automatic type transformations between Candid and
34
+ * display-friendly types, plus optional argument validation.
35
+ *
36
+ * ### Type Transformations
37
+ * - `bigint` → `string` (for JSON/UI display)
38
+ * - `Principal` → `string` (text representation)
39
+ * - `[T] | []` → `T | null` (optional unwrapping)
40
+ * - Small blobs → hex strings
41
+ *
42
+ * ### Validation (Optional)
43
+ * Register validators to check arguments before canister calls.
44
+ * Validators receive **display types** (strings), making them perfect for
45
+ * form validation.
46
+ *
47
+ * @typeParam A - The actor service type
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { DisplayReactor } from "@ic-reactor/core"
52
+ *
53
+ * const reactor = new DisplayReactor<_SERVICE>({
54
+ * clientManager,
55
+ * canisterId: "...",
56
+ * idlFactory,
57
+ * })
58
+ *
59
+ * // Optional: Add validation
60
+ * reactor.registerValidator("transfer", ([input]) => {
61
+ * if (!input.to) {
62
+ * return {
63
+ * success: false,
64
+ * issues: [{ path: ["to"], message: "Recipient is required" }]
65
+ * }
66
+ * }
67
+ * return { success: true }
68
+ * })
69
+ *
70
+ * // Call with display types
71
+ * await reactor.callMethod({
72
+ * functionName: "transfer",
73
+ * args: [{ to: "aaaaa-aa", amount: "100" }], // strings!
74
+ * })
75
+ * ```
76
+ */
77
+ export class DisplayReactor<
78
+ A = BaseActor,
79
+ T extends TransformKey = "display",
80
+ > extends Reactor<A, T> {
81
+ public readonly transform = "display" as T
82
+ private codecs: Map<
83
+ string,
84
+ { args: ActorDisplayCodec; result: ActorDisplayCodec }
85
+ > = new Map()
86
+ private validators: Map<string, Validator<any>> = new Map()
87
+
88
+ constructor(config: DisplayReactorParameters<A>) {
89
+ super(config)
90
+ this.initializeCodecs()
91
+
92
+ // Register initial validators if provided
93
+ if (config.validators) {
94
+ for (const [methodName, validator] of Object.entries(config.validators)) {
95
+ if (validator) {
96
+ this.validators.set(methodName, validator as Validator)
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Initialize codecs from IDL factory for automatic type transformations
104
+ */
105
+ private initializeCodecs() {
106
+ try {
107
+ const fields = this.getServiceInterface()?._fields
108
+ if (!fields) {
109
+ throw new Error("No fields found")
110
+ }
111
+ for (const [methodName, funcType] of fields) {
112
+ // Generate args codec
113
+ const argsIdlType = didTypeFromArray(funcType.argTypes)
114
+ // Generate result codec
115
+ const retIdlType = didTypeFromArray(funcType.retTypes)
116
+ // Set codec in map
117
+ this.codecs.set(methodName, {
118
+ args: didToDisplayCodec(argsIdlType),
119
+ result: didToDisplayCodec(retIdlType),
120
+ })
121
+ }
122
+ } catch (error) {
123
+ console.error("Failed to initialize codecs:", error)
124
+ }
125
+ }
126
+
127
+ // ============================================================================
128
+ // Codec Methods
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Get a codec for a specific method.
133
+ * Returns the args and result codecs for bidirectional transformation.
134
+ * @param methodName - The name of the method
135
+ * @returns Object with args and result codecs, or null if not found
136
+ */
137
+ public getCodec<M extends FunctionName<A>>(
138
+ methodName: M
139
+ ): ActorMethodCodecs<A, M> | null {
140
+ const cached = this.codecs.get(methodName)
141
+ if (cached) {
142
+ return cached as ActorMethodCodecs<A, M>
143
+ }
144
+
145
+ return null
146
+ }
147
+
148
+ // ============================================================================
149
+ // Validation Methods
150
+ // ============================================================================
151
+
152
+ /**
153
+ * Register a validator for a specific method.
154
+ * Validators receive display types (strings for Principal/bigint).
155
+ *
156
+ * @param methodName - The name of the method to validate
157
+ * @param validator - The validator function receiving display types
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * // input.to is string, input.amount is string
162
+ * reactor.registerValidator("transfer", ([input]) => {
163
+ * if (!/^\d+$/.test(input.amount)) {
164
+ * return {
165
+ * success: false,
166
+ * issues: [{ path: ["amount"], message: "Must be a valid number" }]
167
+ * }
168
+ * }
169
+ * return { success: true }
170
+ * })
171
+ * ```
172
+ */
173
+ registerValidator<M extends FunctionName<A>>(
174
+ methodName: M,
175
+ validator: DisplayValidator<A, M>
176
+ ): void {
177
+ this.validators.set(methodName, validator)
178
+ }
179
+
180
+ /**
181
+ * Unregister a validator for a specific method.
182
+ */
183
+ unregisterValidator<M extends FunctionName<A>>(methodName: M): void {
184
+ this.validators.delete(methodName)
185
+ }
186
+
187
+ /**
188
+ * Check if a method has a registered validator.
189
+ */
190
+ hasValidator<M extends FunctionName<A>>(methodName: M): boolean {
191
+ return this.validators.has(methodName)
192
+ }
193
+
194
+ /**
195
+ * Validate arguments without calling the canister.
196
+ * Arguments are in display format (strings for Principal/bigint).
197
+ * Useful for form validation before submission.
198
+ *
199
+ * @param methodName - The name of the method
200
+ * @param args - The display-type arguments to validate
201
+ * @returns ValidationResult indicating success or failure
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // Validate form data before submission
206
+ * const result = await reactor.validate("transfer", [{
207
+ * to: formData.recipient, // string
208
+ * amount: formData.amount, // string
209
+ * }])
210
+ *
211
+ * if (!result.success) {
212
+ * result.issues.forEach(issue => {
213
+ * form.setError(issue.path[0], issue.message)
214
+ * })
215
+ * }
216
+ * ```
217
+ */
218
+ async validate<M extends FunctionName<A>>(
219
+ methodName: M,
220
+ args: ReactorArgs<A, M, T>
221
+ ): Promise<ValidationResult> {
222
+ const validator = this.validators.get(methodName)
223
+ if (!validator) {
224
+ return { success: true }
225
+ }
226
+
227
+ return validator(args)
228
+ }
229
+
230
+ /**
231
+ * Call a method with async validation support.
232
+ * Use this instead of callMethod() when you have async validators.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * // Async validator (e.g., check if address is blocked)
237
+ * reactor.registerValidator("transfer", async ([input]) => {
238
+ * const isBlocked = await checkBlocklist(input.to)
239
+ * if (isBlocked) {
240
+ * return {
241
+ * success: false,
242
+ * issues: [{ path: ["to"], message: "Address is blocked" }]
243
+ * }
244
+ * }
245
+ * return { success: true }
246
+ * })
247
+ *
248
+ * await reactor.callMethodWithValidation({
249
+ * functionName: "transfer",
250
+ * args: [{ to: "...", amount: "100" }],
251
+ * })
252
+ * ```
253
+ */
254
+ async callMethodWithValidation<M extends FunctionName<A>>(params: {
255
+ functionName: M
256
+ args?: ReactorArgs<A, M, T>
257
+ callConfig?: Parameters<
258
+ Reactor<A, "display">["callMethod"]
259
+ >[0]["callConfig"]
260
+ }): Promise<ReactorReturnOk<A, M, T>> {
261
+ // Run async validation first (on display types)
262
+ if (params.args) {
263
+ const result = await this.validate(params.functionName, params.args)
264
+ if (!result.success) {
265
+ throw new ValidationError(String(params.functionName), result.issues)
266
+ }
267
+ }
268
+
269
+ // Skip synchronous validation in transformArgs by temporarily removing validator
270
+ const validator = this.validators.get(params.functionName)
271
+ if (validator) {
272
+ this.validators.delete(params.functionName)
273
+ }
274
+
275
+ try {
276
+ // @ts-ignore
277
+ return await this.callMethod(params)
278
+ } finally {
279
+ // Restore validator
280
+ if (validator) {
281
+ this.validators.set(params.functionName, validator)
282
+ }
283
+ }
284
+ }
285
+
286
+ // ============================================================================
287
+ // Transform Methods
288
+ // ============================================================================
289
+
290
+ /**
291
+ * Transform arguments before calling the actor method.
292
+ * 1. Validates display-type args (if validator registered)
293
+ * 2. Converts Display → Candid
294
+ */
295
+ protected transformArgs<M extends FunctionName<A>>(
296
+ methodName: M,
297
+ args?: ReactorArgs<A, M, T>
298
+ ): ActorMethodParameters<A[M]> {
299
+ // 1. Validate FIRST (on display types)
300
+ const validator = this.validators.get(methodName)
301
+ const displayArgs = args as unknown as ReactorArgs<A, M, "display">
302
+
303
+ if (validator && displayArgs) {
304
+ const result = validator(displayArgs)
305
+
306
+ // Handle Promise (async validator)
307
+ if (
308
+ result &&
309
+ typeof (result as Promise<ValidationResult>).then === "function"
310
+ ) {
311
+ throw new Error(
312
+ `Async validators are not supported in callMethod(). ` +
313
+ `Use reactor.callMethodWithValidation() for async validation.`
314
+ )
315
+ }
316
+
317
+ const syncResult = result as ValidationResult
318
+ if (!syncResult.success) {
319
+ throw new ValidationError(String(methodName), syncResult.issues)
320
+ }
321
+ }
322
+
323
+ // 2. THEN transform: Display → Candid
324
+ if (this.codecs.has(methodName)) {
325
+ const codec = this.codecs.get(methodName)!
326
+ return transformArgsWithCodec<ActorMethodParameters<A[M]>>(
327
+ codec.args,
328
+ displayArgs
329
+ )
330
+ }
331
+ if (!args) {
332
+ return [] as unknown as ActorMethodParameters<A[M]>
333
+ }
334
+ return args as ActorMethodParameters<A[M]>
335
+ }
336
+
337
+ /**
338
+ * Transform the result after calling the actor method.
339
+ * Always extracts the Ok value from Result types (throws CanisterError on Err).
340
+ * Also converts Candid → Display format.
341
+ */
342
+ protected transformResult<M extends FunctionName<A>>(
343
+ methodName: M,
344
+ result: ActorMethodReturnType<A[M]>
345
+ ): ReactorReturnOk<A, M, T> {
346
+ let transformedResult = result
347
+ // 1. Apply display transformation to the FULL result
348
+ if (this.codecs.has(methodName)) {
349
+ const codec = this.codecs.get(methodName)!
350
+ transformedResult = transformResultWithCodec(codec.result, result)
351
+ }
352
+
353
+ // 2. Extract Ok value from the TRANSFORMED (or raw) result
354
+ // This handles { ok: T } / { err: E } from Motoko/Rust canisters
355
+ return extractOkResult(transformedResult) as unknown as ReactorReturnOk<
356
+ A,
357
+ M,
358
+ T
359
+ >
360
+ }
361
+ }
@@ -0,0 +1,246 @@
1
+ import { NullishType } from "../display/types"
2
+
3
+ /**
4
+ * Interface representing the generic shape of an API error.
5
+ */
6
+ export interface ApiError {
7
+ code: string
8
+ message: NullishType<string>
9
+ details: NullishType<Map<string, string>>
10
+ }
11
+
12
+ /**
13
+ * Error thrown when there's an issue calling the canister.
14
+ * This includes network errors, agent errors, canister not found, etc.
15
+ */
16
+ export class CallError extends Error {
17
+ public readonly cause?: unknown
18
+
19
+ constructor(message: string, cause?: unknown) {
20
+ super(message)
21
+ this.name = "CallError"
22
+ this.cause = cause
23
+
24
+ // Maintains proper stack trace for where our error was thrown
25
+ if (Error.captureStackTrace) {
26
+ Error.captureStackTrace(this, CallError)
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Error thrown when the canister returns an Err result.
33
+ * The `err` property contains the typed error value from the canister.
34
+ *
35
+ * It also supports accessing `code`, `message`, and `details` directly
36
+ * if the error object follows the common API error format or is a variant.
37
+ *
38
+ * @typeParam E - The type of the error value from the canister
39
+ */
40
+ export class CanisterError<E = unknown> extends Error {
41
+ /** The raw error value from the canister */
42
+ public readonly err: E
43
+ /** The error code, extracted from the error object or variant key */
44
+ public readonly code: string
45
+ /** Optional error details Map */
46
+ public readonly details: NullishType<Map<string, string>>
47
+
48
+ constructor(err: E) {
49
+ let code: string | undefined
50
+ let message: string | undefined
51
+ let details: NullishType<Map<string, string>> = undefined
52
+ let isApiShape = false
53
+
54
+ if (typeof err === "object" && err !== null) {
55
+ // 1. Check for structured ApiError shape (has code)
56
+ if ("code" in err && typeof err.code === "string") {
57
+ code = err.code
58
+ isApiShape = true
59
+ if ("message" in err && typeof err.message === "string") {
60
+ message = err.message
61
+ }
62
+ if ("details" in err) {
63
+ details = err.details as any
64
+ }
65
+ }
66
+ // 2. Check for ic-reactor transformed variant shape (_type)
67
+ else if ("_type" in err && typeof err._type === "string") {
68
+ code = err._type
69
+ }
70
+ // 3. Simple variant check (single key object)
71
+ else {
72
+ const keys = Object.keys(err)
73
+ if (keys.length === 1) {
74
+ code = keys[0]
75
+ }
76
+ }
77
+ }
78
+
79
+ const finalCode = code ?? "UNKNOWN_ERROR"
80
+ const finalMessage =
81
+ message ??
82
+ (typeof err === "object" && err !== null
83
+ ? JSON.stringify(
84
+ err,
85
+ (_, v) => (typeof v === "bigint" ? v.toString() : v),
86
+ 2
87
+ )
88
+ : String(err))
89
+
90
+ super(isApiShape ? finalMessage : `Canister Error: ${finalMessage}`)
91
+ this.name = "CanisterError"
92
+ this.err = err
93
+ this.code = finalCode
94
+ this.details = details
95
+
96
+ // Maintains proper stack trace for where our error was thrown
97
+ if (Error.captureStackTrace) {
98
+ Error.captureStackTrace(this, CanisterError)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Type guard to check if an error object follows the API error format.
104
+ */
105
+ static isApiError(error: unknown): error is ApiError {
106
+ if (typeof error !== "object" || error === null) {
107
+ return false
108
+ }
109
+
110
+ return "code" in error && "message" in error && "details" in error
111
+ }
112
+
113
+ /**
114
+ * Factory method to create a CanisterError from any error.
115
+ * If the input is already a CanisterError, it returns it.
116
+ * If it's an API error shape, it wraps it.
117
+ * Otherwise, it creates a new CanisterError with an "UNKNOWN_ERROR" code.
118
+ */
119
+ static create(error: unknown, message?: string): CanisterError {
120
+ if (error instanceof CanisterError) {
121
+ return error
122
+ }
123
+
124
+ if (CanisterError.isApiError(error)) {
125
+ return new CanisterError(error)
126
+ }
127
+
128
+ return new CanisterError({
129
+ code: "UNKNOWN_ERROR",
130
+ message:
131
+ error instanceof Error
132
+ ? error.message
133
+ : message || "An unknown error occurred",
134
+ details: undefined,
135
+ } as any)
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Type guard to check if an error is a CanisterError.
141
+ * Preserves the generic type E from the input when used in type narrowing.
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * // err is typed as CanisterError<TransferError> | CallError
146
+ * if (isCanisterError(err)) {
147
+ * // err.err is typed as TransferError (preserved!)
148
+ * console.log(err.err)
149
+ * }
150
+ * ```
151
+ */
152
+ export function isCanisterError<E>(
153
+ error: CanisterError<E> | CallError
154
+ ): error is CanisterError<E>
155
+ export function isCanisterError(error: unknown): error is CanisterError<unknown>
156
+ export function isCanisterError(
157
+ error: unknown
158
+ ): error is CanisterError<unknown> {
159
+ return error instanceof CanisterError
160
+ }
161
+
162
+ /**
163
+ * Type guard to check if an error is a CallError
164
+ */
165
+ export function isCallError(error: unknown): error is CallError {
166
+ return error instanceof CallError
167
+ }
168
+
169
+ // ============================================================================
170
+ // Validation Errors
171
+ // ============================================================================
172
+
173
+ /**
174
+ * Represents a single validation issue
175
+ */
176
+ export interface ValidationIssue {
177
+ /** Path to the invalid field (e.g., ["to", "amount"]) */
178
+ path: (string | number)[]
179
+ /** Human-readable error message */
180
+ message: string
181
+ /** Validation code (e.g., "required", "min_length") */
182
+ code?: string
183
+ }
184
+
185
+ /**
186
+ * Error thrown when argument validation fails before calling the canister.
187
+ * Contains detailed information about which fields failed validation.
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * try {
192
+ * await reactor.callMethod({
193
+ * functionName: "transfer",
194
+ * args: [{ to: "", amount: -100 }],
195
+ * })
196
+ * } catch (error) {
197
+ * if (isValidationError(error)) {
198
+ * console.log(error.issues)
199
+ * // [
200
+ * // { path: ["to"], message: "Recipient is required" },
201
+ * // { path: ["amount"], message: "Amount must be positive" }
202
+ * // ]
203
+ * }
204
+ * }
205
+ * ```
206
+ */
207
+ export class ValidationError extends Error {
208
+ /** Array of validation issues */
209
+ public readonly issues: ValidationIssue[]
210
+ /** The method name that failed validation */
211
+ public readonly methodName: string
212
+
213
+ constructor(methodName: string, issues: ValidationIssue[]) {
214
+ const messages = issues.map((i) => i.message).join(", ")
215
+ super(`Validation failed for "${methodName}": ${messages}`)
216
+ this.name = "ValidationError"
217
+ this.methodName = methodName
218
+ this.issues = issues
219
+
220
+ // Maintains proper stack trace for where our error was thrown
221
+ if (Error.captureStackTrace) {
222
+ Error.captureStackTrace(this, ValidationError)
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Get issues for a specific field path
228
+ */
229
+ getIssuesForPath(path: string): ValidationIssue[] {
230
+ return this.issues.filter((issue) => issue.path.includes(path))
231
+ }
232
+
233
+ /**
234
+ * Check if a specific field has errors
235
+ */
236
+ hasErrorForPath(path: string): boolean {
237
+ return this.issues.some((issue) => issue.path.includes(path))
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Type guard to check if an error is a ValidationError
243
+ */
244
+ export function isValidationError(error: unknown): error is ValidationError {
245
+ return error instanceof ValidationError
246
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./reactor"
2
+ export * from "./client"
3
+ export * from "./utils"
4
+ export * from "./display"
5
+ export * from "./display-reactor"
6
+ export * from "./types"
7
+ export * from "./errors"
8
+ export * from "./version"