@calimero-network/agent-skills 0.2.0 → 0.3.0
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/SKILL.md +7 -2
- package/package.json +2 -2
- package/skills/calimero-client-js/SKILL.md +11 -1
- package/skills/calimero-client-js/references/websocket-events.md +33 -0
- package/skills/calimero-client-js/rules/token-refresh.md +59 -21
- package/skills/calimero-client-py/SKILL.md +4 -0
- package/skills/calimero-desktop/SKILL.md +7 -1
- package/skills/calimero-desktop/references/sso-integration.md +49 -22
- package/skills/calimero-node/SKILL.md +6 -0
- package/skills/calimero-rust-sdk/rules/state-derives.md +46 -0
- package/skills/calimero-sdk-js/SKILL.md +137 -0
- package/skills/calimero-sdk-js/references/build-pipeline.md +98 -0
- package/skills/calimero-sdk-js/references/collections.md +132 -0
- package/skills/calimero-sdk-js/references/events.md +59 -0
- package/skills/calimero-sdk-js/rules/crdt-only-state.md +47 -0
- package/skills/calimero-sdk-js/rules/no-console-log.md +38 -0
- package/skills/calimero-sdk-js/rules/view-decorator.md +46 -0
package/SKILL.md
CHANGED
|
@@ -6,8 +6,9 @@ This package provides AI agent skills for building on the Calimero Network stack
|
|
|
6
6
|
|
|
7
7
|
| Skill | Install command | When to use |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications |
|
|
10
|
-
| `calimero-
|
|
9
|
+
| `calimero-rust-sdk` | `npx @calimero-network/agent-skills calimero-rust-sdk` | Building Rust WASM applications that run on a node |
|
|
10
|
+
| `calimero-sdk-js` | `npx @calimero-network/agent-skills calimero-sdk-js` | Building TypeScript/JS WASM applications that run on a node |
|
|
11
|
+
| `calimero-client-js` | `npx @calimero-network/agent-skills calimero-client-js` | Browser/Node.js frontends connecting to a node (not building apps) |
|
|
11
12
|
| `calimero-registry` | `npx @calimero-network/agent-skills calimero-registry` | Signing and publishing apps to the registry |
|
|
12
13
|
| `calimero-desktop` | `npx @calimero-network/agent-skills calimero-desktop` | Integrating apps with Calimero Desktop SSO |
|
|
13
14
|
| `calimero-node` | `npx @calimero-network/agent-skills calimero-node` | Node operators and meroctl scripting |
|
|
@@ -22,11 +23,15 @@ Skills should be loaded when the following are detected in the project:
|
|
|
22
23
|
| Signal | Load skill |
|
|
23
24
|
| --- | --- |
|
|
24
25
|
| `calimero-sdk` in `Cargo.toml` | `calimero-rust-sdk` |
|
|
26
|
+
| `@calimero-network/calimero-sdk-js` in `package.json` | `calimero-sdk-js` |
|
|
27
|
+
| `@State` / `@Logic` decorators in TypeScript source | `calimero-sdk-js` |
|
|
28
|
+
| `calimero-sdk build` in any script | `calimero-sdk-js` |
|
|
25
29
|
| `@calimero-network/calimero-client` in `package.json` | `calimero-client-js` |
|
|
26
30
|
| `@calimero-network/mero-js` in `package.json` | `calimero-client-js` |
|
|
27
31
|
| `mero-sign` in any script or Makefile | `calimero-registry` |
|
|
28
32
|
| `calimero-registry` CLI usage | `calimero-registry` |
|
|
29
33
|
| `access_token` read from `window.location.hash` | `calimero-desktop` |
|
|
34
|
+
| `readDesktopSSO` / `hash.get('access_token')` pattern in frontend code | `calimero-desktop` |
|
|
30
35
|
| `merobox` in `package.json` or `requirements.txt` | `calimero-merobox` |
|
|
31
36
|
| `calimero-client-py` in `requirements.txt` or `pyproject.toml` | `calimero-client-py` |
|
|
32
37
|
| `calimero-abi-codegen` or `abi.json` in project | `calimero-abi-codegen` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calimero-network/agent-skills",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI agent skills for Calimero Network development — Rust SDK, JS client, registry publishing, desktop SSO, and more.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI agent skills for Calimero Network development — Rust SDK, JS SDK, JS client, registry publishing, desktop SSO, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calimero",
|
|
7
7
|
"agent-skills",
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# calimero-client-js — Agent Instructions
|
|
2
2
|
|
|
3
|
-
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node
|
|
3
|
+
You are helping a developer connect a **browser or Node.js frontend** to a Calimero node
|
|
4
|
+
using `@calimero-network/calimero-client` or `@calimero-network/mero-js`.
|
|
5
|
+
|
|
6
|
+
> **NOT this skill** if the developer is building the application logic that *runs on the
|
|
7
|
+
> node* in TypeScript — that is `calimero-sdk-js` (`@calimero-network/calimero-sdk-js`).
|
|
8
|
+
> This skill is for the *client* side: auth, RPC calls, and WebSocket subscriptions from
|
|
9
|
+
> a browser or backend service.
|
|
4
10
|
|
|
5
11
|
## Package versions
|
|
6
12
|
|
|
@@ -9,6 +15,10 @@ You are helping a developer connect a **browser or Node.js frontend** to a Calim
|
|
|
9
15
|
| `@calimero-network/calimero-client` | latest | Stable client for browser/Node — auth, RPC, WebSocket |
|
|
10
16
|
| `@calimero-network/mero-js` | `>=2.0.0-beta.1` | v2 API — all request fields are **camelCase** |
|
|
11
17
|
|
|
18
|
+
**Which to use:** new projects should prefer `@calimero-network/mero-js` v2. If you are
|
|
19
|
+
maintaining an existing codebase that uses `calimero-client`, check for snake_case field
|
|
20
|
+
names before migrating — do not mix both packages in the same project.
|
|
21
|
+
|
|
12
22
|
## Critical: mero-js v2 uses camelCase
|
|
13
23
|
|
|
14
24
|
v2 changed all request field names from `snake_case` to `camelCase`.
|
|
@@ -97,6 +97,39 @@ ws.disconnect();
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## Connection errors and reconnection
|
|
101
|
+
|
|
102
|
+
`WsSubscriptionsClient` does **not** reconnect automatically. If the connection drops
|
|
103
|
+
or the initial connect fails, you must retry manually.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
async function connectWithRetry(ws: WsSubscriptionsClient, contextId: string, retries = 3): Promise<void> {
|
|
107
|
+
for (let i = 0; i < retries; i++) {
|
|
108
|
+
try {
|
|
109
|
+
await ws.connect();
|
|
110
|
+
ws.subscribe([contextId]);
|
|
111
|
+
return;
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
if (err?.status === 401) {
|
|
114
|
+
// Token expired — cannot reconnect until re-authenticated
|
|
115
|
+
clientLogout();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (i === retries - 1) throw err;
|
|
119
|
+
await new Promise(r => setTimeout(r, 1000 * (i + 1))); // exponential backoff
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To detect drops after the connection is established, check if events stop arriving and
|
|
126
|
+
reconnect periodically, or listen to `ws.onClose` if exposed.
|
|
127
|
+
|
|
128
|
+
> WebSocket tokens are **not** auto-refreshed unlike `rpcClient` RPC calls.
|
|
129
|
+
> See `rules/token-refresh.md` for details.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
100
133
|
## React hook pattern
|
|
101
134
|
|
|
102
135
|
```typescript
|
|
@@ -1,41 +1,79 @@
|
|
|
1
|
-
# Rule:
|
|
1
|
+
# Rule: rpcClient handles token refresh automatically — but not all 401s
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`rpcClient` (from `@calimero-network/calimero-client`) automatically retries with a
|
|
4
|
+
refreshed token when the server returns `401 token_expired`. **You do not need to
|
|
5
|
+
implement a refresh wrapper around `rpcClient.execute()` calls.**
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
What you DO need to handle: 401s that cannot be refreshed.
|
|
8
|
+
|
|
9
|
+
## WRONG — manual refresh wrapper (not needed for rpcClient):
|
|
6
10
|
|
|
7
11
|
```typescript
|
|
8
|
-
|
|
12
|
+
// ✗ Don't do this — rpcClient already retries on token_expired
|
|
13
|
+
async function callWithRefresh<T>(fn: () => Promise<T>) {
|
|
9
14
|
try {
|
|
10
|
-
return await
|
|
15
|
+
return await fn();
|
|
11
16
|
} catch (err: any) {
|
|
12
|
-
if (err?.code === 401
|
|
13
|
-
await refreshAccessToken();
|
|
14
|
-
return
|
|
17
|
+
if (err?.code === 401) {
|
|
18
|
+
await refreshAccessToken(); // rpcClient already does this
|
|
19
|
+
return fn();
|
|
15
20
|
}
|
|
16
21
|
throw err;
|
|
17
22
|
}
|
|
18
23
|
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CORRECT — handle only non-refreshable auth failures:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const response = await rpcClient.execute({ ... });
|
|
30
|
+
|
|
31
|
+
if (response.error) {
|
|
32
|
+
const authError = response.error.headers?.['x-auth-error'];
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
if (response.error.code === 401) {
|
|
35
|
+
if (authError === 'token_revoked' || authError === 'invalid_token') {
|
|
36
|
+
// Token is invalid — cannot be refreshed. Send user to login.
|
|
37
|
+
clientLogout();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// token_expired: rpcClient already retried and the retry failed.
|
|
41
|
+
// This means the refresh token is also expired — send to login.
|
|
42
|
+
clientLogout();
|
|
25
43
|
return;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
localStorage.setItem('calimero_jwt', JSON.stringify(newTokens));
|
|
46
|
+
throw new Error(response.error.error.cause.info?.message ?? 'Unknown error');
|
|
30
47
|
}
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
##
|
|
50
|
+
## WebSocket connections are different — no auto-refresh
|
|
51
|
+
|
|
52
|
+
`WsSubscriptionsClient` does **not** auto-refresh tokens. If the token expires while
|
|
53
|
+
a WebSocket connection is open, events will stop arriving silently. Reconnect manually:
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
```typescript
|
|
56
|
+
async function connectWithAuth() {
|
|
57
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
58
|
+
try {
|
|
59
|
+
await ws.connect();
|
|
60
|
+
ws.subscribe([getContextId()!]);
|
|
61
|
+
ws.addCallback(handleEvent);
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
if (err?.status === 401) {
|
|
64
|
+
// Token expired before WS connect — logout and re-authenticate
|
|
65
|
+
clientLogout();
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
38
71
|
|
|
39
|
-
##
|
|
72
|
+
## Summary
|
|
40
73
|
|
|
41
|
-
|
|
74
|
+
| Scenario | Handled by |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `401 token_expired` on RPC call | `rpcClient` automatically (transparent retry) |
|
|
77
|
+
| `401 token_revoked` on RPC call | You — redirect to login |
|
|
78
|
+
| `401 invalid_token` on RPC call | You — redirect to login |
|
|
79
|
+
| Auth failure on WebSocket connect | You — catch and redirect to login |
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
You are helping a developer use the **Calimero Python client** (`calimero-client-py`) to
|
|
4
4
|
interact with a Calimero node from Python — automation scripts, backend services, or CLI tools.
|
|
5
5
|
|
|
6
|
+
> **NOT this skill** if the developer is building the application logic that runs on the
|
|
7
|
+
> node — that is `calimero-rust-sdk` (Rust/WASM) or `calimero-sdk-js` (TypeScript/WASM).
|
|
8
|
+
> This skill is for Python code that *calls* the node from outside.
|
|
9
|
+
|
|
6
10
|
## What it is
|
|
7
11
|
|
|
8
12
|
`calimero-client-py` is a Python package built with PyO3 (Rust bindings). It provides:
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# calimero-desktop — Agent Instructions
|
|
2
2
|
|
|
3
|
-
You are helping a developer integrate their app frontend with **Calimero Desktop SSO
|
|
3
|
+
You are helping a developer integrate their app frontend with **Calimero Desktop SSO** —
|
|
4
|
+
the flow where users open an app from the Desktop and are automatically logged in without
|
|
5
|
+
a manual auth screen.
|
|
6
|
+
|
|
7
|
+
> **NOT this skill** if the developer just needs to authenticate manually via the login
|
|
8
|
+
> form — that is handled entirely by `calimero-client-js`. This skill is specifically for
|
|
9
|
+
> reading SSO tokens that Calimero Desktop passes in the URL hash.
|
|
4
10
|
|
|
5
11
|
## What Desktop does
|
|
6
12
|
|
|
@@ -2,28 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## Full startup flow
|
|
4
4
|
|
|
5
|
+
Use the storage helpers from `@calimero-network/calimero-client` — do **not** write to
|
|
6
|
+
`localStorage` directly. The helpers ensure correct key names and extract contextId +
|
|
7
|
+
executorPublicKey from the JWT automatically.
|
|
8
|
+
|
|
5
9
|
```typescript
|
|
10
|
+
import {
|
|
11
|
+
setAppEndpointKey,
|
|
12
|
+
setAccessToken,
|
|
13
|
+
setRefreshToken,
|
|
14
|
+
setApplicationId,
|
|
15
|
+
setContextAndIdentityFromJWT,
|
|
16
|
+
getAuthConfig,
|
|
17
|
+
} from '@calimero-network/calimero-client';
|
|
18
|
+
|
|
6
19
|
interface SSOParams {
|
|
7
20
|
accessToken: string;
|
|
8
|
-
refreshToken: string;
|
|
21
|
+
refreshToken: string | null;
|
|
9
22
|
nodeUrl: string;
|
|
10
|
-
applicationId: string;
|
|
23
|
+
applicationId: string | null;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
function readDesktopSSO(): SSOParams | null {
|
|
14
27
|
const hash = new URLSearchParams(window.location.hash.slice(1));
|
|
15
28
|
const accessToken = hash.get('access_token');
|
|
16
|
-
const refreshToken = hash.get('refresh_token');
|
|
17
29
|
const nodeUrl = hash.get('node_url');
|
|
18
|
-
const applicationId = hash.get('application_id');
|
|
19
30
|
|
|
20
31
|
if (!accessToken || !nodeUrl) return null;
|
|
21
32
|
|
|
22
33
|
return {
|
|
23
34
|
accessToken,
|
|
24
|
-
refreshToken:
|
|
35
|
+
refreshToken: hash.get('refresh_token'),
|
|
25
36
|
nodeUrl,
|
|
26
|
-
applicationId:
|
|
37
|
+
applicationId: hash.get('application_id'),
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -31,21 +42,29 @@ async function bootstrap() {
|
|
|
31
42
|
const sso = readDesktopSSO();
|
|
32
43
|
|
|
33
44
|
if (sso) {
|
|
45
|
+
// Store tokens via SDK helpers (sets localStorage keys correctly)
|
|
46
|
+
setAppEndpointKey(sso.nodeUrl);
|
|
47
|
+
setAccessToken(sso.accessToken);
|
|
48
|
+
if (sso.refreshToken) setRefreshToken(sso.refreshToken);
|
|
49
|
+
if (sso.applicationId) setApplicationId(sso.applicationId);
|
|
50
|
+
// Extracts contextId + executorPublicKey from JWT claims
|
|
51
|
+
setContextAndIdentityFromJWT(sso.accessToken);
|
|
52
|
+
|
|
34
53
|
// Clear hash from URL bar (tokens shouldn't sit in browser history)
|
|
35
54
|
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} else {
|
|
47
|
-
renderLoginScreen();
|
|
56
|
+
renderAuthenticatedApp();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// No SSO hash — check if already authenticated from a prior session
|
|
61
|
+
const config = getAuthConfig();
|
|
62
|
+
if (config.error === null) {
|
|
63
|
+
renderAuthenticatedApp();
|
|
64
|
+
return;
|
|
48
65
|
}
|
|
66
|
+
|
|
67
|
+
renderLoginScreen();
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
|
@@ -53,7 +72,8 @@ document.addEventListener('DOMContentLoaded', bootstrap);
|
|
|
53
72
|
|
|
54
73
|
## How Desktop discovers your app's frontend URL
|
|
55
74
|
|
|
56
|
-
Desktop reads the `links.frontend` field from your app's `manifest.json` inside the
|
|
75
|
+
Desktop reads the `links.frontend` field from your app's `manifest.json` inside the
|
|
76
|
+
installed bundle. Set this field so Desktop can open your app:
|
|
57
77
|
|
|
58
78
|
```json
|
|
59
79
|
{
|
|
@@ -70,18 +90,25 @@ Desktop opens this URL and appends the SSO hash params.
|
|
|
70
90
|
|
|
71
91
|
```typescript
|
|
72
92
|
// App.tsx
|
|
93
|
+
import { getAuthConfig, setAccessToken, setAppEndpointKey,
|
|
94
|
+
setRefreshToken, setApplicationId, setContextAndIdentityFromJWT } from '@calimero-network/calimero-client';
|
|
95
|
+
|
|
73
96
|
function App() {
|
|
74
97
|
const [authState, setAuthState] = useState<'loading' | 'authenticated' | 'unauthenticated'>('loading');
|
|
75
98
|
|
|
76
99
|
useEffect(() => {
|
|
77
100
|
const sso = readDesktopSSO();
|
|
78
101
|
if (sso) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
setAppEndpointKey(sso.nodeUrl);
|
|
103
|
+
setAccessToken(sso.accessToken);
|
|
104
|
+
if (sso.refreshToken) setRefreshToken(sso.refreshToken);
|
|
105
|
+
if (sso.applicationId) setApplicationId(sso.applicationId);
|
|
106
|
+
setContextAndIdentityFromJWT(sso.accessToken);
|
|
107
|
+
history.replaceState(null, '', window.location.pathname);
|
|
82
108
|
setAuthState('authenticated');
|
|
83
109
|
} else {
|
|
84
|
-
|
|
110
|
+
const config = getAuthConfig();
|
|
111
|
+
setAuthState(config.error === null ? 'authenticated' : 'unauthenticated');
|
|
85
112
|
}
|
|
86
113
|
}, []);
|
|
87
114
|
|
|
@@ -67,6 +67,12 @@ meroctl --node-url http://localhost:2428 <command> # connect to specific node
|
|
|
67
67
|
meroctl --home ~/.calimero <command> # use alternate config path
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
`--home` defaults to `~/.calimero`. The home directory contains `config.toml`,
|
|
71
|
+
the node's key material, and local storage. Each node must have its own home directory.
|
|
72
|
+
|
|
73
|
+
`--node-url` defaults to `http://localhost:2428` if not specified (the default port
|
|
74
|
+
`merod` listens on after `merod --home ~/.calimero run`).
|
|
75
|
+
|
|
70
76
|
## References
|
|
71
77
|
|
|
72
78
|
See `references/` for full meroctl command reference and context lifecycle.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Rule: State struct must derive Default, BorshDeserialize, BorshSerialize
|
|
2
|
+
|
|
3
|
+
The three derives are all required. Missing any one of them causes a **runtime panic**,
|
|
4
|
+
not a compile error — the app will install and start, then crash when the context is
|
|
5
|
+
first accessed.
|
|
6
|
+
|
|
7
|
+
## WRONG — missing derives:
|
|
8
|
+
|
|
9
|
+
```rust
|
|
10
|
+
// ✗ Missing all derives — runtime panic on first call
|
|
11
|
+
pub struct AppState {
|
|
12
|
+
items: UnorderedMap<String, String>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ✗ Missing BorshSerialize — state cannot be persisted
|
|
16
|
+
#[derive(Default, BorshDeserialize)]
|
|
17
|
+
pub struct AppState {
|
|
18
|
+
items: UnorderedMap<String, String>,
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CORRECT:
|
|
23
|
+
|
|
24
|
+
```rust
|
|
25
|
+
#[derive(Default, BorshDeserialize, BorshSerialize)]
|
|
26
|
+
pub struct AppState {
|
|
27
|
+
items: UnorderedMap<String, String>,
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What each derive does
|
|
32
|
+
|
|
33
|
+
| Derive | Required for |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `Default` | `#[app::init]` calls `AppState::default()` as the baseline |
|
|
36
|
+
| `BorshDeserialize` | Loading state from node storage on every method call |
|
|
37
|
+
| `BorshSerialize` | Saving state to node storage after every mutation |
|
|
38
|
+
|
|
39
|
+
## Import
|
|
40
|
+
|
|
41
|
+
```rust
|
|
42
|
+
use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Do not use `borsh` crate directly — import from `calimero_sdk::borsh` to ensure
|
|
46
|
+
version compatibility.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# calimero-sdk-js — Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are helping a developer **build a Calimero P2P application in TypeScript** using
|
|
4
|
+
`@calimero-network/calimero-sdk-js`. The app compiles to WebAssembly and runs inside the
|
|
5
|
+
`merod` node runtime.
|
|
6
|
+
|
|
7
|
+
> **NOT this skill** if the developer is connecting a browser/Node.js *frontend* to a node
|
|
8
|
+
> (that's `calimero-client-js` / `@calimero-network/calimero-client`). This skill is for
|
|
9
|
+
> writing the application logic that *runs on the node*.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @calimero-network/calimero-sdk-js
|
|
15
|
+
pnpm add -D @calimero-network/calimero-cli-js typescript
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The CLI's `postinstall` hook downloads QuickJS, WASI-SDK, and Binaryen automatically.
|
|
19
|
+
If you used `--ignore-scripts`, re-run with `pnpm install --ignore-scripts=false`.
|
|
20
|
+
|
|
21
|
+
## Core concepts
|
|
22
|
+
|
|
23
|
+
| Concept | What it is |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `@State` class | Persisted data — fields must be CRDT types |
|
|
26
|
+
| `@Logic(StateClass)` class | Entry points callable via JSON-RPC; must extend the state class |
|
|
27
|
+
| `@Init` static method | Seeds the initial state when context is first created |
|
|
28
|
+
| `@View()` method | Read-only — skips persistence; required for query methods |
|
|
29
|
+
| CRDT collection | Conflict-free type (`Counter`, `UnorderedMap`, etc.) — all state fields must use these |
|
|
30
|
+
|
|
31
|
+
## Minimal app
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { State, Logic, Init, View } from '@calimero-network/calimero-sdk-js';
|
|
35
|
+
import { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
|
|
36
|
+
import * as env from '@calimero-network/calimero-sdk-js/env';
|
|
37
|
+
|
|
38
|
+
@State
|
|
39
|
+
export class KvState {
|
|
40
|
+
items: UnorderedMap<string, string> = new UnorderedMap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Logic(KvState)
|
|
44
|
+
export class KvLogic extends KvState {
|
|
45
|
+
@Init
|
|
46
|
+
static init(): KvState {
|
|
47
|
+
return new KvState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set(key: string, value: string): void {
|
|
51
|
+
env.log(`set ${key}`);
|
|
52
|
+
this.items.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@View()
|
|
56
|
+
get(key: string): string | null {
|
|
57
|
+
return this.items.get(key) ?? null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CRDT collections quick reference
|
|
63
|
+
|
|
64
|
+
| Type | Use case | Key ops |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `Counter` | Distributed counting (returns `bigint`) | `increment()`, `incrementBy(n)`, `value()` |
|
|
67
|
+
| `UnorderedMap<K,V>` | Key-value store (LWW per key) | `set()`, `get()`, `has()`, `remove()`, `entries()` |
|
|
68
|
+
| `UnorderedSet<T>` | Unique membership (LWW per element) | `add()`, `has()`, `delete()`, `toArray()` |
|
|
69
|
+
| `Vector<T>` | Ordered list | `push()`, `get(i)`, `pop()`, `len()` |
|
|
70
|
+
| `LwwRegister<T>` | Single value (timestamp LWW) | `set()`, `get()` |
|
|
71
|
+
|
|
72
|
+
Nested collections (`Map<K, Set<V>>`) propagate changes automatically — no manual
|
|
73
|
+
re-serialization.
|
|
74
|
+
|
|
75
|
+
## Build & deploy
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Build to WASM
|
|
79
|
+
npx calimero-sdk build src/index.ts -o build/service.wasm
|
|
80
|
+
|
|
81
|
+
# Install on node
|
|
82
|
+
meroctl --node-name <NODE> app install \
|
|
83
|
+
--path build/service.wasm \
|
|
84
|
+
--context-id <CONTEXT_ID>
|
|
85
|
+
|
|
86
|
+
# Call a method
|
|
87
|
+
meroctl --node-name <NODE> call \
|
|
88
|
+
--context-id <CONTEXT_ID> \
|
|
89
|
+
--method set \
|
|
90
|
+
--args '{"key":"hello","value":"world"}'
|
|
91
|
+
|
|
92
|
+
# Call a view
|
|
93
|
+
meroctl --node-name <NODE> call \
|
|
94
|
+
--context-id <CONTEXT_ID> \
|
|
95
|
+
--method get \
|
|
96
|
+
--args '{"key":"hello"}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Key rules
|
|
100
|
+
|
|
101
|
+
- All `@State` fields must be CRDT types — never plain `Map`, `Set`, `Array`, or primitives
|
|
102
|
+
- `@View()` is **required** on every read-only method — omitting it causes unnecessary persistence
|
|
103
|
+
- Use `env.log()` not `console.log()` — `console` is not available in the WASM runtime
|
|
104
|
+
- `Counter.value()` returns `bigint`, not `number`
|
|
105
|
+
- `@Init` must be a static method that returns the state class instance
|
|
106
|
+
- `@Logic(StateClass)` must extend the state class
|
|
107
|
+
- No async, no I/O, no threads in app logic — the WASM runtime is synchronous
|
|
108
|
+
- **Windows: building is not supported natively — use WSL** (QuickJS/WASI-SDK toolchain requires Linux/macOS)
|
|
109
|
+
|
|
110
|
+
## Events
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { emit } from '@calimero-network/calimero-sdk-js';
|
|
114
|
+
|
|
115
|
+
// Inside a mutation method:
|
|
116
|
+
emit({ type: 'ItemAdded', key: 'foo', value: 'bar' });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Events are pushed to all context members via WebSocket. Clients subscribe using
|
|
120
|
+
`WsSubscriptionsClient` from `@calimero-network/calimero-client`.
|
|
121
|
+
|
|
122
|
+
## Private storage (node-local)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { createPrivateEntry } from '@calimero-network/calimero-sdk-js';
|
|
126
|
+
|
|
127
|
+
const secret = createPrivateEntry<string>();
|
|
128
|
+
secret.set('my-api-key');
|
|
129
|
+
const val = secret.get();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Private entries are never broadcast to other nodes.
|
|
133
|
+
|
|
134
|
+
## References
|
|
135
|
+
|
|
136
|
+
See `references/` for CRDT collections, events, and build pipeline details.
|
|
137
|
+
See `rules/` for hard constraints.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Build Pipeline
|
|
2
|
+
|
|
3
|
+
`@calimero-network/calimero-cli-js` compiles your TypeScript app to a `.wasm` binary.
|
|
4
|
+
|
|
5
|
+
## Pipeline stages
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
TypeScript → Rollup → QuickJS → WASI-SDK → Binaryen → .wasm
|
|
9
|
+
Source Bundle C bytecode WASM Optimized
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Install SDK and CLI
|
|
16
|
+
pnpm add @calimero-network/calimero-sdk-js
|
|
17
|
+
pnpm add -D @calimero-network/calimero-cli-js typescript
|
|
18
|
+
|
|
19
|
+
# If postinstall didn't run (--ignore-scripts was set):
|
|
20
|
+
pnpm install --ignore-scripts=false
|
|
21
|
+
# Or manually:
|
|
22
|
+
pnpm --filter @calimero-network/calimero-cli-js run install-deps
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## package.json (minimal)
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@calimero-network/calimero-sdk-js": "^0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@calimero-network/calimero-cli-js": "^0.1.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "calimero-sdk build src/index.ts -o build/service.wasm"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Build command
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Build
|
|
46
|
+
npx calimero-sdk build src/index.ts -o build/service.wasm
|
|
47
|
+
|
|
48
|
+
# Or via npm script
|
|
49
|
+
pnpm build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Deploy to node
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Install the app
|
|
56
|
+
meroctl --node-name <NODE> app install \
|
|
57
|
+
--path build/service.wasm \
|
|
58
|
+
--context-id <CONTEXT_ID>
|
|
59
|
+
|
|
60
|
+
# Create a new context (runs @Init)
|
|
61
|
+
meroctl --node-name <NODE> context create \
|
|
62
|
+
--app-id <APP_ID>
|
|
63
|
+
|
|
64
|
+
# Call a mutation
|
|
65
|
+
meroctl --node-name <NODE> call \
|
|
66
|
+
--context-id <CONTEXT_ID> \
|
|
67
|
+
--method set \
|
|
68
|
+
--args '{"key":"hello","value":"world"}'
|
|
69
|
+
|
|
70
|
+
# Call a view
|
|
71
|
+
meroctl --node-name <NODE> call \
|
|
72
|
+
--context-id <CONTEXT_ID> \
|
|
73
|
+
--method get \
|
|
74
|
+
--args '{"key":"hello"}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## End-to-end test with Merobox
|
|
78
|
+
|
|
79
|
+
Each example in the SDK ships a Merobox workflow for multi-node E2E testing:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
merobox bootstrap run examples/counter/workflows/counter-js.yml --log-level=trace
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Platform support
|
|
86
|
+
|
|
87
|
+
| Platform | Supported |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| macOS (x64, arm64) | Yes |
|
|
90
|
+
| Linux (x64, arm64) | Yes |
|
|
91
|
+
| Windows (native) | No — use WSL |
|
|
92
|
+
|
|
93
|
+
## Definition of done (before PR)
|
|
94
|
+
|
|
95
|
+
1. `pnpm lint` passes
|
|
96
|
+
2. `pnpm format:check` passes
|
|
97
|
+
3. `pnpm test` passes
|
|
98
|
+
4. Example app builds: `cd examples/counter && pnpm build`
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# CRDT Collections
|
|
2
|
+
|
|
3
|
+
All persistent state in a `calimero-sdk-js` app must use CRDT types from
|
|
4
|
+
`@calimero-network/calimero-sdk-js/collections`.
|
|
5
|
+
|
|
6
|
+
## Counter (G-Counter)
|
|
7
|
+
|
|
8
|
+
Distributed counting. The value is the sum across all contributing nodes.
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { Counter } from '@calimero-network/calimero-sdk-js/collections';
|
|
12
|
+
|
|
13
|
+
const counter = new Counter();
|
|
14
|
+
counter.increment(); // +1
|
|
15
|
+
counter.incrementBy(5n); // +5 (bigint)
|
|
16
|
+
const total = counter.value(); // bigint
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Counter values are always `bigint`. Do not compare with `=== 0` — use `=== 0n`.
|
|
20
|
+
|
|
21
|
+
## UnorderedMap\<K, V\>
|
|
22
|
+
|
|
23
|
+
Key-value store with Last-Write-Wins conflict resolution per key.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { UnorderedMap } from '@calimero-network/calimero-sdk-js/collections';
|
|
27
|
+
|
|
28
|
+
const map = new UnorderedMap<string, string>();
|
|
29
|
+
map.set('key', 'value');
|
|
30
|
+
const val = map.get('key'); // 'value' | undefined
|
|
31
|
+
const exists = map.has('key'); // boolean
|
|
32
|
+
map.remove('key');
|
|
33
|
+
const all = map.entries(); // [['key', 'value'], ...]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## UnorderedSet\<T\>
|
|
37
|
+
|
|
38
|
+
Set of unique values with Last-Write-Wins per element.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { UnorderedSet } from '@calimero-network/calimero-sdk-js/collections';
|
|
42
|
+
|
|
43
|
+
const set = new UnorderedSet<string>();
|
|
44
|
+
set.add('item'); // true on first insert, false if already present
|
|
45
|
+
set.has('item'); // true
|
|
46
|
+
set.delete('item');
|
|
47
|
+
const items = set.toArray();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Vector\<T\>
|
|
51
|
+
|
|
52
|
+
Ordered list. Conflicts resolved by position.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { Vector } from '@calimero-network/calimero-sdk-js/collections';
|
|
56
|
+
|
|
57
|
+
const vec = new Vector<string>();
|
|
58
|
+
vec.push('first');
|
|
59
|
+
vec.push('second');
|
|
60
|
+
const item = vec.get(0); // 'first'
|
|
61
|
+
const last = vec.pop(); // 'second'
|
|
62
|
+
const len = vec.len(); // 1 (bigint)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## LwwRegister\<T\>
|
|
66
|
+
|
|
67
|
+
Holds a single value. Last write wins based on timestamp.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { LwwRegister } from '@calimero-network/calimero-sdk-js/collections';
|
|
71
|
+
|
|
72
|
+
const reg = new LwwRegister<string>();
|
|
73
|
+
reg.set('current value');
|
|
74
|
+
const current = reg.get(); // 'current value' | undefined
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## UserStorage
|
|
78
|
+
|
|
79
|
+
User-owned signed data. Writes are verified by the owner's signature.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { UserStorage } from '@calimero-network/calimero-sdk-js/collections';
|
|
83
|
+
|
|
84
|
+
const storage = new UserStorage<string>();
|
|
85
|
+
storage.set(executorId, 'my value');
|
|
86
|
+
const val = storage.get(executorId);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## FrozenStorage
|
|
90
|
+
|
|
91
|
+
Immutable, content-addressed storage. Write once; content never changes.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { FrozenStorage } from '@calimero-network/calimero-sdk-js/collections';
|
|
95
|
+
|
|
96
|
+
const frozen = new FrozenStorage<string>();
|
|
97
|
+
const id = frozen.store('immutable content');
|
|
98
|
+
const val = frozen.get(id);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Nested collections
|
|
102
|
+
|
|
103
|
+
Nested structures propagate changes automatically — no manual re-serialization needed.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Map<projectId, Set<tags>> — just works
|
|
107
|
+
const projectTags = new UnorderedMap<string, UnorderedSet<string>>();
|
|
108
|
+
|
|
109
|
+
const tags = new UnorderedSet<string>();
|
|
110
|
+
tags.add('urgent');
|
|
111
|
+
projectTags.set('proj:123', tags);
|
|
112
|
+
|
|
113
|
+
// Modifying the inner set propagates automatically
|
|
114
|
+
projectTags.get('proj:123')?.add('high-priority');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## State field initialization
|
|
118
|
+
|
|
119
|
+
CRDT fields must be initialized inline in the `@State` class:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
@State
|
|
123
|
+
export class AppState {
|
|
124
|
+
// ✅ Inline initialization
|
|
125
|
+
items: UnorderedMap<string, string> = new UnorderedMap();
|
|
126
|
+
count: Counter = new Counter();
|
|
127
|
+
|
|
128
|
+
// ❌ Never use plain JS types for state
|
|
129
|
+
// plainMap: Map<string, string> = new Map();
|
|
130
|
+
// array: string[] = [];
|
|
131
|
+
}
|
|
132
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
Events let your app push real-time notifications to all context members.
|
|
4
|
+
They are emitted during mutation methods and received by clients via WebSocket.
|
|
5
|
+
|
|
6
|
+
## Emitting events
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
import { emit, emitWithHandler } from '@calimero-network/calimero-sdk-js';
|
|
10
|
+
|
|
11
|
+
// Simple event — clients receive it and dispatch themselves
|
|
12
|
+
emit({ type: 'ItemAdded', key: 'foo', value: 'bar' });
|
|
13
|
+
|
|
14
|
+
// Event with a named handler — clients call `onItemAdded(event)` if defined
|
|
15
|
+
emitWithHandler({ type: 'ItemAdded', key: 'foo' }, 'onItemAdded');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Events can only be emitted inside mutation methods (not `@View()` methods).
|
|
19
|
+
|
|
20
|
+
## Receiving events on the client
|
|
21
|
+
|
|
22
|
+
Clients use `WsSubscriptionsClient` from `@calimero-network/calimero-client`:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { WsSubscriptionsClient, getAppEndpointKey, getContextId } from '@calimero-network/calimero-client';
|
|
26
|
+
|
|
27
|
+
const ws = new WsSubscriptionsClient(getAppEndpointKey()!, '/ws');
|
|
28
|
+
await ws.connect();
|
|
29
|
+
ws.subscribe([getContextId()!]);
|
|
30
|
+
|
|
31
|
+
ws.addCallback((event) => {
|
|
32
|
+
if (event.type === 'ExecutionEvent') {
|
|
33
|
+
for (const e of event.data.events) {
|
|
34
|
+
// e.kind — matches the `type` field from emit()
|
|
35
|
+
// e.data — the rest of the emitted object
|
|
36
|
+
console.log(e.kind, e.data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Event typing (recommended)
|
|
43
|
+
|
|
44
|
+
Define a discriminated union to type your events:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
type AppEvent =
|
|
48
|
+
| { type: 'ItemAdded'; key: string; value: string }
|
|
49
|
+
| { type: 'ItemRemoved'; key: string };
|
|
50
|
+
|
|
51
|
+
// Emit
|
|
52
|
+
emit({ type: 'ItemAdded', key: 'foo', value: 'bar' } satisfies AppEvent);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Important
|
|
56
|
+
|
|
57
|
+
- Events are best-effort — clients may miss them if disconnected
|
|
58
|
+
- Do not rely on events for state consistency — use RPC calls to query current state
|
|
59
|
+
- Emitting from a `@View()` method is not supported
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Rule: All @State fields must be CRDT types
|
|
2
|
+
|
|
3
|
+
Every field on a `@State` class must be a CRDT collection from
|
|
4
|
+
`@calimero-network/calimero-sdk-js/collections`. Plain JavaScript types are not
|
|
5
|
+
persisted or synchronized.
|
|
6
|
+
|
|
7
|
+
## WRONG — plain JS types in state:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
@State
|
|
11
|
+
export class AppState {
|
|
12
|
+
items: Map<string, string> = new Map(); // ✗ — not a CRDT, not synced
|
|
13
|
+
tags: string[] = []; // ✗ — not a CRDT, not synced
|
|
14
|
+
name: string = ''; // ✗ — use LwwRegister instead
|
|
15
|
+
count: number = 0; // ✗ — use Counter instead
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CORRECT — CRDT types:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
@State
|
|
23
|
+
export class AppState {
|
|
24
|
+
items: UnorderedMap<string, string> = new UnorderedMap();
|
|
25
|
+
tags: UnorderedSet<string> = new UnorderedSet();
|
|
26
|
+
name: LwwRegister<string> = new LwwRegister();
|
|
27
|
+
count: Counter = new Counter();
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why this matters
|
|
32
|
+
|
|
33
|
+
Plain JS types are not serialized to the node's CRDT storage. State that uses
|
|
34
|
+
`Map`, `Set`, `Array`, or primitives directly will be lost on the next call and
|
|
35
|
+
will never sync to other nodes in the context.
|
|
36
|
+
|
|
37
|
+
## Choosing the right CRDT
|
|
38
|
+
|
|
39
|
+
| Data shape | Use |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| Counting (monotonic) | `Counter` |
|
|
42
|
+
| Key-value lookup | `UnorderedMap<K, V>` |
|
|
43
|
+
| Unique membership | `UnorderedSet<T>` |
|
|
44
|
+
| Ordered list | `Vector<T>` |
|
|
45
|
+
| Single value (overwrites) | `LwwRegister<T>` |
|
|
46
|
+
| User-owned signed data | `UserStorage<T>` |
|
|
47
|
+
| Immutable content | `FrozenStorage<T>` |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Rule: Use env.log() — not console.log()
|
|
2
|
+
|
|
3
|
+
`console` is not available in the QuickJS/WASM runtime. Calling `console.log()` will
|
|
4
|
+
throw a runtime error. Use `env.log()` from the SDK's env module instead.
|
|
5
|
+
|
|
6
|
+
## WRONG:
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
console.log('Processing item:', key); // ✗ — throws at runtime
|
|
10
|
+
console.error('Something went wrong'); // ✗ — throws at runtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## CORRECT:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import * as env from '@calimero-network/calimero-sdk-js/env';
|
|
17
|
+
|
|
18
|
+
env.log(`Processing item: ${key}`); // ✓ — output appears in node logs
|
|
19
|
+
env.log(`Error: ${error}`); // ✓
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## env.log() format
|
|
23
|
+
|
|
24
|
+
`env.log()` accepts a single string. Use template literals for dynamic values:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
env.log(`set called: key=${key}, value=${value}`);
|
|
28
|
+
env.log(`counter is now ${this.count.value()}`);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Other env utilities
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import * as env from '@calimero-network/calimero-sdk-js/env';
|
|
35
|
+
|
|
36
|
+
// Get the calling executor's public key (Uint8Array, 32 bytes)
|
|
37
|
+
const executorId = env.executorId();
|
|
38
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Rule: @View() is required on read-only methods
|
|
2
|
+
|
|
3
|
+
Every method that does not modify state must be decorated with `@View()`. Without it,
|
|
4
|
+
the runtime will persist state after every call — even when nothing changed — causing
|
|
5
|
+
unnecessary storage writes and cross-node syncs.
|
|
6
|
+
|
|
7
|
+
## WRONG — read-only method without @View():
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
@Logic(AppState)
|
|
11
|
+
export class AppLogic extends AppState {
|
|
12
|
+
// ✗ — no @View(), triggers persistence on every call
|
|
13
|
+
getItem(key: string): string | null {
|
|
14
|
+
return this.items.get(key) ?? null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ✗ — returning a count without @View()
|
|
18
|
+
getCount(): bigint {
|
|
19
|
+
return this.count.value();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CORRECT:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
@Logic(AppState)
|
|
28
|
+
export class AppLogic extends AppState {
|
|
29
|
+
@View()
|
|
30
|
+
getItem(key: string): string | null {
|
|
31
|
+
return this.items.get(key) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@View()
|
|
35
|
+
getCount(): bigint {
|
|
36
|
+
return this.count.value();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How to tell if a method needs @View()
|
|
42
|
+
|
|
43
|
+
A method is read-only if it:
|
|
44
|
+
- Does not call any mutation methods on CRDT fields (`set`, `insert`, `add`, `push`, `increment`, etc.)
|
|
45
|
+
- Does not call `emit()` or `emitWithHandler()`
|
|
46
|
+
- Only reads values and returns results
|