@ic-reactor/core 3.0.0-beta.1 β†’ 3.0.0-beta.2

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 (2) hide show
  1. package/README.md +506 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # @ic-reactor/core
2
+
3
+ <div align="center">
4
+ <strong>The Core Library for Internet Computer Applications</strong>
5
+ <br><br>
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@ic-reactor/core.svg)](https://www.npmjs.com/package/@ic-reactor/core)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
10
+ </div>
11
+
12
+ ---
13
+
14
+ Framework-agnostic core library for building type-safe Internet Computer applications with [TanStack Query](https://tanstack.com/query) integration.
15
+
16
+ > **Note**: For React applications, use [`@ic-reactor/react`](../react) instead, which re-exports everything from this package plus React-specific hooks.
17
+
18
+ ## Features
19
+
20
+ - πŸ”’ **End-to-End Type Safety** β€” From Candid to your application
21
+ - ⚑ **TanStack Query Integration** β€” Automatic caching, background refetching, optimistic updates
22
+ - πŸ”„ **Auto Transformations** β€” `DisplayReactor` converts BigInt to string, Principal to text
23
+ - πŸ“¦ **Result Unwrapping** β€” Automatic `Ok`/`Err` handling from Candid Result types
24
+ - πŸ” **Internet Identity** β€” Built-in authentication with session restoration
25
+ - πŸ—οΈ **Multi-Canister Support** β€” Shared authentication across canisters
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ # Core library
31
+ npm install @ic-reactor/core @icp-sdk/core @tanstack/query-core
32
+
33
+ # Optional: For Internet Identity authentication
34
+ npm install @icp-sdk/auth
35
+ ```
36
+
37
+ ## Core Concepts
38
+
39
+ ### Architecture Overview
40
+
41
+ ```
42
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
43
+ β”‚ ClientManager │───▢│ Reactor │───▢│ TanStack Query β”‚
44
+ β”‚ (Agent + Auth) β”‚ β”‚ (Canister) β”‚ β”‚ (Caching Layer) β”‚
45
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
46
+ β”‚ β”‚
47
+ β”‚ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
48
+ β”‚ β”‚ Display β”‚
49
+ β”‚ β”‚ Reactor β”‚
50
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
51
+ β”‚ (Type Transforms)
52
+ β–Ό
53
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
54
+ β”‚ Internet β”‚
55
+ β”‚ Identity β”‚
56
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ### 1. Create ClientManager
62
+
63
+ The `ClientManager` handles the IC agent, authentication, and query client:
64
+
65
+ ```typescript
66
+ import { ClientManager } from "@ic-reactor/core"
67
+ import { QueryClient } from "@tanstack/query-core"
68
+
69
+ const queryClient = new QueryClient({
70
+ defaultOptions: {
71
+ queries: { staleTime: 5 * 60 * 1000 }, // 5 minutes
72
+ },
73
+ })
74
+
75
+ const clientManager = new ClientManager({
76
+ queryClient,
77
+ withProcessEnv: true, // Reads DFX_NETWORK from environment
78
+ })
79
+
80
+ // Initialize agent and restore session
81
+ await clientManager.initialize()
82
+ ```
83
+
84
+ ### 2. Create Reactor
85
+
86
+ The `Reactor` class wraps a canister with type-safe methods and caching:
87
+
88
+ ```typescript
89
+ import { Reactor } from "@ic-reactor/core"
90
+ import { idlFactory, type _SERVICE } from "./declarations/my_canister"
91
+
92
+ const backend = new Reactor<_SERVICE>({
93
+ clientManager,
94
+ idlFactory,
95
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
96
+ })
97
+ ```
98
+
99
+ ### 3. Call Methods
100
+
101
+ ```typescript
102
+ // Direct call (no caching)
103
+ const greeting = await backend.callMethod({
104
+ functionName: "greet",
105
+ args: ["World"],
106
+ })
107
+
108
+ // Fetch with caching
109
+ const cachedGreeting = await backend.fetchQuery({
110
+ functionName: "greet",
111
+ args: ["World"],
112
+ })
113
+
114
+ // Get from cache (no network call)
115
+ const fromCache = backend.getQueryData({
116
+ functionName: "greet",
117
+ args: ["World"],
118
+ })
119
+
120
+ // Invalidate cache
121
+ backend.invalidateQueries({ functionName: "greet" })
122
+ ```
123
+
124
+ ## ClientManager API
125
+
126
+ ### Constructor Options
127
+
128
+ ```typescript
129
+ interface ClientManagerParameters {
130
+ queryClient: QueryClient // TanStack Query client
131
+ port?: number // Local replica port (default: 4943)
132
+ withLocalEnv?: boolean // Force local network
133
+ withProcessEnv?: boolean // Read DFX_NETWORK from env
134
+ agentOptions?: HttpAgentOptions // Custom agent options
135
+ authClient?: AuthClient // Pre-configured auth client
136
+ }
137
+ ```
138
+
139
+ ### Authentication Methods
140
+
141
+ ```typescript
142
+ // Initialize agent and restore previous session
143
+ await clientManager.initialize()
144
+
145
+ // Trigger login flow (opens Internet Identity)
146
+ await clientManager.login({
147
+ identityProvider: "https://identity.ic0.app", // optional, auto-detected
148
+ onSuccess: () => console.log("Logged in!"),
149
+ onError: (error) => console.error(error),
150
+ })
151
+
152
+ // Logout and revert to anonymous identity
153
+ await clientManager.logout()
154
+
155
+ // Manually authenticate (restore session)
156
+ const identity = await clientManager.authenticate()
157
+ ```
158
+
159
+ ### State Subscriptions
160
+
161
+ ```typescript
162
+ // Subscribe to agent state changes
163
+ const unsubAgent = clientManager.subscribeAgentState((state) => {
164
+ console.log("Agent state:", state.isInitialized, state.network)
165
+ })
166
+
167
+ // Subscribe to auth state changes
168
+ const unsubAuth = clientManager.subscribeAuthState((state) => {
169
+ console.log("Auth state:", state.isAuthenticated, state.identity)
170
+ })
171
+
172
+ // Subscribe to identity changes
173
+ const unsubIdentity = clientManager.subscribe((identity) => {
174
+ console.log("New identity:", identity.getPrincipal().toText())
175
+ })
176
+
177
+ // Cleanup
178
+ unsubAgent()
179
+ unsubAuth()
180
+ unsubIdentity()
181
+ ```
182
+
183
+ ### Properties
184
+
185
+ ```typescript
186
+ clientManager.agent // HttpAgent instance
187
+ clientManager.agentState // { isInitialized, isInitializing, error, network, isLocalhost }
188
+ clientManager.authState // { identity, isAuthenticated, isAuthenticating, error }
189
+ clientManager.queryClient // TanStack QueryClient
190
+ clientManager.network // "ic" | "local"
191
+ clientManager.isLocal // boolean
192
+ ```
193
+
194
+ ## Reactor API
195
+
196
+ ### Constructor Options
197
+
198
+ ```typescript
199
+ interface ReactorParameters<A> {
200
+ clientManager: ClientManager
201
+ idlFactory: IDL.InterfaceFactory
202
+ canisterId: string | Principal
203
+ name?: string // Optional display name
204
+ pollingOptions?: PollingOptions // Custom polling for update calls
205
+ }
206
+ ```
207
+
208
+ ### Core Methods
209
+
210
+ ```typescript
211
+ // Call a canister method (auto-detects query vs update)
212
+ const result = await reactor.callMethod({
213
+ functionName: "my_method",
214
+ args: [arg1, arg2],
215
+ callConfig: { effectiveCanisterId: ... }, // optional
216
+ })
217
+
218
+ // Fetch and cache data
219
+ const data = await reactor.fetchQuery({
220
+ functionName: "get_data",
221
+ args: [],
222
+ })
223
+
224
+ // Get cached data (synchronous, no network)
225
+ const cached = reactor.getQueryData({
226
+ functionName: "get_data",
227
+ args: [],
228
+ })
229
+
230
+ // Invalidate cached queries
231
+ reactor.invalidateQueries() // all queries for this canister
232
+ reactor.invalidateQueries({ functionName: "get_data" }) // specific method
233
+ reactor.invalidateQueries({ functionName: "get_user", args: ["user-1"] }) // specific args
234
+
235
+ // Get query options for TanStack Query
236
+ const options = reactor.getQueryOptions({ functionName: "get_data" })
237
+ ```
238
+
239
+ ### Properties
240
+
241
+ ```typescript
242
+ reactor.canisterId // Principal
243
+ reactor.service // IDL.ServiceClass
244
+ reactor.queryClient // TanStack QueryClient
245
+ reactor.agent // HttpAgent
246
+ reactor.name // string
247
+ ```
248
+
249
+ ## DisplayReactor
250
+
251
+ `DisplayReactor` extends `Reactor` with automatic type transformations for UI-friendly values:
252
+
253
+ ### Type Transformations
254
+
255
+ | Candid Type | Reactor (raw) | DisplayReactor |
256
+ | -------------------------------- | ------------- | -------------- |
257
+ | `nat`, `int` | `bigint` | `string` |
258
+ | `nat8/16/32/64`, `int8/16/32/64` | `bigint` | `string` |
259
+ | `Principal` | `Principal` | `string` |
260
+ | `vec nat8` (≀32 bytes) | `Uint8Array` | `string` (hex) |
261
+ | `Result<Ok, Err>` | Unwrapped | Unwrapped |
262
+
263
+ ### Usage
264
+
265
+ ```typescript
266
+ import { DisplayReactor } from "@ic-reactor/core"
267
+
268
+ const backend = new DisplayReactor<_SERVICE>({
269
+ clientManager,
270
+ idlFactory,
271
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
272
+ })
273
+
274
+ // Args and results use display-friendly types
275
+ const balance = await backend.callMethod({
276
+ functionName: "icrc1_balance_of",
277
+ args: [{ owner: "aaaaa-aa", subaccount: [] }], // string instead of Principal
278
+ })
279
+ // balance is "100000000" (string) instead of 100000000n (bigint)
280
+ ```
281
+
282
+ ### Form Validation
283
+
284
+ `DisplayReactor` supports validators for mutation arguments:
285
+
286
+ ```typescript
287
+ import { DisplayReactor, ValidationError } from "@ic-reactor/core"
288
+
289
+ const backend = new DisplayReactor<_SERVICE>({
290
+ clientManager,
291
+ idlFactory,
292
+ canisterId: "...",
293
+ validators: {
294
+ transfer: (args) => {
295
+ const [{ to, amount }] = args
296
+ const issues = []
297
+
298
+ if (!to || to.length < 5) {
299
+ issues.push({ path: ["to"], message: "Invalid recipient" })
300
+ }
301
+ if (!amount || parseFloat(amount) <= 0) {
302
+ issues.push({ path: ["amount"], message: "Amount must be positive" })
303
+ }
304
+
305
+ return issues.length > 0 ? { success: false, issues } : { success: true }
306
+ },
307
+ },
308
+ })
309
+
310
+ // Validate before calling
311
+ const result = await backend.validate("transfer", [{ to: "", amount: "0" }])
312
+ if (!result.success) {
313
+ console.log(result.issues) // [{ path: ["to"], message: "Invalid recipient" }, ...]
314
+ }
315
+
316
+ // Or call with validation (throws ValidationError on failure)
317
+ try {
318
+ await backend.callMethodWithValidation({
319
+ functionName: "transfer",
320
+ args: [{ to: "", amount: "0" }],
321
+ })
322
+ } catch (error) {
323
+ if (error instanceof ValidationError) {
324
+ console.log(error.issues)
325
+ }
326
+ }
327
+ ```
328
+
329
+ ## Error Handling
330
+
331
+ ### Error Types
332
+
333
+ ```typescript
334
+ import {
335
+ CallError,
336
+ CanisterError,
337
+ ValidationError,
338
+ isCallError,
339
+ isCanisterError,
340
+ isValidationError,
341
+ } from "@ic-reactor/core"
342
+ ```
343
+
344
+ | Error Type | Description |
345
+ | ------------------ | -------------------------------------------------------- |
346
+ | `CallError` | Network/agent errors (canister not found, timeout, etc.) |
347
+ | `CanisterError<E>` | Canister returned an `Err` result |
348
+ | `ValidationError` | Argument validation failed (DisplayReactor) |
349
+
350
+ ### Handling Errors
351
+
352
+ ```typescript
353
+ try {
354
+ await backend.callMethod({
355
+ functionName: "transfer",
356
+ args: [{ to: principal, amount: 100n }],
357
+ })
358
+ } catch (error) {
359
+ if (isCanisterError(error)) {
360
+ // Business logic error from canister
361
+ console.log("Canister error:", error.code, error.err)
362
+ // error.err is typed based on your Candid Result type
363
+ } else if (isCallError(error)) {
364
+ // Network/agent error
365
+ console.log("Network error:", error.message)
366
+ } else if (isValidationError(error)) {
367
+ // Validation error (DisplayReactor only)
368
+ console.log("Validation failed:", error.issues)
369
+ }
370
+ }
371
+ ```
372
+
373
+ ### CanisterError Properties
374
+
375
+ ```typescript
376
+ interface CanisterError<E> {
377
+ err: E // The raw error value from canister
378
+ code: string // Error code (from variant key or "code" field)
379
+ message: string // Human-readable message
380
+ details?: Map<string, string> // Optional details
381
+ }
382
+ ```
383
+
384
+ ## Utilities
385
+
386
+ ### Result Unwrapping
387
+
388
+ Results are automatically unwrapped. The `extractOkResult` utility handles both uppercase (`Ok`/`Err`) and lowercase (`ok`/`err`) variants:
389
+
390
+ ```typescript
391
+ import { extractOkResult } from "@ic-reactor/core"
392
+
393
+ // Candid: Result<Text, TransferError>
394
+ // Returns the Ok value or throws CanisterError with the Err value
395
+ const result = extractOkResult({ Ok: "success" }) // "success"
396
+ const result2 = extractOkResult({ ok: "success" }) // "success"
397
+ ```
398
+
399
+ ### Query Key Generation
400
+
401
+ ```typescript
402
+ const queryKey = reactor.generateQueryKey({
403
+ functionName: "get_user",
404
+ args: ["user-123"],
405
+ })
406
+ // ["canister-id", "get_user", "serialized-args"]
407
+ ```
408
+
409
+ ## TypeScript Types
410
+
411
+ ### Actor Types
412
+
413
+ ```typescript
414
+ import type {
415
+ FunctionName, // Method names from actor service
416
+ ActorMethodParameters, // Parameter types for a method
417
+ ActorMethodReturnType, // Return type for a method
418
+ ReactorArgs, // Args with optional transforms
419
+ ReactorReturnOk, // Return type (Ok extracted from Result)
420
+ ReactorReturnErr, // Error type (Err from Result)
421
+ } from "@ic-reactor/core"
422
+ ```
423
+
424
+ ### State Types
425
+
426
+ ```typescript
427
+ import type { AgentState, AuthState } from "@ic-reactor/core"
428
+
429
+ interface AgentState {
430
+ isInitialized: boolean
431
+ isInitializing: boolean
432
+ error: Error | undefined
433
+ network: "ic" | "local" | undefined
434
+ isLocalhost: boolean
435
+ }
436
+
437
+ interface AuthState {
438
+ identity: Identity | null
439
+ isAuthenticated: boolean
440
+ isAuthenticating: boolean
441
+ error: Error | undefined
442
+ }
443
+ ```
444
+
445
+ ## Advanced Usage
446
+
447
+ ### Multiple Canisters
448
+
449
+ ```typescript
450
+ const clientManager = new ClientManager({ queryClient, withProcessEnv: true })
451
+
452
+ // All reactors share the same agent and authentication
453
+ const backend = new Reactor<Backend>({
454
+ clientManager,
455
+ idlFactory: backendIdl,
456
+ canisterId: "...",
457
+ })
458
+ const ledger = new DisplayReactor<Ledger>({
459
+ clientManager,
460
+ idlFactory: ledgerIdl,
461
+ canisterId: "...",
462
+ })
463
+ const nft = new Reactor<NFT>({
464
+ clientManager,
465
+ idlFactory: nftIdl,
466
+ canisterId: "...",
467
+ })
468
+
469
+ // Login once, all canisters use the same identity
470
+ await clientManager.login()
471
+ ```
472
+
473
+ ### Custom Polling Options
474
+
475
+ ```typescript
476
+ const backend = new Reactor<_SERVICE>({
477
+ clientManager,
478
+ idlFactory,
479
+ canisterId: "...",
480
+ pollingOptions: {
481
+ maxRetries: 5,
482
+ strategyFactory: () => /* custom strategy */,
483
+ },
484
+ })
485
+ ```
486
+
487
+ ### Direct Agent Access
488
+
489
+ ```typescript
490
+ // Get subnet ID
491
+ const subnetId = await backend.subnetId()
492
+
493
+ // Read subnet state
494
+ const state = await backend.subnetState({ paths: [...] })
495
+
496
+ // Access underlying agent
497
+ const agent = backend.agent
498
+ ```
499
+
500
+ ## Documentation
501
+
502
+ For comprehensive guides and API reference, visit the [documentation site](https://b3pay.github.io/ic-reactor/v3).
503
+
504
+ ## License
505
+
506
+ MIT Β© [Behrad Deylami](https://github.com/b3hr4d)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/core",
3
- "version": "3.0.0-beta.1",
3
+ "version": "3.0.0-beta.2",
4
4
  "description": "IC Reactor Core Library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",