rastack 0.0.16 → 0.0.20

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 (105) hide show
  1. package/CHANGELOG.md +43876 -43013
  2. package/dist/.prettierrc +7 -7
  3. package/dist/compile/analyze.js +19 -11
  4. package/dist/compile/index.js +26 -17
  5. package/dist/compile/manifest.js +2 -3
  6. package/dist/compile/openapi.js +5 -8
  7. package/dist/compile/program.js +19 -10
  8. package/dist/csv-schema.js +5 -6
  9. package/dist/define/index.js +2 -2
  10. package/dist/entity-generation/delete-method.js +8 -10
  11. package/dist/entity-generation/form.js +80 -82
  12. package/dist/entity-generation/get-method.js +35 -36
  13. package/dist/entity-generation/imports.js +24 -20
  14. package/dist/entity-generation/sync-method.d.ts +1 -1
  15. package/dist/entity-generation/sync-method.js +45 -46
  16. package/dist/entity-generation/update-method.js +9 -10
  17. package/dist/rad-wasm-build.d.ts +1 -1
  18. package/dist/rad-wasm-build.js +15 -6
  19. package/dist/rad.js +9 -2
  20. package/dist/schema/camel-to-pastel.js +1 -2
  21. package/dist/schema/capitalise-first-letter.js +1 -2
  22. package/dist/schema/extract-response.js +3 -4
  23. package/dist/schema/fetch-schema.js +4 -16
  24. package/dist/schema/remove-non-model-paths.js +1 -2
  25. package/dist/schema/to-camel-case.js +1 -2
  26. package/dist/schema/to-pastel-case.js +1 -2
  27. package/dist/schema-convert.js +22 -33
  28. package/dist/schema-entities.js +177 -189
  29. package/dist/schema-fetch.js +46 -63
  30. package/dist/schema-full.js +23 -36
  31. package/dist/schema-index.js +16 -27
  32. package/dist/schema-params.js +11 -22
  33. package/dist/seed.js +95 -97
  34. package/hooks/form/form.ts +207 -207
  35. package/hooks/form/index.ts +8 -8
  36. package/hooks/form/interfaces.ts +217 -217
  37. package/hooks/form/structure.ts +39 -39
  38. package/hooks/form/validate-schema.ts +49 -49
  39. package/hooks/index.ts +3 -3
  40. package/hooks/query/api.ts +42 -42
  41. package/hooks/query/delete.ts +45 -45
  42. package/hooks/query/fetch.ts +48 -48
  43. package/hooks/query/index.ts +21 -21
  44. package/hooks/query/interfaces.ts +111 -111
  45. package/hooks/query/list.ts +286 -286
  46. package/hooks/query/update.ts +88 -88
  47. package/hooks/query/url.ts +31 -31
  48. package/hooks/real-time/index.ts +1 -1
  49. package/hooks/real-time/pusher.ts +43 -43
  50. package/jest.config.cjs +6 -6
  51. package/package.json +57 -57
  52. package/provider/index.ts +13 -13
  53. package/provider/provider.tsx +187 -187
  54. package/provider/types.ts +114 -114
  55. package/provider/warehouse.ts +167 -167
  56. package/provider/wasm.ts +124 -124
  57. package/runtime.ts +3 -3
  58. package/src/.prettierrc +7 -7
  59. package/src/compile/analyze.ts +224 -224
  60. package/src/compile/index.ts +86 -86
  61. package/src/compile/manifest.ts +10 -10
  62. package/src/compile/model.ts +69 -69
  63. package/src/compile/openapi.ts +266 -266
  64. package/src/compile/program.ts +40 -40
  65. package/src/csv-schema.ts +187 -187
  66. package/src/define/index.ts +162 -162
  67. package/src/entity-generation/delete-method.ts +37 -37
  68. package/src/entity-generation/form.ts +169 -169
  69. package/src/entity-generation/get-method.ts +80 -80
  70. package/src/entity-generation/imports.ts +110 -110
  71. package/src/entity-generation/sync-method.ts +222 -222
  72. package/src/entity-generation/update-method.ts +38 -38
  73. package/src/index.ts +77 -77
  74. package/src/rad-compile.ts +88 -88
  75. package/src/rad-wasm-build.ts +102 -102
  76. package/src/rad.ts +101 -101
  77. package/src/scan.ts +89 -89
  78. package/src/schema/camel-to-pastel.ts +3 -3
  79. package/src/schema/capitalise-first-letter.ts +3 -3
  80. package/src/schema/extract-response.ts +38 -38
  81. package/src/schema/fetch-schema.ts +12 -12
  82. package/src/schema/remove-non-model-paths.ts +13 -13
  83. package/src/schema/to-camel-case.ts +7 -7
  84. package/src/schema/to-pastel-case.ts +6 -6
  85. package/src/schema-convert.ts +61 -61
  86. package/src/schema-entities.ts +401 -401
  87. package/src/schema-fetch.ts +82 -82
  88. package/src/schema-full.ts +38 -38
  89. package/src/schema-index.ts +29 -29
  90. package/src/schema-params.ts +116 -116
  91. package/src/seed.ts +297 -297
  92. package/sync/engine.ts +392 -392
  93. package/sync/hooks.ts +450 -450
  94. package/sync/index.ts +8 -8
  95. package/sync/persistence.ts +237 -237
  96. package/sync/provider.tsx +91 -91
  97. package/sync/registry.ts +36 -36
  98. package/sync/store.ts +126 -126
  99. package/sync/transactions.ts +300 -300
  100. package/sync/types.ts +94 -94
  101. package/test/compile.spec.ts +192 -192
  102. package/test/csv-schema.spec.ts +143 -143
  103. package/test/schema-entities.spec.ts +688 -688
  104. package/tsconfig.json +24 -24
  105. package/types.ts +18 -18
@@ -1,167 +1,167 @@
1
- /**
2
- * Persistence for the in-browser warehouse blob produced by
3
- * `RadApi.exportWarehouse()`. A single blob (the whole Iceberg warehouse)
4
- * lives in a one-row IndexedDB store so `local` mode survives reloads; `s3`
5
- * mode swaps this for a {@link WarehouseSource} over object storage.
6
- */
7
-
8
- const STORE = "warehouse";
9
- const KEY = "blob";
10
-
11
- function openDb(name: string): Promise<IDBDatabase> {
12
- return new Promise((resolve, reject) => {
13
- const request = indexedDB.open(name, 1);
14
- request.onupgradeneeded = () => {
15
- const db = request.result;
16
- if (!db.objectStoreNames.contains(STORE)) {
17
- db.createObjectStore(STORE);
18
- }
19
- };
20
- request.onsuccess = () => resolve(request.result);
21
- request.onerror = () => reject(request.error);
22
- });
23
- }
24
-
25
- export class WarehousePersistence {
26
- private db: Promise<IDBDatabase> | null;
27
-
28
- constructor(private databaseName: string) {
29
- this.db =
30
- typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
31
- }
32
-
33
- get enabled(): boolean {
34
- return this.db !== null;
35
- }
36
-
37
- async load(): Promise<Uint8Array | null> {
38
- if (!this.db) return null;
39
- const db = await this.db;
40
- return new Promise((resolve, reject) => {
41
- const tx = db.transaction(STORE, "readonly");
42
- const request = tx.objectStore(STORE).get(KEY);
43
- request.onsuccess = () => {
44
- const value = request.result;
45
- resolve(value ? new Uint8Array(value) : null);
46
- };
47
- request.onerror = () => reject(request.error);
48
- });
49
- }
50
-
51
- async save(blob: Uint8Array): Promise<void> {
52
- if (!this.db) return;
53
- const db = await this.db;
54
- await new Promise<void>((resolve, reject) => {
55
- const tx = db.transaction(STORE, "readwrite");
56
- // Store a copy of the bytes; the ArrayBuffer is structured-clonable.
57
- tx.objectStore(STORE).put(blob.slice().buffer, KEY);
58
- tx.oncomplete = () => resolve();
59
- tx.onerror = () => reject(tx.error);
60
- });
61
- }
62
- }
63
-
64
- /** Fetch bytes, or `null` on 404. */
65
- async function fetchBytes(url: string): Promise<Uint8Array | null> {
66
- const response = await fetch(url);
67
- if (!response.ok) {
68
- if (response.status === 404) return null;
69
- throw new Error(`failed to fetch ${url} (${response.status})`);
70
- }
71
- return new Uint8Array(await response.arrayBuffer());
72
- }
73
-
74
- /** The engine surface the seeder needs. */
75
- interface Seedable {
76
- seedFile(key: string, bytes: Uint8Array): Promise<void>;
77
- }
78
-
79
- /**
80
- * Seed the engine by walking a committed Iceberg warehouse over HTTP — the
81
- * database is a *list of Iceberg files*, not a bundled blob. For each resource
82
- * it follows the Iceberg pointers: `version-hint.text` → the current snapshot
83
- * metadata → that snapshot's `.parquet` data files, handing each raw file to
84
- * `engine.seedFile`. This is the same walk the server does over object storage;
85
- * no directory listing is needed because Iceberg metadata is self-describing.
86
- *
87
- * Returns the number of tables seeded.
88
- */
89
- export async function seedFromIceberg(
90
- engine: Seedable,
91
- baseUrl: string,
92
- resources: { app: string; model: string }[],
93
- ): Promise<number> {
94
- const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
95
- const decoder = new TextDecoder();
96
- let tables = 0;
97
-
98
- for (const { app, model } of resources) {
99
- const prefix = `${app}.db/${model}`;
100
- const hintKey = `${prefix}/metadata/version-hint.text`;
101
- const hint = await fetchBytes(base + hintKey);
102
- if (!hint) continue; // table has no committed data yet
103
-
104
- await engine.seedFile(hintKey, hint);
105
- const version = decoder.decode(hint).trim();
106
-
107
- const metaKey = `${prefix}/metadata/v${version}.metadata.json`;
108
- const metaBytes = await fetchBytes(base + metaKey);
109
- if (!metaBytes) continue;
110
- await engine.seedFile(metaKey, metaBytes);
111
-
112
- const meta = JSON.parse(decoder.decode(metaBytes)) as { data_files?: string[] };
113
- for (const rel of meta.data_files || []) {
114
- const key = `${prefix}/${rel}`;
115
- const bytes = await fetchBytes(base + key);
116
- if (bytes) await engine.seedFile(key, bytes);
117
- }
118
- tables++;
119
- }
120
- return tables;
121
- }
122
-
123
- /** Read the `{app, model}` list out of a compiled manifest (object or string). */
124
- export function manifestResources(
125
- manifest: object | string,
126
- ): { app: string; model: string }[] {
127
- const parsed = typeof manifest === "string" ? JSON.parse(manifest) : manifest;
128
- return (parsed?.resources || []).map((r: any) => ({
129
- app: r.app,
130
- model: r.model,
131
- }));
132
- }
133
-
134
- /** Trigger a browser download of the warehouse blob so it can be committed. */
135
- export function downloadBlob(blob: Uint8Array, filename: string): void {
136
- const url = URL.createObjectURL(new Blob([blob.slice().buffer]));
137
- const a = document.createElement("a");
138
- a.href = url;
139
- a.download = filename;
140
- document.body.appendChild(a);
141
- a.click();
142
- a.remove();
143
- URL.revokeObjectURL(url);
144
- }
145
-
146
- /** A trailing debounce that always fires a final call. */
147
- export function debounce<T extends (...args: any[]) => void>(
148
- fn: T,
149
- ms: number,
150
- ): T & { flush: () => void } {
151
- let timer: ReturnType<typeof setTimeout> | null = null;
152
- const wrapped = ((...args: any[]) => {
153
- if (timer) clearTimeout(timer);
154
- timer = setTimeout(() => {
155
- timer = null;
156
- fn(...args);
157
- }, ms);
158
- }) as T & { flush: () => void };
159
- wrapped.flush = () => {
160
- if (timer) {
161
- clearTimeout(timer);
162
- timer = null;
163
- fn();
164
- }
165
- };
166
- return wrapped;
167
- }
1
+ /**
2
+ * Persistence for the in-browser warehouse blob produced by
3
+ * `RadApi.exportWarehouse()`. A single blob (the whole Iceberg warehouse)
4
+ * lives in a one-row IndexedDB store so `local` mode survives reloads; `s3`
5
+ * mode swaps this for a {@link WarehouseSource} over object storage.
6
+ */
7
+
8
+ const STORE = "warehouse";
9
+ const KEY = "blob";
10
+
11
+ function openDb(name: string): Promise<IDBDatabase> {
12
+ return new Promise((resolve, reject) => {
13
+ const request = indexedDB.open(name, 1);
14
+ request.onupgradeneeded = () => {
15
+ const db = request.result;
16
+ if (!db.objectStoreNames.contains(STORE)) {
17
+ db.createObjectStore(STORE);
18
+ }
19
+ };
20
+ request.onsuccess = () => resolve(request.result);
21
+ request.onerror = () => reject(request.error);
22
+ });
23
+ }
24
+
25
+ export class WarehousePersistence {
26
+ private db: Promise<IDBDatabase> | null;
27
+
28
+ constructor(private databaseName: string) {
29
+ this.db =
30
+ typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
31
+ }
32
+
33
+ get enabled(): boolean {
34
+ return this.db !== null;
35
+ }
36
+
37
+ async load(): Promise<Uint8Array | null> {
38
+ if (!this.db) return null;
39
+ const db = await this.db;
40
+ return new Promise((resolve, reject) => {
41
+ const tx = db.transaction(STORE, "readonly");
42
+ const request = tx.objectStore(STORE).get(KEY);
43
+ request.onsuccess = () => {
44
+ const value = request.result;
45
+ resolve(value ? new Uint8Array(value) : null);
46
+ };
47
+ request.onerror = () => reject(request.error);
48
+ });
49
+ }
50
+
51
+ async save(blob: Uint8Array): Promise<void> {
52
+ if (!this.db) return;
53
+ const db = await this.db;
54
+ await new Promise<void>((resolve, reject) => {
55
+ const tx = db.transaction(STORE, "readwrite");
56
+ // Store a copy of the bytes; the ArrayBuffer is structured-clonable.
57
+ tx.objectStore(STORE).put(blob.slice().buffer, KEY);
58
+ tx.oncomplete = () => resolve();
59
+ tx.onerror = () => reject(tx.error);
60
+ });
61
+ }
62
+ }
63
+
64
+ /** Fetch bytes, or `null` on 404. */
65
+ async function fetchBytes(url: string): Promise<Uint8Array | null> {
66
+ const response = await fetch(url);
67
+ if (!response.ok) {
68
+ if (response.status === 404) return null;
69
+ throw new Error(`failed to fetch ${url} (${response.status})`);
70
+ }
71
+ return new Uint8Array(await response.arrayBuffer());
72
+ }
73
+
74
+ /** The engine surface the seeder needs. */
75
+ interface Seedable {
76
+ seedFile(key: string, bytes: Uint8Array): Promise<void>;
77
+ }
78
+
79
+ /**
80
+ * Seed the engine by walking a committed Iceberg warehouse over HTTP — the
81
+ * database is a *list of Iceberg files*, not a bundled blob. For each resource
82
+ * it follows the Iceberg pointers: `version-hint.text` → the current snapshot
83
+ * metadata → that snapshot's `.parquet` data files, handing each raw file to
84
+ * `engine.seedFile`. This is the same walk the server does over object storage;
85
+ * no directory listing is needed because Iceberg metadata is self-describing.
86
+ *
87
+ * Returns the number of tables seeded.
88
+ */
89
+ export async function seedFromIceberg(
90
+ engine: Seedable,
91
+ baseUrl: string,
92
+ resources: { app: string; model: string }[],
93
+ ): Promise<number> {
94
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
95
+ const decoder = new TextDecoder();
96
+ let tables = 0;
97
+
98
+ for (const { app, model } of resources) {
99
+ const prefix = `${app}.db/${model}`;
100
+ const hintKey = `${prefix}/metadata/version-hint.text`;
101
+ const hint = await fetchBytes(base + hintKey);
102
+ if (!hint) continue; // table has no committed data yet
103
+
104
+ await engine.seedFile(hintKey, hint);
105
+ const version = decoder.decode(hint).trim();
106
+
107
+ const metaKey = `${prefix}/metadata/v${version}.metadata.json`;
108
+ const metaBytes = await fetchBytes(base + metaKey);
109
+ if (!metaBytes) continue;
110
+ await engine.seedFile(metaKey, metaBytes);
111
+
112
+ const meta = JSON.parse(decoder.decode(metaBytes)) as { data_files?: string[] };
113
+ for (const rel of meta.data_files || []) {
114
+ const key = `${prefix}/${rel}`;
115
+ const bytes = await fetchBytes(base + key);
116
+ if (bytes) await engine.seedFile(key, bytes);
117
+ }
118
+ tables++;
119
+ }
120
+ return tables;
121
+ }
122
+
123
+ /** Read the `{app, model}` list out of a compiled manifest (object or string). */
124
+ export function manifestResources(
125
+ manifest: object | string,
126
+ ): { app: string; model: string }[] {
127
+ const parsed = typeof manifest === "string" ? JSON.parse(manifest) : manifest;
128
+ return (parsed?.resources || []).map((r: any) => ({
129
+ app: r.app,
130
+ model: r.model,
131
+ }));
132
+ }
133
+
134
+ /** Trigger a browser download of the warehouse blob so it can be committed. */
135
+ export function downloadBlob(blob: Uint8Array, filename: string): void {
136
+ const url = URL.createObjectURL(new Blob([blob.slice().buffer]));
137
+ const a = document.createElement("a");
138
+ a.href = url;
139
+ a.download = filename;
140
+ document.body.appendChild(a);
141
+ a.click();
142
+ a.remove();
143
+ URL.revokeObjectURL(url);
144
+ }
145
+
146
+ /** A trailing debounce that always fires a final call. */
147
+ export function debounce<T extends (...args: any[]) => void>(
148
+ fn: T,
149
+ ms: number,
150
+ ): T & { flush: () => void } {
151
+ let timer: ReturnType<typeof setTimeout> | null = null;
152
+ const wrapped = ((...args: any[]) => {
153
+ if (timer) clearTimeout(timer);
154
+ timer = setTimeout(() => {
155
+ timer = null;
156
+ fn(...args);
157
+ }, ms);
158
+ }) as T & { flush: () => void };
159
+ wrapped.flush = () => {
160
+ if (timer) {
161
+ clearTimeout(timer);
162
+ timer = null;
163
+ fn();
164
+ }
165
+ };
166
+ return wrapped;
167
+ }
package/provider/wasm.ts CHANGED
@@ -1,124 +1,124 @@
1
- import { AxiosAdapter, AxiosError, AxiosResponse } from "axios";
2
- import { RadEngine, RadEngineLoader, RadWasmModule } from "./types";
3
-
4
- /**
5
- * Default WASM loader: import the bundle `rad wasm` generates
6
- * (`rastack/wasm/rad_wasm.js`) and run its init. The indirection through
7
- * `new Function` keeps a genuine dynamic `import()` at runtime even when the
8
- * package is transpiled to CommonJS, so the consumer's bundler resolves and
9
- * code-splits the `.wasm`.
10
- */
11
- const runtimeImport: (specifier: string) => Promise<any> =
12
- // eslint-disable-next-line no-new-func
13
- new Function("s", "return import(s)") as any;
14
-
15
- export const defaultEngineLoader: RadEngineLoader = async () => {
16
- const mod: RadWasmModule = await runtimeImport("rastack/wasm/rad_wasm.js");
17
- const init = mod.default || mod.init;
18
- if (typeof init === "function") {
19
- await init();
20
- }
21
- return mod;
22
- };
23
-
24
- /**
25
- * Instantiate the engine from a loaded module and the compiled schema. The
26
- * manifest/openapi are passed as JSON strings — the same `schema.rad.json` /
27
- * `openapi.json` the Rust server reads.
28
- */
29
- export function createEngine(
30
- mod: RadWasmModule,
31
- manifest: object | string,
32
- openapi?: object | string,
33
- ): RadEngine {
34
- const manifestJson =
35
- typeof manifest === "string" ? manifest : JSON.stringify(manifest);
36
- const openapiJson =
37
- openapi === undefined
38
- ? undefined
39
- : typeof openapi === "string"
40
- ? openapi
41
- : JSON.stringify(openapi);
42
- return new mod.RadApi(manifestJson, openapiJson);
43
- }
44
-
45
- const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
46
-
47
- /**
48
- * An axios adapter that dispatches requests into the in-browser WASM engine
49
- * instead of over the network. Translates an axios request into the engine's
50
- * `handle(method, path, query, body)` and maps the `{ status, body }` result
51
- * back to an `AxiosResponse` — rejecting on non-2xx exactly as HTTP axios does,
52
- * so `react-query` error handling is unchanged.
53
- *
54
- * `onWrite` fires after each successful mutation so the provider can debounce a
55
- * persist of the warehouse.
56
- */
57
- export function createWasmAdapter(
58
- engine: RadEngine,
59
- onWrite?: () => void,
60
- ): AxiosAdapter {
61
- return async (config: any): Promise<AxiosResponse> => {
62
- const method = String(config.method || "get").toUpperCase();
63
-
64
- const rawUrl = config.url || "";
65
- const [rawPath, rawQuery] = rawUrl.split("?");
66
- // Strip any origin so only the `/api/...` path reaches the engine.
67
- const path = rawPath.replace(/^https?:\/\/[^/]+/i, "") || "/";
68
-
69
- const query: Record<string, string> = {};
70
- if (rawQuery) {
71
- new URLSearchParams(rawQuery).forEach((value, key) => {
72
- query[key] = value;
73
- });
74
- }
75
- if (config.params && typeof config.params === "object") {
76
- for (const [key, value] of Object.entries(config.params)) {
77
- if (value !== undefined && value !== null) query[key] = String(value);
78
- }
79
- }
80
-
81
- // axios has already run transformRequest, so JSON bodies arrive as strings.
82
- let body: unknown;
83
- if (typeof config.data === "string" && config.data.length > 0) {
84
- try {
85
- body = JSON.parse(config.data);
86
- } catch {
87
- body = config.data;
88
- }
89
- } else if (config.data && typeof config.data === "object") {
90
- body = config.data;
91
- }
92
-
93
- const result = await engine.handle(
94
- method,
95
- path,
96
- Object.keys(query).length ? query : undefined,
97
- body,
98
- );
99
-
100
- const response: AxiosResponse = {
101
- data: result.body,
102
- status: result.status,
103
- statusText: String(result.status),
104
- headers: {},
105
- config,
106
- request: null,
107
- };
108
-
109
- const validate =
110
- config.validateStatus || ((s: number) => s >= 200 && s < 300);
111
- if (!validate(result.status)) {
112
- throw new AxiosError(
113
- `Request failed with status code ${result.status}`,
114
- result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
115
- config,
116
- null,
117
- response,
118
- );
119
- }
120
-
121
- if (MUTATING.has(method) && onWrite) onWrite();
122
- return response;
123
- };
124
- }
1
+ import { AxiosAdapter, AxiosError, AxiosResponse } from "axios";
2
+ import { RadEngine, RadEngineLoader, RadWasmModule } from "./types";
3
+
4
+ /**
5
+ * Default WASM loader: import the bundle `rad wasm` generates
6
+ * (`rastack/wasm/rad_wasm.js`) and run its init. The indirection through
7
+ * `new Function` keeps a genuine dynamic `import()` at runtime even when the
8
+ * package is transpiled to CommonJS, so the consumer's bundler resolves and
9
+ * code-splits the `.wasm`.
10
+ */
11
+ const runtimeImport: (specifier: string) => Promise<any> =
12
+ // eslint-disable-next-line no-new-func
13
+ new Function("s", "return import(s)") as any;
14
+
15
+ export const defaultEngineLoader: RadEngineLoader = async () => {
16
+ const mod: RadWasmModule = await runtimeImport("rastack/wasm/rad_wasm.js");
17
+ const init = mod.default || mod.init;
18
+ if (typeof init === "function") {
19
+ await init();
20
+ }
21
+ return mod;
22
+ };
23
+
24
+ /**
25
+ * Instantiate the engine from a loaded module and the compiled schema. The
26
+ * manifest/openapi are passed as JSON strings — the same `schema.rad.json` /
27
+ * `openapi.json` the Rust server reads.
28
+ */
29
+ export function createEngine(
30
+ mod: RadWasmModule,
31
+ manifest: object | string,
32
+ openapi?: object | string,
33
+ ): RadEngine {
34
+ const manifestJson =
35
+ typeof manifest === "string" ? manifest : JSON.stringify(manifest);
36
+ const openapiJson =
37
+ openapi === undefined
38
+ ? undefined
39
+ : typeof openapi === "string"
40
+ ? openapi
41
+ : JSON.stringify(openapi);
42
+ return new mod.RadApi(manifestJson, openapiJson);
43
+ }
44
+
45
+ const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
46
+
47
+ /**
48
+ * An axios adapter that dispatches requests into the in-browser WASM engine
49
+ * instead of over the network. Translates an axios request into the engine's
50
+ * `handle(method, path, query, body)` and maps the `{ status, body }` result
51
+ * back to an `AxiosResponse` — rejecting on non-2xx exactly as HTTP axios does,
52
+ * so `react-query` error handling is unchanged.
53
+ *
54
+ * `onWrite` fires after each successful mutation so the provider can debounce a
55
+ * persist of the warehouse.
56
+ */
57
+ export function createWasmAdapter(
58
+ engine: RadEngine,
59
+ onWrite?: () => void,
60
+ ): AxiosAdapter {
61
+ return async (config: any): Promise<AxiosResponse> => {
62
+ const method = String(config.method || "get").toUpperCase();
63
+
64
+ const rawUrl = config.url || "";
65
+ const [rawPath, rawQuery] = rawUrl.split("?");
66
+ // Strip any origin so only the `/api/...` path reaches the engine.
67
+ const path = rawPath.replace(/^https?:\/\/[^/]+/i, "") || "/";
68
+
69
+ const query: Record<string, string> = {};
70
+ if (rawQuery) {
71
+ new URLSearchParams(rawQuery).forEach((value, key) => {
72
+ query[key] = value;
73
+ });
74
+ }
75
+ if (config.params && typeof config.params === "object") {
76
+ for (const [key, value] of Object.entries(config.params)) {
77
+ if (value !== undefined && value !== null) query[key] = String(value);
78
+ }
79
+ }
80
+
81
+ // axios has already run transformRequest, so JSON bodies arrive as strings.
82
+ let body: unknown;
83
+ if (typeof config.data === "string" && config.data.length > 0) {
84
+ try {
85
+ body = JSON.parse(config.data);
86
+ } catch {
87
+ body = config.data;
88
+ }
89
+ } else if (config.data && typeof config.data === "object") {
90
+ body = config.data;
91
+ }
92
+
93
+ const result = await engine.handle(
94
+ method,
95
+ path,
96
+ Object.keys(query).length ? query : undefined,
97
+ body,
98
+ );
99
+
100
+ const response: AxiosResponse = {
101
+ data: result.body,
102
+ status: result.status,
103
+ statusText: String(result.status),
104
+ headers: {},
105
+ config,
106
+ request: null,
107
+ };
108
+
109
+ const validate =
110
+ config.validateStatus || ((s: number) => s >= 200 && s < 300);
111
+ if (!validate(result.status)) {
112
+ throw new AxiosError(
113
+ `Request failed with status code ${result.status}`,
114
+ result.status >= 500 ? "ERR_BAD_RESPONSE" : "ERR_BAD_REQUEST",
115
+ config,
116
+ null,
117
+ response,
118
+ );
119
+ }
120
+
121
+ if (MUTATING.has(method) && onWrite) onWrite();
122
+ return response;
123
+ };
124
+ }
package/runtime.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from "./hooks";
2
- export * from "./types";
3
- export * from "./provider";
1
+ export * from "./hooks";
2
+ export * from "./types";
3
+ export * from "./provider";
package/src/.prettierrc CHANGED
@@ -1,7 +1,7 @@
1
- {
2
- "tabWidth": 2,
3
- "semi": true,
4
- "bracketSpacing": true,
5
- "arrowParens": "always",
6
- "printWidth": 80
7
- }
1
+ {
2
+ "tabWidth": 2,
3
+ "semi": true,
4
+ "bracketSpacing": true,
5
+ "arrowParens": "always",
6
+ "printWidth": 80
7
+ }