atmx-web 0.42.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.
@@ -0,0 +1,223 @@
1
+ // FILE: src/core/wasm.ts
2
+ import { setupWebCallback } from "./callback";
3
+ import { AtmxConfig } from "../core/types";
4
+ import { initRouter } from "./router";
5
+ import { InitResult } from "./types";
6
+
7
+ // @ts-ignore
8
+ import wasmGlueCode from "./vendor/axiom_runtime.js?raw";
9
+
10
+ export let wasmEngine: any = null;
11
+
12
+ export function allocBytes(bytes: Uint8Array): number {
13
+ if (bytes.length === 0) return 0;
14
+ const ptr = wasmEngine.axiom_malloc(bytes.length);
15
+ const mem = new Uint8Array(wasmEngine.memory.buffer);
16
+ mem.set(bytes, ptr);
17
+ return ptr;
18
+ }
19
+
20
+ export function allocString(str: string): { ptr: number; len: number } {
21
+ if (!str) return { ptr: 0, len: 0 };
22
+ const bytes = new TextEncoder().encode(str);
23
+ const ptr = allocBytes(bytes);
24
+ return { ptr, len: bytes.length };
25
+ }
26
+
27
+ function getWasmUrl(): string {
28
+ return "https://atmx.axiomcore.dev/latest/axiom_runtime.wasm";
29
+ }
30
+
31
+ export async function initWasm(config: AtmxConfig): Promise<InitResult> {
32
+ const startTime = performance.now();
33
+ const result: InitResult = {
34
+ ok: false,
35
+ contracts: {},
36
+ initDurationMs: 0,
37
+ };
38
+
39
+ try {
40
+ if (!(window as any).wasm_bindgen) {
41
+ const script = document.createElement("script");
42
+ script.type = "text/javascript";
43
+ script.text = `${wasmGlueCode}\nwindow.wasm_bindgen = wasm_bindgen;`;
44
+ document.head.appendChild(script);
45
+ }
46
+
47
+ const wasmBindgen = (window as any).wasm_bindgen;
48
+ const wasmLocation = getWasmUrl();
49
+ const fetchUrl = `${wasmLocation}?t=${Date.now()}`;
50
+
51
+ try {
52
+ wasmEngine = await wasmBindgen(fetch(fetchUrl));
53
+ } catch (e: any) {
54
+ throw {
55
+ stage: "ffiBoundary",
56
+ category: "network",
57
+ code: "WasmFetchFailed",
58
+ message: `Failed to download WASM engine: ${e.message}`,
59
+ retryable: true,
60
+ };
61
+ }
62
+
63
+ setupWebCallback(wasmEngine);
64
+
65
+ setInterval(() => {
66
+ if (wasmEngine) wasmEngine.axiom_process_responses();
67
+ }, 16);
68
+
69
+ const dbPath = allocString("");
70
+ const initResultCode = wasmEngine.axiom_wasm_initialize(
71
+ dbPath.ptr,
72
+ dbPath.len,
73
+ );
74
+ wasmEngine.axiom_free_memory(dbPath.ptr, dbPath.len);
75
+
76
+ if (initResultCode !== 0) {
77
+ throw {
78
+ stage: "runtime",
79
+ category: "server",
80
+ code: "InitFailed",
81
+ message: `Engine init failed (Code: ${initResultCode})`,
82
+ retryable: false,
83
+ };
84
+ }
85
+
86
+ for (const [namespace, contractConfig] of Object.entries(
87
+ config.contracts,
88
+ )) {
89
+ try {
90
+ const contractRes = await fetch(contractConfig.contractUrl);
91
+ if (!contractRes.ok) {
92
+ throw new Error(`HTTP ${contractRes.status}`);
93
+ }
94
+
95
+ const contractArrayBuffer = await contractRes.arrayBuffer();
96
+ let endpointCount = 0;
97
+
98
+ try {
99
+ const jsonStr = new TextDecoder().decode(contractArrayBuffer);
100
+ const contractJson = JSON.parse(jsonStr);
101
+ const endpoints = contractJson?.ir?.endpoints;
102
+ if (endpoints) {
103
+ endpointCount = Array.isArray(endpoints)
104
+ ? endpoints.length
105
+ : Object.keys(endpoints).length;
106
+ }
107
+ initRouter(namespace, contractJson);
108
+ } catch (e) {
109
+ console.error(
110
+ `ATMX: Failed to parse .axiom contract for '${namespace}'`,
111
+ e,
112
+ );
113
+ }
114
+
115
+ const contractBytes = new Uint8Array(contractArrayBuffer);
116
+ const cPtr = allocBytes(contractBytes);
117
+ const nsStr = allocString(namespace);
118
+ const urlStr = allocString(contractConfig.baseUrl);
119
+
120
+ const loadResult = wasmEngine.axiom_wasm_load_contract(
121
+ nsStr.ptr,
122
+ nsStr.len,
123
+ urlStr.ptr,
124
+ urlStr.len,
125
+ cPtr,
126
+ contractBytes.length,
127
+ 0,
128
+ 0,
129
+ 0,
130
+ 0,
131
+ );
132
+
133
+ wasmEngine.axiom_free_memory(cPtr, contractBytes.length);
134
+ wasmEngine.axiom_free_memory(nsStr.ptr, nsStr.len);
135
+ wasmEngine.axiom_free_memory(urlStr.ptr, urlStr.len);
136
+
137
+ if (loadResult !== 0 && loadResult !== -1) {
138
+ throw new Error(`Load error (Code: ${loadResult})`);
139
+ }
140
+
141
+ if (config.debug) {
142
+ console.log(
143
+ `%c🔒 Axiom Contract Loaded: ${namespace} (${endpointCount} endpoints)`,
144
+ "color: #10b981; font-weight: bold;",
145
+ );
146
+ }
147
+
148
+ result.contracts[namespace] = { loaded: true, endpointCount };
149
+
150
+ // ✨ EVENT: Emit contract loaded
151
+ document.dispatchEvent(
152
+ new CustomEvent("atmx:contract-loaded", {
153
+ detail: { namespace, endpointCount },
154
+ }),
155
+ );
156
+ } catch (contractErr: any) {
157
+ result.contracts[namespace] = {
158
+ loaded: false,
159
+ endpointCount: 0,
160
+ error: contractErr.message,
161
+ };
162
+ console.error(`ATMX: Contract failed for ${namespace}`, contractErr);
163
+ }
164
+ }
165
+
166
+ result.ok = true;
167
+ } catch (fatalError: any) {
168
+ result.ok = false;
169
+ // Map native JS errors into structured AxiomErrors
170
+ result.error = fatalError.stage
171
+ ? fatalError
172
+ : {
173
+ stage: "runtime",
174
+ category: "unknown",
175
+ code: "FatalError",
176
+ message: fatalError.message || String(fatalError),
177
+ retryable: false,
178
+ };
179
+ }
180
+
181
+ result.initDurationMs = Math.round(performance.now() - startTime);
182
+ return result;
183
+ }
184
+
185
+ export function setAuthToken(
186
+ namespace: string,
187
+ methodName: string,
188
+ token: string,
189
+ ) {
190
+ if (!wasmEngine) return;
191
+ const nStr = allocString(namespace);
192
+ const mStr = allocString(methodName);
193
+ const tStr = allocString(token);
194
+
195
+ wasmEngine.axiom_wasm_set_auth_token(
196
+ nStr.ptr,
197
+ nStr.len,
198
+ mStr.ptr,
199
+ mStr.len,
200
+ tStr.ptr,
201
+ tStr.len,
202
+ );
203
+
204
+ wasmEngine.axiom_free_memory(nStr.ptr, nStr.len);
205
+ wasmEngine.axiom_free_memory(mStr.ptr, mStr.len);
206
+ wasmEngine.axiom_free_memory(tStr.ptr, tStr.len);
207
+ }
208
+
209
+ export function clearAuthToken(namespace: string, methodName: string) {
210
+ if (!wasmEngine) return;
211
+ const nStr = allocString(namespace);
212
+ const mStr = allocString(methodName);
213
+
214
+ wasmEngine.axiom_wasm_clear_auth_token(
215
+ nStr.ptr,
216
+ nStr.len,
217
+ mStr.ptr,
218
+ mStr.len,
219
+ );
220
+
221
+ wasmEngine.axiom_free_memory(nStr.ptr, nStr.len);
222
+ wasmEngine.axiom_free_memory(mStr.ptr, mStr.len);
223
+ }
@@ -0,0 +1,62 @@
1
+ // FILE: src/dom/components/engine-status.ts
2
+
3
+ export class AxEngineStatusElement extends HTMLElement {
4
+ constructor() {
5
+ super();
6
+ }
7
+
8
+ connectedCallback() {
9
+ const loadingText = this.getAttribute("loading-text") || "Initializing...";
10
+ const readyText = this.getAttribute("ready-text") || "Online";
11
+ const errorText = this.getAttribute("error-text") || "Failed to Connect";
12
+
13
+ // Initial state
14
+ this.renderState("loading", loadingText, "text-amber-500 animate-pulse");
15
+
16
+ // Listen for ATMX engine events globally
17
+ document.addEventListener("atmx:ready", () => {
18
+ this.renderState("ready", readyText, "text-green-600");
19
+ });
20
+
21
+ document.addEventListener("atmx:error", (e: Event) => {
22
+ const customEvent = e as CustomEvent;
23
+ // Check if this is a fatal initialization error vs a normal query error
24
+ if (customEvent.detail && customEvent.detail.stage === "runtime") {
25
+ this.renderState(
26
+ "error",
27
+ `${errorText}: ${customEvent.detail.message}`,
28
+ "text-red-600",
29
+ );
30
+ }
31
+ });
32
+ }
33
+
34
+ private renderState(state: string, text: string, defaultClasses: string) {
35
+ this.setAttribute("data-state", state);
36
+
37
+ // If developer is using raw ax-engine-status directly with inner HTML template matching, we allow it.
38
+ // Otherwise, we just swap the raw text node.
39
+ const stateElements = this.querySelectorAll(`[data-ax-when]`);
40
+
41
+ if (stateElements.length > 0) {
42
+ stateElements.forEach((el) => {
43
+ if (el.getAttribute("data-ax-when") === state) {
44
+ el.removeAttribute("hidden");
45
+ } else {
46
+ el.setAttribute("hidden", "");
47
+ }
48
+ });
49
+ } else {
50
+ // No custom inner templates, just replace text
51
+ this.innerText = text;
52
+ if (!this.getAttribute("class")) {
53
+ this.className = `text-xs font-bold ${defaultClasses}`;
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Automatically register the web component
60
+ if (typeof window !== "undefined" && !customElements.get("ax-engine-status")) {
61
+ customElements.define("ax-engine-status", AxEngineStatusElement);
62
+ }
@@ -0,0 +1,36 @@
1
+ const activeRequests = new Map<number, { trigger: HTMLElement, indicators: HTMLElement[] }>();
2
+
3
+ export function startRequest(reqId: number, triggerEl: HTMLElement) {
4
+ // 1. Add 'ax-request' to the element that triggered the call (e.g., the button)
5
+ triggerEl.classList.add('ax-request');
6
+
7
+ // 2. Find any associated loading indicators
8
+ let indicators: HTMLElement[] = [];
9
+ const indicatorSelector = triggerEl.getAttribute('ax-indicator');
10
+
11
+ if (indicatorSelector) {
12
+ // If ax-indicator="#spinner" is explicitly defined
13
+ indicators = Array.from(document.querySelectorAll(indicatorSelector));
14
+ } else {
15
+ // Fallback: Look for any children inside the trigger with the class 'ax-indicator'
16
+ indicators = Array.from(triggerEl.querySelectorAll('.ax-indicator'));
17
+ }
18
+
19
+ // 3. Turn on the indicators
20
+ indicators.forEach(el => el.classList.add('ax-loading'));
21
+
22
+ // 4. Save state so we can turn them off later
23
+ activeRequests.set(reqId, { trigger: triggerEl, indicators });
24
+ }
25
+
26
+ export function endRequest(reqId: number) {
27
+ const req = activeRequests.get(reqId);
28
+ if (!req) return;
29
+
30
+ // Turn off all classes
31
+ req.trigger.classList.remove('ax-request');
32
+ req.indicators.forEach(el => el.classList.remove('ax-loading'));
33
+
34
+ // Clean up
35
+ activeRequests.delete(reqId);
36
+ }
@@ -0,0 +1,379 @@
1
+ // FILE: src/dom/lifecycle.ts
2
+ import { wasmEngine, allocString, allocBytes } from "../core/wasm";
3
+ import { pendingRequests } from "../core/callback";
4
+ import { resolveRoute, buildRequestPath } from "../core/router";
5
+ import {
6
+ getContext,
7
+ updateContext,
8
+ getClosestData,
9
+ AtmxContextData,
10
+ } from "../core/context";
11
+ import { EventType } from "../core/types";
12
+ import { executeAction, evaluateExpression } from "../resolver/evaluator";
13
+ import { queryRegistry, ActiveQuery } from "../core/query";
14
+ import { atmx } from "../index";
15
+
16
+ let globalReqCounter = 0;
17
+
18
+ function triggerEscapeHatch(element: HTMLElement, eventName: string) {
19
+ const attrName = `ax-on:${eventName}`;
20
+ if (element.hasAttribute(attrName)) {
21
+ const expr = element.getAttribute(attrName)!;
22
+ const ctx = getContext(element);
23
+ executeAction(expr, ctx);
24
+ }
25
+ }
26
+
27
+ function logTransaction(direction: "OUT" | "IN", reqId: number, details: any) {
28
+ if (!atmx.debug) return;
29
+ const color = direction === "OUT" ? "#7c3aed" : "#059669";
30
+ const label = direction === "OUT" ? "➔ WASM CALL" : "← WASM RESP";
31
+ console.groupCollapsed(
32
+ `%c${label} [#${reqId}]`,
33
+ `color: ${color}; font-weight: bold;`,
34
+ );
35
+ console.log("Details:", details);
36
+ console.groupEnd();
37
+ }
38
+
39
+ export function parseRpcCall(
40
+ element: HTMLElement,
41
+ attrValue: string,
42
+ ): { target: string; args: Record<string, any> } | null {
43
+ const match = attrValue.trim().match(/^([\w\.]+)(?:\((.*)\))?$/);
44
+ if (!match) return null;
45
+
46
+ const target = match[1];
47
+ let args = {};
48
+
49
+ if (match[2] && match[2].trim() !== "") {
50
+ try {
51
+ const ctx = getContext(element);
52
+ const availableData = ctx.data || getClosestData(element);
53
+ const fn = new Function("$data", "$state", `return (${match[2]});`);
54
+ args = fn(availableData, ctx) || {};
55
+ } catch (e) {
56
+ console.error(`ATMX Scope Error in "${attrValue}":`, e);
57
+ }
58
+ }
59
+ return { target, args };
60
+ }
61
+
62
+ export function connectQuery(element: HTMLElement) {
63
+ const attrVal = element.getAttribute("ax-query");
64
+ if (!attrVal) return;
65
+
66
+ const parsed = parseRpcCall(element, attrVal);
67
+ if (!parsed) return;
68
+
69
+ const route = resolveRoute(parsed.target);
70
+ if (!route) {
71
+ updateContext(element, {
72
+ state: "error",
73
+ error: { message: `Route not found: ${parsed.target}` },
74
+ });
75
+ return;
76
+ }
77
+
78
+ const queryKey = `${route.namespace}:${route.id}:${JSON.stringify(parsed.args)}`;
79
+
80
+ let activeQuery = queryRegistry.get(queryKey);
81
+ if (!activeQuery) {
82
+ activeQuery = new ActiveQuery(queryKey, route, parsed.args);
83
+ queryRegistry.set(queryKey, activeQuery);
84
+ }
85
+
86
+ if (!(element as any)._axListener) {
87
+ (element as any)._axListener = (newState: Partial<AtmxContextData>) => {
88
+ updateContext(element, newState);
89
+ if (newState.state === "data" || newState.state === "error") {
90
+ triggerEscapeHatch(element, newState.state);
91
+ element.dispatchEvent(
92
+ new CustomEvent(`atmx:${newState.state}`, {
93
+ bubbles: true,
94
+ detail: newState.data || newState.error,
95
+ }),
96
+ );
97
+ }
98
+ };
99
+ activeQuery.subscribe((element as any)._axListener);
100
+ }
101
+
102
+ activeQuery.fetch();
103
+ }
104
+
105
+ export function executeMutation(element: HTMLElement) {
106
+ const attrVal = element.getAttribute("ax-mutate");
107
+ if (!attrVal) return;
108
+
109
+ const parsed = parseRpcCall(element, attrVal);
110
+ if (!parsed) return;
111
+
112
+ const route = resolveRoute(parsed.target);
113
+ if (!route) {
114
+ updateContext(element, {
115
+ state: "error",
116
+ error: { message: `Route not found: ${parsed.target}` },
117
+ });
118
+ triggerEscapeHatch(element, "error");
119
+ return;
120
+ }
121
+
122
+ updateContext(element, { state: "mutating", error: null, data: null });
123
+ triggerEscapeHatch(element, "mutating");
124
+
125
+ const formDataObj: Record<string, any> = {};
126
+ if (element instanceof HTMLFormElement) {
127
+ const formData = new FormData(element);
128
+ formData.forEach((value, key) => {
129
+ const input = element.elements.namedItem(key);
130
+ let isNumber = false;
131
+
132
+ if (input instanceof HTMLInputElement) {
133
+ isNumber = input.type === "number";
134
+ } else if (input instanceof RadioNodeList && input.length > 0) {
135
+ isNumber = (input[0] as HTMLInputElement).type === "number";
136
+ }
137
+
138
+ const finalVal = isNumber && value !== "" ? Number(value) : value;
139
+
140
+ // ✨ NEW: Un-flatten dot notation! (e.g. "user.email" -> { user: { email: ... } })
141
+ if (key.includes(".")) {
142
+ const parts = key.split(".");
143
+ let current = formDataObj;
144
+ for (let i = 0; i < parts.length - 1; i++) {
145
+ if (!current[parts[i]]) current[parts[i]] = {};
146
+ current = current[parts[i]];
147
+ }
148
+ current[parts[parts.length - 1]] = finalVal;
149
+ } else {
150
+ formDataObj[key] = finalVal;
151
+ }
152
+ });
153
+ }
154
+
155
+ const mergedArgs = { ...formDataObj, ...parsed.args };
156
+ const { path, remainingArgs } = buildRequestPath(route, mergedArgs);
157
+
158
+ // ✨ NEW: Smart Body Extraction for FastAPI!
159
+ let payloadObj = remainingArgs;
160
+ if (route.bodyParam && remainingArgs[route.bodyParam] !== undefined) {
161
+ // If the developer passed the data inside the wrapper (e.g., args.user), extract it so it's flat!
162
+ payloadObj = remainingArgs[route.bodyParam];
163
+ }
164
+
165
+ let customHeaders: Record<string, string> = {};
166
+ const headersAttr = element.getAttribute("ax-headers");
167
+ if (headersAttr) {
168
+ try {
169
+ const fn = new Function("return (" + headersAttr + ");");
170
+ customHeaders = fn() || {};
171
+ } catch (e) {
172
+ console.error("ATMX: Invalid ax-headers syntax", e);
173
+ }
174
+ }
175
+
176
+ const isFormUrlEncoded = Object.entries(customHeaders).some(
177
+ ([k, v]) =>
178
+ k.toLowerCase() === "content-type" &&
179
+ v.includes("application/x-www-form-urlencoded"),
180
+ );
181
+
182
+ let payloadBytes = new Uint8Array(0);
183
+ if (Object.keys(payloadObj).length > 0) {
184
+ if (isFormUrlEncoded) {
185
+ const params = new URLSearchParams();
186
+ for (const [k, v] of Object.entries(payloadObj)) {
187
+ params.append(k, String(v));
188
+ }
189
+ payloadBytes = new TextEncoder().encode(params.toString());
190
+ } else {
191
+ payloadBytes = new TextEncoder().encode(JSON.stringify(payloadObj));
192
+ }
193
+ }
194
+
195
+ executeWasmCall(
196
+ element,
197
+ route.namespace,
198
+ route.id,
199
+ route.method,
200
+ path,
201
+ payloadBytes,
202
+ route.isStream,
203
+ true,
204
+ customHeaders,
205
+ isFormUrlEncoded,
206
+ );
207
+ }
208
+
209
+ function executeWasmCall(
210
+ element: HTMLElement,
211
+ namespace: string,
212
+ endpointId: number,
213
+ method: string,
214
+ path: string,
215
+ payloadBytes: Uint8Array,
216
+ isStream: boolean,
217
+ isMutation: boolean,
218
+ customHeaders: Record<string, string> = {},
219
+ isFormUrlEncoded: boolean = false,
220
+ ) {
221
+ const reqId = ++globalReqCounter;
222
+
223
+ logTransaction("OUT", reqId, {
224
+ namespace,
225
+ endpointId,
226
+ method,
227
+ path,
228
+ headers: customHeaders,
229
+ payload:
230
+ payloadBytes.length > 0
231
+ ? isFormUrlEncoded
232
+ ? new TextDecoder().decode(payloadBytes)
233
+ : JSON.parse(new TextDecoder().decode(payloadBytes))
234
+ : null,
235
+ });
236
+
237
+ pendingRequests.set(reqId, {
238
+ isStream: isStream,
239
+ onResponse: (res) => handleResponse(element, res, isMutation, reqId),
240
+ onComplete: () => {
241
+ if (atmx.debug)
242
+ console.log(`%c✔ Stream Complete [#${reqId}]`, "color: #94a3b8;");
243
+ },
244
+ });
245
+
246
+ const nsStr = allocString(namespace);
247
+ const mStr = allocString(method);
248
+ const pStr = allocString(path);
249
+ const tpStr = allocString("");
250
+ const hStr = allocString(
251
+ Object.keys(customHeaders).length > 0 ? JSON.stringify(customHeaders) : "",
252
+ );
253
+ const payloadPtr = allocBytes(payloadBytes);
254
+
255
+ wasmEngine.axiom_wasm_call(
256
+ reqId,
257
+ nsStr.ptr,
258
+ nsStr.len,
259
+ endpointId,
260
+ mStr.ptr,
261
+ mStr.len,
262
+ pStr.ptr,
263
+ pStr.len,
264
+ tpStr.ptr,
265
+ tpStr.len,
266
+ hStr.ptr,
267
+ hStr.len,
268
+ payloadPtr,
269
+ payloadBytes.length,
270
+ );
271
+
272
+ wasmEngine.axiom_free_memory(nsStr.ptr, nsStr.len);
273
+ wasmEngine.axiom_free_memory(mStr.ptr, mStr.len);
274
+ wasmEngine.axiom_free_memory(pStr.ptr, pStr.len);
275
+ wasmEngine.axiom_free_memory(tpStr.ptr, tpStr.len);
276
+ wasmEngine.axiom_free_memory(hStr.ptr, hStr.len);
277
+ if (payloadPtr) wasmEngine.axiom_free_memory(payloadPtr, payloadBytes.length);
278
+ }
279
+
280
+ function handleResponse(
281
+ element: HTMLElement,
282
+ res: any,
283
+ isMutation: boolean,
284
+ reqId: number,
285
+ ) {
286
+ logTransaction("IN", reqId, {
287
+ eventType: EventType[res.eventType],
288
+ data: res.data,
289
+ error: res.error,
290
+ });
291
+
292
+ if (res.eventType === EventType.Error) {
293
+ updateContext(element, {
294
+ state: "error",
295
+ error: res.error,
296
+ isFetching: false,
297
+ isMutating: false,
298
+ });
299
+ triggerEscapeHatch(element, "error");
300
+ element.dispatchEvent(
301
+ new CustomEvent("atmx:error", { bubbles: true, detail: res.error }),
302
+ );
303
+ return;
304
+ }
305
+
306
+ if (res.data) {
307
+ let source: "cache" | "network" = "network";
308
+ let isFetching = false;
309
+
310
+ if (res.eventType === EventType.CacheHit) {
311
+ source = "cache";
312
+ } else if (res.eventType === EventType.CacheHitAndFetching) {
313
+ source = "cache";
314
+ isFetching = true;
315
+ } else if (res.eventType === EventType.NetworkSuccess) {
316
+ source = "network";
317
+ }
318
+
319
+ const nextState = isMutation ? "success" : "data";
320
+
321
+ updateContext(element, {
322
+ state: nextState,
323
+ data: res.data,
324
+ source,
325
+ isFetching,
326
+ isMutating: false,
327
+ });
328
+
329
+ // ✨ PHASE 1: Declarative Auth
330
+ if (isMutation) {
331
+ const storeTokenAttr = element.getAttribute("ax-store-token");
332
+ if (storeTokenAttr) {
333
+ const tokenVal = evaluateExpression(
334
+ storeTokenAttr,
335
+ getContext(element),
336
+ );
337
+ if (tokenVal) {
338
+ const rpcAttr = element.getAttribute("ax-mutate");
339
+ const route = resolveRoute(parseRpcCall(element, rpcAttr!)!.target);
340
+ const headerName =
341
+ element.getAttribute("ax-token-header") || "Authorization";
342
+ atmx.setAuthToken(
343
+ route?.namespace || "default",
344
+ headerName,
345
+ tokenVal,
346
+ );
347
+ }
348
+ }
349
+
350
+ if (element.hasAttribute("ax-clear-token")) {
351
+ const rpcAttr = element.getAttribute("ax-mutate");
352
+ const route = resolveRoute(parseRpcCall(element, rpcAttr!)!.target);
353
+ const headerName =
354
+ element.getAttribute("ax-token-header") || "Authorization";
355
+ atmx.clearAuthToken(route?.namespace || "default", headerName);
356
+ }
357
+
358
+ // ✨ PHASE 1: Declarative Refresh
359
+ const refreshAttr = element.getAttribute("ax-refresh");
360
+ if (refreshAttr) {
361
+ const targets = refreshAttr.split(",").map((s) => s.trim());
362
+ for (const [_, activeQuery] of queryRegistry.entries()) {
363
+ const routeName = `${activeQuery.route.namespace}.${activeQuery.route.name}`;
364
+ if (targets.includes(routeName)) {
365
+ activeQuery.fetch(true); // Force refresh
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ triggerEscapeHatch(element, nextState);
372
+ element.dispatchEvent(
373
+ new CustomEvent(isMutation ? "atmx:success" : "atmx:data", {
374
+ bubbles: true,
375
+ detail: res.data,
376
+ }),
377
+ );
378
+ }
379
+ }