rastack 0.0.16
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/CHANGELOG.md +43013 -0
- package/dist/.prettierrc +7 -0
- package/dist/compile/analyze.d.ts +12 -0
- package/dist/compile/analyze.js +233 -0
- package/dist/compile/index.d.ts +19 -0
- package/dist/compile/index.js +107 -0
- package/dist/compile/manifest.d.ts +4 -0
- package/dist/compile/manifest.js +12 -0
- package/dist/compile/model.d.ts +54 -0
- package/dist/compile/model.js +7 -0
- package/dist/compile/openapi.d.ts +11 -0
- package/dist/compile/openapi.js +233 -0
- package/dist/compile/program.d.ts +10 -0
- package/dist/compile/program.js +61 -0
- package/dist/csv-schema.d.ts +28 -0
- package/dist/csv-schema.js +162 -0
- package/dist/define/index.d.ts +117 -0
- package/dist/define/index.js +74 -0
- package/dist/entity-generation/delete-method.d.ts +1 -0
- package/dist/entity-generation/delete-method.js +31 -0
- package/dist/entity-generation/form.d.ts +13 -0
- package/dist/entity-generation/form.js +136 -0
- package/dist/entity-generation/get-method.d.ts +1 -0
- package/dist/entity-generation/get-method.js +67 -0
- package/dist/entity-generation/imports.d.ts +5 -0
- package/dist/entity-generation/imports.js +91 -0
- package/dist/entity-generation/sync-method.d.ts +30 -0
- package/dist/entity-generation/sync-method.js +158 -0
- package/dist/entity-generation/update-method.d.ts +1 -0
- package/dist/entity-generation/update-method.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/rad-compile.d.ts +11 -0
- package/dist/rad-compile.js +82 -0
- package/dist/rad-wasm-build.d.ts +14 -0
- package/dist/rad-wasm-build.js +74 -0
- package/dist/rad.d.ts +17 -0
- package/dist/rad.js +88 -0
- package/dist/scan.d.ts +12 -0
- package/dist/scan.js +79 -0
- package/dist/schema/camel-to-pastel.d.ts +1 -0
- package/dist/schema/camel-to-pastel.js +7 -0
- package/dist/schema/capitalise-first-letter.d.ts +1 -0
- package/dist/schema/capitalise-first-letter.js +7 -0
- package/dist/schema/extract-response.d.ts +2 -0
- package/dist/schema/extract-response.js +30 -0
- package/dist/schema/fetch-schema.d.ts +7 -0
- package/dist/schema/fetch-schema.js +23 -0
- package/dist/schema/remove-non-model-paths.d.ts +2 -0
- package/dist/schema/remove-non-model-paths.js +15 -0
- package/dist/schema/to-camel-case.d.ts +1 -0
- package/dist/schema/to-camel-case.js +11 -0
- package/dist/schema/to-pastel-case.d.ts +1 -0
- package/dist/schema/to-pastel-case.js +10 -0
- package/dist/schema-convert.d.ts +1 -0
- package/dist/schema-convert.js +63 -0
- package/dist/schema-entities.d.ts +14 -0
- package/dist/schema-entities.js +293 -0
- package/dist/schema-fetch.d.ts +1 -0
- package/dist/schema-fetch.js +81 -0
- package/dist/schema-full.d.ts +1 -0
- package/dist/schema-full.js +45 -0
- package/dist/schema-index.d.ts +1 -0
- package/dist/schema-index.js +34 -0
- package/dist/schema-params.d.ts +1 -0
- package/dist/schema-params.js +100 -0
- package/dist/seed.d.ts +17 -0
- package/dist/seed.js +252 -0
- package/hooks/form/form.ts +207 -0
- package/hooks/form/index.ts +8 -0
- package/hooks/form/interfaces.ts +217 -0
- package/hooks/form/structure.ts +39 -0
- package/hooks/form/validate-schema.ts +49 -0
- package/hooks/index.ts +3 -0
- package/hooks/query/api.ts +42 -0
- package/hooks/query/delete.ts +45 -0
- package/hooks/query/fetch.ts +48 -0
- package/hooks/query/index.ts +21 -0
- package/hooks/query/interfaces.ts +111 -0
- package/hooks/query/list.ts +286 -0
- package/hooks/query/update.ts +88 -0
- package/hooks/query/url.ts +31 -0
- package/hooks/real-time/index.ts +1 -0
- package/hooks/real-time/pusher.ts +43 -0
- package/jest.config.cjs +6 -0
- package/package.json +57 -0
- package/provider/index.ts +13 -0
- package/provider/provider.tsx +187 -0
- package/provider/types.ts +114 -0
- package/provider/warehouse.ts +167 -0
- package/provider/wasm.ts +124 -0
- package/runtime.ts +3 -0
- package/src/.prettierrc +7 -0
- package/src/compile/analyze.ts +224 -0
- package/src/compile/index.ts +86 -0
- package/src/compile/manifest.ts +10 -0
- package/src/compile/model.ts +69 -0
- package/src/compile/openapi.ts +266 -0
- package/src/compile/program.ts +40 -0
- package/src/csv-schema.ts +187 -0
- package/src/define/index.ts +162 -0
- package/src/entity-generation/delete-method.ts +37 -0
- package/src/entity-generation/form.ts +169 -0
- package/src/entity-generation/get-method.ts +80 -0
- package/src/entity-generation/imports.ts +110 -0
- package/src/entity-generation/sync-method.ts +222 -0
- package/src/entity-generation/update-method.ts +38 -0
- package/src/index.ts +77 -0
- package/src/rad-compile.ts +88 -0
- package/src/rad-wasm-build.ts +102 -0
- package/src/rad.ts +101 -0
- package/src/scan.ts +89 -0
- package/src/schema/camel-to-pastel.ts +3 -0
- package/src/schema/capitalise-first-letter.ts +3 -0
- package/src/schema/extract-response.ts +38 -0
- package/src/schema/fetch-schema.ts +12 -0
- package/src/schema/remove-non-model-paths.ts +13 -0
- package/src/schema/to-camel-case.ts +7 -0
- package/src/schema/to-pastel-case.ts +6 -0
- package/src/schema-convert.ts +61 -0
- package/src/schema-entities.ts +401 -0
- package/src/schema-fetch.ts +82 -0
- package/src/schema-full.ts +38 -0
- package/src/schema-index.ts +29 -0
- package/src/schema-params.ts +116 -0
- package/src/seed.ts +297 -0
- package/sync/engine.ts +392 -0
- package/sync/hooks.ts +450 -0
- package/sync/index.ts +8 -0
- package/sync/persistence.ts +237 -0
- package/sync/provider.tsx +91 -0
- package/sync/registry.ts +36 -0
- package/sync/store.ts +126 -0
- package/sync/transactions.ts +300 -0
- package/sync/types.ts +94 -0
- package/test/compile.spec.ts +192 -0
- package/test/csv-schema.spec.ts +143 -0
- package/test/schema-entities.spec.ts +688 -0
- package/tsconfig.json +24 -0
- package/types.ts +19 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { AxiosResponse } from "axios";
|
|
2
|
+
import { useMutation, useQueryClient } from "react-query";
|
|
3
|
+
import { IUpdateQueryResult, WriteQueryVariables } from "./interfaces";
|
|
4
|
+
import { API } from "./api";
|
|
5
|
+
import { stringifyQueryParams } from "./url";
|
|
6
|
+
|
|
7
|
+
interface UpdateQueryProps<TParam> {
|
|
8
|
+
putUrl?: ((params: TParam) => string) | string;
|
|
9
|
+
patchUrl?: ((params: TParam) => string) | string;
|
|
10
|
+
postUrl?: ((params: TParam) => string) | string;
|
|
11
|
+
listQueryCacheKey?: string | undefined;
|
|
12
|
+
itemQueryCacheKey?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useUpdateQuery<TParams, TPayload, TResult>({
|
|
16
|
+
putUrl,
|
|
17
|
+
patchUrl,
|
|
18
|
+
postUrl,
|
|
19
|
+
listQueryCacheKey,
|
|
20
|
+
itemQueryCacheKey,
|
|
21
|
+
}: UpdateQueryProps<TParams>): IUpdateQueryResult<TParams, TPayload, TResult> {
|
|
22
|
+
if (!putUrl && !patchUrl && !postUrl) {
|
|
23
|
+
throw new Error("Either putUrl, patchUrl, or postUrl must be provided");
|
|
24
|
+
}
|
|
25
|
+
const queryClient = useQueryClient();
|
|
26
|
+
|
|
27
|
+
return useMutation<
|
|
28
|
+
TResult,
|
|
29
|
+
unknown,
|
|
30
|
+
WriteQueryVariables<TParams, TPayload>,
|
|
31
|
+
unknown
|
|
32
|
+
>(({ params, payload }) => {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let urlType: "put" | "patch" | "post" = "patch";
|
|
35
|
+
let url: string | undefined;
|
|
36
|
+
|
|
37
|
+
if (putUrl) {
|
|
38
|
+
url = typeof putUrl === "function" ? putUrl(params) : putUrl;
|
|
39
|
+
urlType = "put";
|
|
40
|
+
} else if (patchUrl) {
|
|
41
|
+
url = typeof patchUrl === "function" ? patchUrl(params) : patchUrl;
|
|
42
|
+
urlType = "patch";
|
|
43
|
+
} else if (postUrl) {
|
|
44
|
+
url = typeof postUrl === "function" ? postUrl(params) : postUrl;
|
|
45
|
+
urlType = "post";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!url) {
|
|
49
|
+
throw new Error("URL must be provided for the API call");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
const urlParams = stringifyQueryParams([], params);
|
|
54
|
+
const serverURL = `${url}${urlParams ? `?${urlParams}` : ""}`;
|
|
55
|
+
|
|
56
|
+
const getAPIFunction = (
|
|
57
|
+
urlType: string,
|
|
58
|
+
url: string,
|
|
59
|
+
payload: TPayload,
|
|
60
|
+
) => {
|
|
61
|
+
switch (urlType) {
|
|
62
|
+
case "put":
|
|
63
|
+
return API.put(serverURL, payload);
|
|
64
|
+
case "patch":
|
|
65
|
+
return API.patch(serverURL, payload);
|
|
66
|
+
case "post":
|
|
67
|
+
return API.post(serverURL, payload);
|
|
68
|
+
default:
|
|
69
|
+
throw new Error("Invalid URL type");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
getAPIFunction(urlType, url, payload)
|
|
74
|
+
.then((response: AxiosResponse<TResult>) => {
|
|
75
|
+
if (itemQueryCacheKey) {
|
|
76
|
+
queryClient.invalidateQueries([itemQueryCacheKey]);
|
|
77
|
+
}
|
|
78
|
+
if (listQueryCacheKey) {
|
|
79
|
+
queryClient.invalidateQueries(listQueryCacheKey);
|
|
80
|
+
}
|
|
81
|
+
resolve(response.data);
|
|
82
|
+
})
|
|
83
|
+
.catch((error) => {
|
|
84
|
+
reject(error);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function stringifyQueryParams<TParam>(
|
|
2
|
+
pathParameters: (keyof TParam)[],
|
|
3
|
+
filteredQueryParams?: Partial<TParam>,
|
|
4
|
+
): string | undefined {
|
|
5
|
+
if (!filteredQueryParams) return undefined;
|
|
6
|
+
|
|
7
|
+
const urlParams = new URLSearchParams();
|
|
8
|
+
|
|
9
|
+
for (const key in filteredQueryParams) {
|
|
10
|
+
if (pathParameters.includes(key as any)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (Object.prototype.hasOwnProperty.call(filteredQueryParams, key)) {
|
|
14
|
+
const value = filteredQueryParams[key];
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
value.forEach((item) => {
|
|
18
|
+
if (item !== undefined && item !== null) {
|
|
19
|
+
urlParams.append(key, item.toString());
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} else {
|
|
23
|
+
if (value !== undefined && value !== null) {
|
|
24
|
+
urlParams.set(key, value.toString());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return urlParams.toString();
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePusher } from "./pusher";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useSyncEngineOptional } from "../../sync/provider";
|
|
3
|
+
|
|
4
|
+
export interface PusherWakeOptions {
|
|
5
|
+
/** Channel to subscribe to. Defaults to "rad-sync". */
|
|
6
|
+
channel?: string;
|
|
7
|
+
/** Event name carrying sync notifications. Defaults to "sync". */
|
|
8
|
+
event?: string;
|
|
9
|
+
/** A connected Pusher client instance. */
|
|
10
|
+
client?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Optional push transport for the sync engine. Given a connected Pusher
|
|
15
|
+
* client (passed in, or found on `window.Pusher.instances`), subscribes to
|
|
16
|
+
* a channel and wakes the engine whenever a sync event arrives — the engine
|
|
17
|
+
* then pulls the actual changes over HTTP. Push is a latency optimization
|
|
18
|
+
* only; the long-poll `/changes` loop stays the source of truth, so this
|
|
19
|
+
* hook is always safe to omit.
|
|
20
|
+
*/
|
|
21
|
+
export function usePusher(options?: PusherWakeOptions) {
|
|
22
|
+
const engine = useSyncEngineOptional();
|
|
23
|
+
const channelName = options?.channel || "rad-sync";
|
|
24
|
+
const eventName = options?.event || "sync";
|
|
25
|
+
const client =
|
|
26
|
+
options?.client ??
|
|
27
|
+
(typeof window !== "undefined"
|
|
28
|
+
? (window as any).Pusher?.instances?.[0]
|
|
29
|
+
: undefined);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!engine || !client) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const channel = client.subscribe(channelName);
|
|
36
|
+
const handler = () => engine.wake();
|
|
37
|
+
channel.bind(eventName, handler);
|
|
38
|
+
return () => {
|
|
39
|
+
channel.unbind(eventName, handler);
|
|
40
|
+
client.unsubscribe(channelName);
|
|
41
|
+
};
|
|
42
|
+
}, [engine, client, channelName, eventName]);
|
|
43
|
+
}
|
package/jest.config.cjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rastack",
|
|
3
|
+
"version": "0.0.16",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "runtime.ts",
|
|
6
|
+
"types": "runtime.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rastack": "dist/index.js",
|
|
9
|
+
"rad": "dist/rad.js"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
16
|
+
"build": "tsc && cp src/.prettierrc dist/.prettierrc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"test": "jest"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@apidevtools/json-schema-ref-parser": "^15.4.0",
|
|
25
|
+
"@tanstack/react-query": "^5.101.2",
|
|
26
|
+
"@ts-stack/openapi-spec": "^3.1.5",
|
|
27
|
+
"@types/json-schema": "^7.0.15",
|
|
28
|
+
"@types/react": "^19.2.17",
|
|
29
|
+
"ajv": "^8.20.0",
|
|
30
|
+
"ajv-formats": "^3.0.1",
|
|
31
|
+
"axios": "^1.16.0",
|
|
32
|
+
"formik": "^2.4.9",
|
|
33
|
+
"js-yaml": "^5.2.1",
|
|
34
|
+
"json-schema": "^0.4.0",
|
|
35
|
+
"json-schema-to-typescript": "^15.0.4",
|
|
36
|
+
"jsonschema": "^1.5.0",
|
|
37
|
+
"lodash": "^4.18.0",
|
|
38
|
+
"luxon": "^3.7.2",
|
|
39
|
+
"minimatch": "^10.2.5",
|
|
40
|
+
"prettier": "^3.9.4",
|
|
41
|
+
"react-query": "^3.39.3",
|
|
42
|
+
"typescript": "^6.0.3",
|
|
43
|
+
"usehooks-ts": "^3.1.1"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^19.2.7"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/jest": "^30.0.0",
|
|
50
|
+
"@types/js-yaml": "^4.0.9",
|
|
51
|
+
"@types/mocha": "^10.0.6",
|
|
52
|
+
"@types/node": "^26.1.0",
|
|
53
|
+
"eslint": "^10.6.0",
|
|
54
|
+
"jest": "^30.4.2",
|
|
55
|
+
"ts-jest": "^29.4.11"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./provider";
|
|
3
|
+
export {
|
|
4
|
+
createWasmAdapter,
|
|
5
|
+
createEngine,
|
|
6
|
+
defaultEngineLoader,
|
|
7
|
+
} from "./wasm";
|
|
8
|
+
export {
|
|
9
|
+
WarehousePersistence,
|
|
10
|
+
seedFromIceberg,
|
|
11
|
+
manifestResources,
|
|
12
|
+
downloadBlob,
|
|
13
|
+
} from "./warehouse";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { API, configureApi, resetApi } from "../hooks/query/api";
|
|
11
|
+
import {
|
|
12
|
+
RadClient,
|
|
13
|
+
RadEngine,
|
|
14
|
+
RAStackProviderConfig,
|
|
15
|
+
RadStatus,
|
|
16
|
+
WarehouseSource,
|
|
17
|
+
} from "./types";
|
|
18
|
+
import { createEngine, createWasmAdapter, defaultEngineLoader } from "./wasm";
|
|
19
|
+
import {
|
|
20
|
+
debounce,
|
|
21
|
+
downloadBlob,
|
|
22
|
+
manifestResources,
|
|
23
|
+
seedFromIceberg,
|
|
24
|
+
WarehousePersistence,
|
|
25
|
+
} from "./warehouse";
|
|
26
|
+
|
|
27
|
+
const RadContext = createContext<RadClient | null>(null);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The one place a React app configures *where the API runs* — over HTTP
|
|
31
|
+
* (`remote`), in the browser via WebAssembly (`local`), or in the browser
|
|
32
|
+
* against S3 (`s3`). It reconfigures the shared axios client in place, so every
|
|
33
|
+
* generated hook keeps working with no changes.
|
|
34
|
+
*
|
|
35
|
+
* ```tsx
|
|
36
|
+
* <RAStackProvider mode="local" manifest={schema} warehouseBaseUrl="/data/warehouse/">
|
|
37
|
+
* <App />
|
|
38
|
+
* </RAStackProvider>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function RAStackProvider({
|
|
42
|
+
children,
|
|
43
|
+
...config
|
|
44
|
+
}: RAStackProviderConfig & { children: ReactNode }): JSX.Element {
|
|
45
|
+
const [status, setStatus] = useState<RadStatus>("idle");
|
|
46
|
+
const engineRef = useRef<RadEngine | null>(null);
|
|
47
|
+
const persistRef = useRef<(() => void) & { flush: () => void }>();
|
|
48
|
+
|
|
49
|
+
const setStatusSafe = (next: RadStatus) => {
|
|
50
|
+
setStatus(next);
|
|
51
|
+
config.onStatusChange?.(next);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
let ejectToken: number | undefined;
|
|
57
|
+
|
|
58
|
+
async function setup() {
|
|
59
|
+
// Bearer-token interceptor works in every mode.
|
|
60
|
+
if (config.getToken) {
|
|
61
|
+
ejectToken = API.interceptors.request.use(async (request) => {
|
|
62
|
+
const token = await config.getToken!();
|
|
63
|
+
request.headers = request.headers || ({} as any);
|
|
64
|
+
(request.headers as any).Authorization = `Bearer ${token}`;
|
|
65
|
+
return request;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.mode === "remote") {
|
|
70
|
+
configureApi({ baseURL: config.baseURL ?? "", adapter: null });
|
|
71
|
+
if (config.baseURL === undefined) resetApi();
|
|
72
|
+
setStatusSafe("ready");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// -- WASM modes (local / s3) --
|
|
77
|
+
if (!config.manifest) {
|
|
78
|
+
throw new Error("RAStackProvider: `manifest` is required in local/s3 mode.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setStatusSafe("loading");
|
|
82
|
+
const loader = config.loadEngine ?? defaultEngineLoader;
|
|
83
|
+
const mod = await loader();
|
|
84
|
+
if (cancelled) return;
|
|
85
|
+
const engine = createEngine(mod, config.manifest, config.openapi);
|
|
86
|
+
|
|
87
|
+
// Warehouse persistence: IndexedDB for `local`, an S3 sink for `s3`.
|
|
88
|
+
const persistence = pickPersistence(config);
|
|
89
|
+
|
|
90
|
+
setStatusSafe("seeding");
|
|
91
|
+
// A persisted local copy (with the user's edits) wins; otherwise seed
|
|
92
|
+
// from the committed Iceberg warehouse — a directory of files walked over
|
|
93
|
+
// HTTP, not a blob.
|
|
94
|
+
const existing = await persistence.load();
|
|
95
|
+
if (existing) {
|
|
96
|
+
await engine.importWarehouse(existing);
|
|
97
|
+
} else {
|
|
98
|
+
const base = config.warehouseBaseUrl ?? "/data/warehouse/";
|
|
99
|
+
await seedFromIceberg(engine, base, manifestResources(config.manifest));
|
|
100
|
+
}
|
|
101
|
+
if (cancelled) return;
|
|
102
|
+
|
|
103
|
+
const persist = debounce(() => {
|
|
104
|
+
void engine
|
|
105
|
+
.exportWarehouse()
|
|
106
|
+
.then((blob) => persistence.save(blob))
|
|
107
|
+
.catch((error) => config.onError?.(error));
|
|
108
|
+
}, 400);
|
|
109
|
+
persistRef.current = persist;
|
|
110
|
+
|
|
111
|
+
engineRef.current = engine;
|
|
112
|
+
configureApi({ baseURL: "", adapter: createWasmAdapter(engine, persist) });
|
|
113
|
+
setStatusSafe("ready");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setup().catch((error) => {
|
|
117
|
+
if (cancelled) return;
|
|
118
|
+
config.onError?.(error);
|
|
119
|
+
setStatusSafe("error");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
persistRef.current?.flush();
|
|
125
|
+
if (ejectToken !== undefined) API.interceptors.request.eject(ejectToken);
|
|
126
|
+
resetApi();
|
|
127
|
+
engineRef.current = null;
|
|
128
|
+
};
|
|
129
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
|
+
}, [config.mode, config.baseURL, config.warehouseBaseUrl]);
|
|
131
|
+
|
|
132
|
+
const client = useMemo<RadClient>(
|
|
133
|
+
() => ({
|
|
134
|
+
mode: config.mode,
|
|
135
|
+
status,
|
|
136
|
+
get engine() {
|
|
137
|
+
return engineRef.current;
|
|
138
|
+
},
|
|
139
|
+
async exportWarehouse() {
|
|
140
|
+
if (!engineRef.current) {
|
|
141
|
+
throw new Error("exportWarehouse is only available in local/s3 mode.");
|
|
142
|
+
}
|
|
143
|
+
return engineRef.current.exportWarehouse();
|
|
144
|
+
},
|
|
145
|
+
async downloadWarehouse(filename = "warehouse-snapshot.json") {
|
|
146
|
+
const blob = await this.exportWarehouse();
|
|
147
|
+
downloadBlob(blob, filename);
|
|
148
|
+
},
|
|
149
|
+
async persistNow() {
|
|
150
|
+
persistRef.current?.flush();
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
[config.mode, status],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return <RadContext.Provider value={client}>{children}</RadContext.Provider>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pickPersistence(config: RAStackProviderConfig): WarehouseSource {
|
|
160
|
+
if (config.mode === "s3") {
|
|
161
|
+
if (!config.warehouse) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"RAStackProvider: `warehouse` (a WarehouseSource over S3) is required in s3 mode.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return config.warehouse;
|
|
167
|
+
}
|
|
168
|
+
if (config.persist === false) {
|
|
169
|
+
return { load: async () => null, save: async () => {} };
|
|
170
|
+
}
|
|
171
|
+
const store = new WarehousePersistence(config.databaseName || "rad-warehouse");
|
|
172
|
+
return { load: () => store.load(), save: (blob) => store.save(blob) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Access the imperative Rad client (engine, status, warehouse helpers). */
|
|
176
|
+
export function useRadClient(): RadClient {
|
|
177
|
+
const client = useContext(RadContext);
|
|
178
|
+
if (!client) {
|
|
179
|
+
throw new Error("useRadClient must be used within a <RAStackProvider>.");
|
|
180
|
+
}
|
|
181
|
+
return client;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Just the current status — handy for a loading gate while WASM boots. */
|
|
185
|
+
export function useRadStatus(): RadStatus {
|
|
186
|
+
return useRadClient().status;
|
|
187
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for `<RAStackProvider>` — the single place a React app chooses
|
|
3
|
+
* *where the API runs*:
|
|
4
|
+
*
|
|
5
|
+
* - `remote` — talk to the Rust API over HTTP (Lambda in prod, `rad serve`
|
|
6
|
+
* locally). The classic client/server split.
|
|
7
|
+
* - `local` — compile-free local development: the entire API runs in the
|
|
8
|
+
* browser via WebAssembly over an in-memory Iceberg warehouse, seeded from a
|
|
9
|
+
* repo `data/` folder and persisted to IndexedDB. No server, no network,
|
|
10
|
+
* instant reads/writes.
|
|
11
|
+
* - `s3` — full client mode: the same WASM engine, but the warehouse is
|
|
12
|
+
* loaded from and saved back to S3, so a static site reads and writes the
|
|
13
|
+
* object-storage database directly.
|
|
14
|
+
*/
|
|
15
|
+
export type RadMode = "remote" | "local" | "s3";
|
|
16
|
+
|
|
17
|
+
/** The subset of the WASM `RadApi` class the provider uses. */
|
|
18
|
+
export interface RadEngine {
|
|
19
|
+
handle(
|
|
20
|
+
method: string,
|
|
21
|
+
path: string,
|
|
22
|
+
query: Record<string, string> | undefined,
|
|
23
|
+
body: unknown,
|
|
24
|
+
): Promise<{ status: number; body: any }>;
|
|
25
|
+
/** Seed one Iceberg file (metadata or `.parquet` data file) at its key. */
|
|
26
|
+
seedFile(key: string, bytes: Uint8Array): Promise<void>;
|
|
27
|
+
importWarehouse(blob: Uint8Array): Promise<void>;
|
|
28
|
+
exportWarehouse(): Promise<Uint8Array>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Constructor of the generated `RadApi` class. */
|
|
32
|
+
export interface RadEngineCtor {
|
|
33
|
+
new (manifestJson: string, openapiJson?: string): RadEngine;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What a WASM bundle loader must resolve to. */
|
|
37
|
+
export interface RadWasmModule {
|
|
38
|
+
/** wasm-pack `web` target exposes an init fn that fetches the `.wasm`. */
|
|
39
|
+
default?: (input?: any) => Promise<any>;
|
|
40
|
+
init?: (input?: any) => Promise<any>;
|
|
41
|
+
RadApi: RadEngineCtor;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Load and initialise the compiled WASM engine module. */
|
|
45
|
+
export type RadEngineLoader = () => Promise<RadWasmModule>;
|
|
46
|
+
|
|
47
|
+
/** Where to read/write the warehouse blob in `s3` mode (or any custom sink). */
|
|
48
|
+
export interface WarehouseSource {
|
|
49
|
+
/** Fetch the current warehouse blob, or `null` if none exists yet. */
|
|
50
|
+
load(): Promise<Uint8Array | null>;
|
|
51
|
+
/** Persist the warehouse blob (called debounced after writes). */
|
|
52
|
+
save(blob: Uint8Array): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RAStackProviderConfig {
|
|
56
|
+
mode: RadMode;
|
|
57
|
+
|
|
58
|
+
// -- remote mode --
|
|
59
|
+
/** HTTP base URL. Defaults to `NEXT_PUBLIC_BASE_URL` or localhost:8000. */
|
|
60
|
+
baseURL?: string;
|
|
61
|
+
/** Attach a Bearer token to every request. */
|
|
62
|
+
getToken?: () => string | Promise<string>;
|
|
63
|
+
|
|
64
|
+
// -- local / s3 mode --
|
|
65
|
+
/** `schema.rad.json` — object or JSON string. Required for WASM modes. */
|
|
66
|
+
manifest?: object | string;
|
|
67
|
+
/** `openapi.json` — object or JSON string. Optional (`/api/schema/`). */
|
|
68
|
+
openapi?: object | string;
|
|
69
|
+
/**
|
|
70
|
+
* Base URL of the committed Iceberg warehouse (a *directory of files*, not a
|
|
71
|
+
* blob) to seed `local` mode from. The provider walks the tree —
|
|
72
|
+
* `version-hint.text` → snapshot metadata → `.parquet` data files — exactly
|
|
73
|
+
* as the server walks it over object storage. Defaults to `/data/warehouse/`.
|
|
74
|
+
* Ignored once a persisted local copy exists.
|
|
75
|
+
*/
|
|
76
|
+
warehouseBaseUrl?: string;
|
|
77
|
+
/** Persist the local warehouse to IndexedDB between reloads (default true). */
|
|
78
|
+
persist?: boolean;
|
|
79
|
+
/** IndexedDB database name for the local warehouse. */
|
|
80
|
+
databaseName?: string;
|
|
81
|
+
/** `s3` mode: how to load/save the warehouse blob from object storage. */
|
|
82
|
+
warehouse?: WarehouseSource;
|
|
83
|
+
/**
|
|
84
|
+
* Override how the WASM module is loaded. Supply this from app code (where
|
|
85
|
+
* your bundler can resolve the generated bundle) if the default loader can't
|
|
86
|
+
* find it. Defaults to importing `rastack/wasm/rad_wasm.js`.
|
|
87
|
+
*/
|
|
88
|
+
loadEngine?: RadEngineLoader;
|
|
89
|
+
|
|
90
|
+
// -- lifecycle --
|
|
91
|
+
onStatusChange?: (status: RadStatus) => void;
|
|
92
|
+
onError?: (error: unknown) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type RadStatus =
|
|
96
|
+
| "idle"
|
|
97
|
+
| "loading" // fetching/instantiating the WASM engine
|
|
98
|
+
| "seeding" // importing the warehouse
|
|
99
|
+
| "ready"
|
|
100
|
+
| "error";
|
|
101
|
+
|
|
102
|
+
/** Imperative handle exposed via `useRadClient()`. */
|
|
103
|
+
export interface RadClient {
|
|
104
|
+
mode: RadMode;
|
|
105
|
+
status: RadStatus;
|
|
106
|
+
/** The live WASM engine (null in `remote` mode). */
|
|
107
|
+
engine: RadEngine | null;
|
|
108
|
+
/** Snapshot the local warehouse to a portable blob (WASM modes only). */
|
|
109
|
+
exportWarehouse(): Promise<Uint8Array>;
|
|
110
|
+
/** Download that snapshot as a file — a portable backup of local edits. */
|
|
111
|
+
downloadWarehouse(filename?: string): Promise<void>;
|
|
112
|
+
/** Force-persist now (IndexedDB and/or the `s3` sink). */
|
|
113
|
+
persistNow(): Promise<void>;
|
|
114
|
+
}
|