@brickflow/http 0.0.12 → 0.0.14

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,197 @@
1
+ import { useLazyAsyncData, useNuxtApp, useRuntimeConfig, useState } from "nuxt/app";
2
+ import { onScopeDispose, shallowReactive, shallowRef } from "vue";
3
+ import { dbDeleteKeysWithPart, dbGet, dbSafeSet, getRandom, hashData } from "../utils/index.js";
4
+ import {
5
+ createHttpUrl,
6
+ createNetworkError,
7
+ isHttpErrorPayload
8
+ } from "../utils/shared.js";
9
+ const CHANNEL_NAME = "brickflow-http-tab-sync";
10
+ let channel;
11
+ export async function useHttp(options) {
12
+ const app = useNuxtApp();
13
+ const runtimeConfig = useRuntimeConfig();
14
+ const httpConfig = runtimeConfig.public.brickflowHttp;
15
+ const mapParams = (params) => {
16
+ if (options.mapParams) {
17
+ return options.mapParams(params ?? {});
18
+ }
19
+ return params ?? {};
20
+ };
21
+ const cacheEnabled = httpConfig.cache && !(httpConfig.disableCacheInDev && import.meta.dev);
22
+ const initFullUrl = createHttpUrl(options.url, mapParams(options.initParams));
23
+ let hasDataFromServer = false;
24
+ const result = shallowReactive({
25
+ data: null,
26
+ error: null,
27
+ fetch: async () => await void 0,
28
+ hasFirstData: false,
29
+ hasFreshData: false,
30
+ pending: true,
31
+ pendingCache: true
32
+ });
33
+ const controller = new AbortController();
34
+ const serverData = useState(`http:${initFullUrl}`, () => null);
35
+ if (options.server && import.meta.server) {
36
+ const paramsReactive = shallowRef(mapParams(options.initParams));
37
+ const ssr = await useLazyAsyncData(initFullUrl, async () => {
38
+ return await app.$http.get(options.url, {
39
+ params: paramsReactive.value
40
+ });
41
+ });
42
+ result.fetch = async (params) => {
43
+ paramsReactive.value = mapParams(params);
44
+ await ssr.refresh();
45
+ };
46
+ const payload = ssr.data.value?.data ?? null;
47
+ serverData.value = payload;
48
+ if (payload === null) {
49
+ result.pending = false;
50
+ result.pendingCache = false;
51
+ } else {
52
+ applyPayload(payload, result);
53
+ result.hasFirstData = true;
54
+ result.hasFreshData = true;
55
+ result.pending = false;
56
+ result.pendingCache = false;
57
+ options.effect?.(payload, {
58
+ cached: false,
59
+ params: mapParams(options.initParams)
60
+ });
61
+ if (!isHttpErrorPayload(payload)) {
62
+ hasDataFromServer = true;
63
+ }
64
+ }
65
+ }
66
+ if (import.meta.client) {
67
+ let onMessage = function(event) {
68
+ if (!event.data.fullUrl || !fullUrlHistory[event.data.fullUrl] || event.data.data === void 0) {
69
+ return;
70
+ }
71
+ const payload = event.data.data;
72
+ applyPayload(payload, result);
73
+ result.hasFirstData = true;
74
+ result.hasFreshData = true;
75
+ options.effect?.(payload, {
76
+ cached: false,
77
+ params: event.data.params ?? mapParams()
78
+ });
79
+ };
80
+ const fullUrlHistory = {};
81
+ const clientChannel = getChannel();
82
+ clientChannel?.addEventListener("message", onMessage);
83
+ if (serverData.value !== null) {
84
+ applyPayload(serverData.value, result);
85
+ result.pending = false;
86
+ result.pendingCache = false;
87
+ result.hasFirstData = true;
88
+ result.hasFreshData = true;
89
+ }
90
+ const raceCondition = {};
91
+ const runFetch = async (params, fetchOpt) => {
92
+ const mappedParams = mapParams(params);
93
+ const fullUrl = createHttpUrl(options.url, mappedParams);
94
+ const fetchId = Date.now() + getRandom(0, 300);
95
+ if (raceCondition[fullUrl]) {
96
+ return;
97
+ }
98
+ raceCondition[fullUrl] = fetchId;
99
+ try {
100
+ result.pending = true;
101
+ result.pendingCache = true;
102
+ const cachedFetch = cacheEnabled && httpConfig.cacheDbName && httpConfig.cacheStoreName ? await dbGet(fullUrl, "smart-cache-v2", httpConfig.cacheStoreName) : null;
103
+ if (cachedFetch) {
104
+ applyPayload(cachedFetch.value, result);
105
+ result.hasFirstData = true;
106
+ result.pendingCache = false;
107
+ options.effect?.(cachedFetch.value, {
108
+ cached: true,
109
+ params: mappedParams
110
+ });
111
+ }
112
+ if (controller.signal.aborted || fetchOpt?.signal?.aborted) {
113
+ throw new DOMException("Aborted", "AbortError");
114
+ }
115
+ const signal = fetchOpt?.signal ? [controller.signal, fetchOpt.signal] : controller.signal;
116
+ const response = await app.$http.get(options.url, {
117
+ params: mappedParams,
118
+ signal
119
+ });
120
+ applyPayload(response.data, result);
121
+ result.hasFirstData = true;
122
+ result.hasFreshData = true;
123
+ fullUrlHistory[fullUrl] = true;
124
+ clientChannel?.postMessage({
125
+ data: normalizeBroadcastValue(response.data),
126
+ fullUrl,
127
+ params: normalizeBroadcastValue(mappedParams),
128
+ type: "STATE_UPDATE"
129
+ });
130
+ options.effect?.(response.data, {
131
+ cached: false,
132
+ params: mappedParams
133
+ });
134
+ if (cacheEnabled && response.status === 200) {
135
+ if (cachedFetch !== null) {
136
+ const currentHash = await hashData(response.data);
137
+ if (currentHash !== cachedFetch.hash) {
138
+ await dbDeleteKeysWithPart(options.url, "smart-cache-v2", httpConfig.cacheStoreName);
139
+ }
140
+ }
141
+ await dbSafeSet(
142
+ fullUrl,
143
+ response.data,
144
+ "smart-cache-v2",
145
+ httpConfig.cacheStoreName,
146
+ httpConfig.cacheTtlMs
147
+ );
148
+ }
149
+ } catch (error) {
150
+ if (!isHttpErrorPayload(result.error)) {
151
+ result.error = createNetworkError(error);
152
+ }
153
+ } finally {
154
+ if (raceCondition[fullUrl] === fetchId) {
155
+ delete raceCondition[fullUrl];
156
+ }
157
+ result.pending = false;
158
+ result.pendingCache = false;
159
+ }
160
+ };
161
+ result.fetch = runFetch;
162
+ if (!hasDataFromServer && options.lazy !== true) {
163
+ await runFetch(options.initParams);
164
+ }
165
+ onScopeDispose(() => {
166
+ clientChannel?.removeEventListener("message", onMessage);
167
+ controller.abort(`Http Abort -> onScopeDispose ${options.url}`);
168
+ if (serverData.value !== null) {
169
+ serverData.value = null;
170
+ }
171
+ });
172
+ }
173
+ return result;
174
+ }
175
+ function applyPayload(payload, result) {
176
+ if (isHttpErrorPayload(payload)) {
177
+ result.data = null;
178
+ result.error = payload;
179
+ return;
180
+ }
181
+ result.data = payload;
182
+ result.error = null;
183
+ }
184
+ function getChannel() {
185
+ if (!import.meta.client) {
186
+ return void 0;
187
+ }
188
+ channel ??= new BroadcastChannel(CHANNEL_NAME);
189
+ return channel;
190
+ }
191
+ function normalizeBroadcastValue(payload) {
192
+ try {
193
+ return structuredClone(payload);
194
+ } catch {
195
+ return JSON.parse(JSON.stringify(payload));
196
+ }
197
+ }
@@ -0,0 +1,3 @@
1
+ import { type CreateHttpClientOptions, type HttpClient } from '../utils/shared.js';
2
+ export declare function createHttpClient(options?: CreateHttpClientOptions): HttpClient;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,217 @@
1
+ import { getRetryDelay, isFormData, sleep } from "../utils/helpers.js";
2
+ import { getHttpRequestMiddlewares, getHttpResponseMiddlewares } from "../utils/middleware.js";
3
+ import {
4
+ joinUrl
5
+ } from "../utils/shared.js";
6
+ export function createHttpClient(options = {}) {
7
+ const requestTimeoutMs = options.requestTimeoutMs ?? 8e4;
8
+ const defaultRetry = {
9
+ delay: options.retry?.delay ?? 300,
10
+ retries: options.retry?.retries ?? 3
11
+ };
12
+ return {
13
+ async get(url, config = {}) {
14
+ const retry = {
15
+ delay: config.retry?.delay ?? defaultRetry.delay,
16
+ retries: config.retry?.retries ?? defaultRetry.retries
17
+ };
18
+ const attemptRequest = async (attempt) => {
19
+ const request = await createRequestContext({
20
+ method: "GET",
21
+ options,
22
+ params: config.params,
23
+ requestTimeoutMs,
24
+ signal: config.signal,
25
+ url
26
+ });
27
+ try {
28
+ const result = await fetch(request.url, {
29
+ credentials: request.credentials,
30
+ headers: request.headers,
31
+ method: request.method,
32
+ signal: request.signal
33
+ });
34
+ const response = buildResponse(
35
+ request.url,
36
+ await readResponseBody(result),
37
+ result.status
38
+ );
39
+ if (!result.ok && isRetryableStatus(result.status) && attempt < retry.retries) {
40
+ await sleep(getRetryDelay(attempt, retry.delay));
41
+ return await attemptRequest(attempt + 1);
42
+ }
43
+ await runResponseMiddlewares(response, request, options.responseMiddleware);
44
+ return response;
45
+ } catch (error) {
46
+ if (isAbortError(error) || attempt >= retry.retries) {
47
+ throw error;
48
+ }
49
+ await sleep(getRetryDelay(attempt, retry.delay));
50
+ return await attemptRequest(attempt + 1);
51
+ }
52
+ };
53
+ return await attemptRequest(0);
54
+ },
55
+ async post(url, data, config = {}) {
56
+ const request = await createRequestContext({
57
+ body: data,
58
+ method: "POST",
59
+ options,
60
+ params: config.params,
61
+ requestTimeoutMs,
62
+ signal: config.signal,
63
+ url
64
+ });
65
+ let requestBody;
66
+ if (request.body !== void 0) {
67
+ requestBody = isFormData(request.body) ? request.body : JSON.stringify(request.body);
68
+ }
69
+ const result = await fetch(request.url, {
70
+ body: requestBody,
71
+ credentials: request.credentials,
72
+ headers: request.headers,
73
+ method: request.method,
74
+ signal: request.signal
75
+ });
76
+ const response = buildResponse(
77
+ request.url,
78
+ await readResponseBody(result),
79
+ result.status
80
+ );
81
+ await runResponseMiddlewares(response, request, options.responseMiddleware);
82
+ return response;
83
+ }
84
+ };
85
+ }
86
+ function buildRequestUrl(url, params, method) {
87
+ if (!params || Object.keys(params).length === 0) {
88
+ return url;
89
+ }
90
+ const searchParams = new URLSearchParams();
91
+ Object.entries(params ?? {}).forEach(([key, value]) => {
92
+ if (value === void 0 || value === null) {
93
+ return;
94
+ }
95
+ if (Array.isArray(value)) {
96
+ if (method === "POST") {
97
+ value.forEach((item) => {
98
+ searchParams.append(`${key}[]`, String(item));
99
+ });
100
+ return;
101
+ }
102
+ searchParams.append(key, JSON.stringify(value));
103
+ return;
104
+ }
105
+ searchParams.append(key, String(value));
106
+ });
107
+ const query = searchParams.toString();
108
+ if (!query) {
109
+ return url;
110
+ }
111
+ const separator = url.includes("?") ? "&" : "?";
112
+ return `${url}${separator}${query}`;
113
+ }
114
+ function buildResponse(url, data, status) {
115
+ return {
116
+ config: {
117
+ url
118
+ },
119
+ data,
120
+ status
121
+ };
122
+ }
123
+ function createAnySignal(signals) {
124
+ if (typeof AbortSignal.any === "function") {
125
+ return AbortSignal.any(signals);
126
+ }
127
+ const controller = new AbortController();
128
+ signals.filter(Boolean).forEach((signal) => {
129
+ signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
130
+ });
131
+ return controller.signal;
132
+ }
133
+ async function createRequestContext(input) {
134
+ const signal = resolveSignal(input.signal, input.requestTimeoutMs);
135
+ const headers = await createRequestHeaders(
136
+ input.options.createHeaders,
137
+ input.method === "POST" && input.body !== void 0 && !isFormData(input.body) ? {
138
+ "Content-Type": "application/json"
139
+ } : void 0
140
+ );
141
+ const request = {
142
+ baseURL: await resolveBaseURL(input.options.baseURL),
143
+ body: input.body,
144
+ credentials: "include",
145
+ headers,
146
+ method: input.method,
147
+ params: input.params,
148
+ signal,
149
+ url: input.url
150
+ };
151
+ await runRequestMiddlewares(request, input.options.requestMiddleware);
152
+ request.url = buildRequestUrl(joinUrl(request.baseURL, request.url), request.params, request.method);
153
+ return request;
154
+ }
155
+ async function createRequestHeaders(createHeaders, extraHeaders) {
156
+ const headers = new Headers(createHeaders ? await createHeaders() : void 0);
157
+ const extra = new Headers(extraHeaders);
158
+ extra.forEach((value, key) => {
159
+ headers.set(key, value);
160
+ });
161
+ return headers;
162
+ }
163
+ function createTimeoutSignal(ms) {
164
+ if (typeof AbortSignal.timeout === "function") {
165
+ return AbortSignal.timeout(ms);
166
+ }
167
+ const controller = new AbortController();
168
+ const timeoutId = setTimeout(() => controller.abort(new DOMException("Timed out", "AbortError")), ms);
169
+ controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), { once: true });
170
+ return controller.signal;
171
+ }
172
+ function isAbortError(error) {
173
+ return error instanceof DOMException && error.name === "AbortError";
174
+ }
175
+ function isRetryableStatus(status) {
176
+ return status >= 500 || status === 429;
177
+ }
178
+ async function readResponseBody(response) {
179
+ const raw = await response.text();
180
+ if (!raw) {
181
+ return null;
182
+ }
183
+ try {
184
+ return JSON.parse(raw);
185
+ } catch {
186
+ return raw;
187
+ }
188
+ }
189
+ async function resolveBaseURL(baseURL) {
190
+ if (!baseURL) {
191
+ return "";
192
+ }
193
+ if (typeof baseURL === "function") {
194
+ return await baseURL() || "";
195
+ }
196
+ return baseURL;
197
+ }
198
+ function resolveSignal(signal, requestTimeoutMs) {
199
+ const timeoutSignal = createTimeoutSignal(requestTimeoutMs);
200
+ if (Array.isArray(signal)) {
201
+ return createAnySignal([...signal, timeoutSignal]);
202
+ }
203
+ if (signal) {
204
+ return createAnySignal([signal, timeoutSignal]);
205
+ }
206
+ return timeoutSignal;
207
+ }
208
+ async function runRequestMiddlewares(request, middlewares) {
209
+ for (const middleware of [...getHttpRequestMiddlewares(), ...middlewares ?? []]) {
210
+ await middleware(request);
211
+ }
212
+ }
213
+ async function runResponseMiddlewares(response, request, middlewares) {
214
+ for (const middleware of [...getHttpResponseMiddlewares(), ...middlewares ?? []]) {
215
+ await middleware(response, request);
216
+ }
217
+ }
@@ -0,0 +1,7 @@
1
+ declare const _default: import("nuxt/app").Plugin<{
2
+ http: import("./utils/index.js").HttpClient;
3
+ }> & import("nuxt/app").ObjectPlugin<{
4
+ http: import("./utils/index.js").HttpClient;
5
+ }>;
6
+ export default _default;
7
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "nuxt/app";
2
+ import { createHttpClient } from "./http/client.js";
3
+ import { isHttpErrorPayload } from "./utils/shared.js";
4
+ export default defineNuxtPlugin(() => {
5
+ const runtimeConfig = useRuntimeConfig();
6
+ const httpConfig = runtimeConfig.public.brickflowHttp;
7
+ const http = createHttpClient({
8
+ baseURL: () => httpConfig.baseURL,
9
+ createHeaders: () => {
10
+ const headers = new Headers(httpConfig.defaultHeaders);
11
+ headers.set("X-Requested-With", "XMLHttpRequest");
12
+ if (httpConfig.clientEnvHeader && import.meta.dev) {
13
+ headers.set("Client-Env", "development");
14
+ }
15
+ return headers;
16
+ },
17
+ requestTimeoutMs: httpConfig.requestTimeoutMs,
18
+ responseMiddleware: [createRuntimeHttpErrorMiddleware()],
19
+ retry: httpConfig.retry
20
+ });
21
+ return {
22
+ provide: {
23
+ http
24
+ }
25
+ };
26
+ });
27
+ function createRuntimeHttpErrorMiddleware() {
28
+ return (response) => {
29
+ const { data, status } = response;
30
+ if (status !== 451 && status < 500 || !isHttpErrorPayload(data)) {
31
+ return;
32
+ }
33
+ const raise = async () => {
34
+ const { showError } = await import("nuxt/app");
35
+ showError({
36
+ data,
37
+ statusCode: status
38
+ });
39
+ };
40
+ const raiseSafely = () => {
41
+ ;
42
+ (async () => {
43
+ try {
44
+ await raise();
45
+ } catch (error) {
46
+ console.error(error);
47
+ }
48
+ })();
49
+ };
50
+ if (import.meta.server) {
51
+ raiseSafely();
52
+ return;
53
+ }
54
+ setTimeout(raiseSafely, 0);
55
+ };
56
+ }
@@ -0,0 +1,21 @@
1
+ import type { HttpRouteMap, TypedHttpClient } from './utils/typed'
2
+
3
+ declare module '#app' {
4
+ interface NuxtApp {
5
+ $http: TypedHttpClient<HttpRouteMap>
6
+ }
7
+ }
8
+
9
+ declare module 'nuxt/app' {
10
+ interface NuxtApp {
11
+ $http: TypedHttpClient<HttpRouteMap>
12
+ }
13
+ }
14
+
15
+ declare module 'vue' {
16
+ interface ComponentCustomProperties {
17
+ $http: TypedHttpClient<HttpRouteMap>
18
+ }
19
+ }
20
+
21
+ export {}
@@ -0,0 +1,5 @@
1
+ export declare function getRandom(min: number, max: number): number;
2
+ export declare function getRetryDelay(attempt: number, baseDelay: number): number;
3
+ export declare function isFormData(value?: FormData | Record<string, unknown> | unknown): value is FormData;
4
+ export declare const sleep: (ms: number) => Promise<void>;
5
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1,14 @@
1
+ export function getRandom(min, max) {
2
+ const hasFloat = !Number.isInteger(min) || !Number.isInteger(max);
3
+ return hasFloat ? Math.random() * (max - min) + min : Math.floor(Math.random() * (max - min + 1)) + min;
4
+ }
5
+ export function getRetryDelay(attempt, baseDelay) {
6
+ return Math.random() * baseDelay * 2 ** attempt;
7
+ }
8
+ export function isFormData(value) {
9
+ if (!value) {
10
+ return false;
11
+ }
12
+ return typeof value === "object" && typeof value.append === "function" && typeof value.has === "function";
13
+ }
14
+ export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,6 @@
1
+ export * from './helpers.js';
2
+ export * from './indexeddb.js';
3
+ export * from './middleware.js';
4
+ export * from './shared.js';
5
+ export * from './typed.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from "./helpers.js";
2
+ export * from "./indexeddb.js";
3
+ export * from "./middleware.js";
4
+ export * from "./shared.js";
5
+ export * from "./typed.js";
@@ -0,0 +1,14 @@
1
+ export interface DbEntry<T> {
2
+ expiresAt: number;
3
+ hash: string;
4
+ key: string;
5
+ value: T;
6
+ }
7
+ declare const allowDb: readonly ["smart-cache-v2"];
8
+ type AllowDB = (typeof allowDb)[number];
9
+ export declare function dbDeleteKeysWithPart(part: string, dbName: AllowDB, storeName: string): Promise<void>;
10
+ export declare function dbGet<T>(key: string, dbName: AllowDB, storeName: string): Promise<DbEntry<T> | null>;
11
+ export declare function dbSafeSet<T>(key: string, value: T, dbName: AllowDB, storeName: string, ttl: number, retries?: number): Promise<void>;
12
+ export declare function hashData(data: unknown): Promise<string>;
13
+ export {};
14
+ //# sourceMappingURL=indexeddb.d.ts.map