universal-api-layer 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/SECURITY.md +9 -0
- package/dist/angular/index.d.ts +62 -0
- package/dist/angular/index.js +142 -0
- package/dist/chunk-DXYWZQW5.js +92 -0
- package/dist/chunk-HDW7BOZI.js +764 -0
- package/dist/chunk-YTM3FV65.js +19 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +205 -0
- package/dist/client-ByBlZaRg.d.ts +169 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +26 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.js +227 -0
- package/dist/types-CVzD7l4p.d.ts +81 -0
- package/dist/vue/index.d.ts +23 -0
- package/dist/vue/index.js +257 -0
- package/dist/websocket/angular.d.ts +9 -0
- package/dist/websocket/angular.js +15 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.js +529 -0
- package/dist/websocket/react.d.ts +8 -0
- package/dist/websocket/react.js +23 -0
- package/dist/websocket/vue.d.ts +9 -0
- package/dist/websocket/vue.js +39 -0
- package/package.json +181 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { f as ApiError, e as ApiClient, h as ResponseSchema } from './client-ByBlZaRg.js';
|
|
2
|
+
|
|
3
|
+
type SocketData = string | ArrayBuffer | Blob;
|
|
4
|
+
type SocketSendData = string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>;
|
|
5
|
+
interface SocketEvents {
|
|
6
|
+
open: {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
};
|
|
9
|
+
error: {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
};
|
|
12
|
+
message: {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
readonly data: unknown;
|
|
15
|
+
};
|
|
16
|
+
close: {
|
|
17
|
+
readonly type: string;
|
|
18
|
+
readonly code: number;
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
readonly wasClean: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Structural native WebSocket interface, also injectable in tests/non-browser runtimes. */
|
|
24
|
+
interface SocketTransport {
|
|
25
|
+
readonly readyState: number;
|
|
26
|
+
readonly bufferedAmount: number;
|
|
27
|
+
binaryType: "blob" | "arraybuffer";
|
|
28
|
+
send(data: SocketSendData): void;
|
|
29
|
+
close(code?: number, reason?: string): void;
|
|
30
|
+
addEventListener<K extends keyof SocketEvents>(type: K, listener: (event: SocketEvents[K]) => void): void;
|
|
31
|
+
removeEventListener<K extends keyof SocketEvents>(type: K, listener: (event: SocketEvents[K]) => void): void;
|
|
32
|
+
}
|
|
33
|
+
interface SocketSnapshot {
|
|
34
|
+
readonly status: "idle" | "connecting" | "open" | "reconnecting" | "closed" | "disposed";
|
|
35
|
+
/** Retries used since the latest explicit connect(), including across short-lived opens. */
|
|
36
|
+
readonly reconnectAttempt: number;
|
|
37
|
+
readonly error: ApiError | null;
|
|
38
|
+
readonly close: Readonly<{
|
|
39
|
+
code: number;
|
|
40
|
+
reason: string;
|
|
41
|
+
wasClean: boolean;
|
|
42
|
+
}> | null;
|
|
43
|
+
}
|
|
44
|
+
interface SocketOptions<T> {
|
|
45
|
+
/** Absolute ws(s) URL, or an abortable provider for short-lived connection tickets. */
|
|
46
|
+
url: string | ((signal: AbortSignal) => string | Promise<string>);
|
|
47
|
+
/** Required for a URL provider. Exact ws(s) origins, not URL prefixes. */
|
|
48
|
+
allowedOrigins?: readonly string[];
|
|
49
|
+
protocols?: readonly string[];
|
|
50
|
+
sessionClient?: Pick<ApiClient, "getSessionSnapshot" | "subscribeSession">;
|
|
51
|
+
/** Defaults to true when sessionClient is provided. */
|
|
52
|
+
requiresAuth?: boolean;
|
|
53
|
+
/** json (default) parses text JSON; raw preserves native string/ArrayBuffer/Blob data. */
|
|
54
|
+
format?: "json" | "raw";
|
|
55
|
+
schema?: ResponseSchema<T>;
|
|
56
|
+
reconnect?: false | {
|
|
57
|
+
maxAttempts?: number;
|
|
58
|
+
baseDelayMs?: number;
|
|
59
|
+
maxDelayMs?: number;
|
|
60
|
+
jitter?: boolean;
|
|
61
|
+
};
|
|
62
|
+
connectTimeoutMs?: number;
|
|
63
|
+
maxMessageSize?: number;
|
|
64
|
+
maxPendingMessages?: number;
|
|
65
|
+
maxBufferedAmount?: number;
|
|
66
|
+
webSocketFactory?: (url: string, protocols: string[]) => SocketTransport;
|
|
67
|
+
}
|
|
68
|
+
interface SocketClient<T = unknown> {
|
|
69
|
+
connect(): void;
|
|
70
|
+
disconnect(code?: number, reason?: string): void;
|
|
71
|
+
dispose(): void;
|
|
72
|
+
/** Sends only while OPEN and under the buffer limit; never queues or replays. */
|
|
73
|
+
send(data: SocketSendData): void;
|
|
74
|
+
sendJson(value: unknown): void;
|
|
75
|
+
getSnapshot(): SocketSnapshot;
|
|
76
|
+
subscribe(listener: () => void): () => void;
|
|
77
|
+
onMessage(listener: (message: T) => void): () => void;
|
|
78
|
+
onError(listener: (error: ApiError) => void): () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type { SocketClient as S, SocketSnapshot as a, SocketOptions as b, SocketData as c, SocketEvents as d, SocketSendData as e, SocketTransport as f };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as vue from 'vue';
|
|
2
|
+
import { MaybeRefOrGetter, Plugin } from 'vue';
|
|
3
|
+
import { QueryConfig, ApiClient, MutationConfig, ApiError, SessionSnapshot } from '../core/index.js';
|
|
4
|
+
import { QueryClient, UseMutationReturnType, UseQueryReturnType } from '@tanstack/vue-query';
|
|
5
|
+
|
|
6
|
+
declare function createApiPlugin({ client, queryClient, }: {
|
|
7
|
+
client: ApiClient;
|
|
8
|
+
queryClient?: QueryClient;
|
|
9
|
+
}): Plugin;
|
|
10
|
+
declare function useApiClient(): ApiClient;
|
|
11
|
+
declare function useApiSession(): vue.ComputedRef<SessionSnapshot>;
|
|
12
|
+
type VueQueryConfig<T, S = T | null> = Omit<QueryConfig<T, S>, "key" | "url" | "params" | "enabled" | "cacheScope"> & {
|
|
13
|
+
key: MaybeRefOrGetter<readonly unknown[]>;
|
|
14
|
+
url: MaybeRefOrGetter<string>;
|
|
15
|
+
params?: MaybeRefOrGetter<QueryConfig<T, S>["params"]>;
|
|
16
|
+
enabled?: MaybeRefOrGetter<boolean>;
|
|
17
|
+
cacheScope?: MaybeRefOrGetter<string>;
|
|
18
|
+
};
|
|
19
|
+
declare function useApiQuery<T = unknown, S = T | null>(config: VueQueryConfig<T, S>): UseQueryReturnType<S, ApiError>;
|
|
20
|
+
declare function useApiMutation<T = unknown, V = unknown, S = T | null>(config: MutationConfig<T, V, S>): UseMutationReturnType<S, ApiError, V, unknown>;
|
|
21
|
+
|
|
22
|
+
export { createApiPlugin, useApiClient, useApiMutation, useApiQuery, useApiSession };
|
|
23
|
+
export type { VueQueryConfig };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultQueryRetry,
|
|
3
|
+
queryIdentity,
|
|
4
|
+
runMutation
|
|
5
|
+
} from "../chunk-HDW7BOZI.js";
|
|
6
|
+
import {
|
|
7
|
+
invokeAsyncCallback,
|
|
8
|
+
invokeCallback
|
|
9
|
+
} from "../chunk-DXYWZQW5.js";
|
|
10
|
+
import "../chunk-YTM3FV65.js";
|
|
11
|
+
|
|
12
|
+
// src/vue/index.ts
|
|
13
|
+
import {
|
|
14
|
+
computed,
|
|
15
|
+
inject,
|
|
16
|
+
onScopeDispose,
|
|
17
|
+
shallowRef,
|
|
18
|
+
toValue
|
|
19
|
+
} from "vue";
|
|
20
|
+
import {
|
|
21
|
+
QueryClient,
|
|
22
|
+
VueQueryPlugin,
|
|
23
|
+
useQuery,
|
|
24
|
+
useMutation,
|
|
25
|
+
useQueryClient
|
|
26
|
+
} from "@tanstack/vue-query";
|
|
27
|
+
var clientKey = /* @__PURE__ */ Symbol("universal-api-layer");
|
|
28
|
+
function clearClientCache(client, cache, staleOnly = false) {
|
|
29
|
+
const current = client.getSessionSnapshot();
|
|
30
|
+
const predicate = (query) => {
|
|
31
|
+
if (!client.ownsQuery(query.queryKey)) return false;
|
|
32
|
+
if (!staleOnly) return true;
|
|
33
|
+
const snapshot = query.queryKey.at(-1).session;
|
|
34
|
+
return !snapshot || snapshot.revision !== current.revision || snapshot.userId !== current.userId || snapshot.tenantId !== current.tenantId;
|
|
35
|
+
};
|
|
36
|
+
void cache.cancelQueries({ predicate }).catch(() => {
|
|
37
|
+
});
|
|
38
|
+
cache.removeQueries({ predicate });
|
|
39
|
+
for (const mutation of cache.getMutationCache().getAll()) {
|
|
40
|
+
const variables = mutation.state.variables;
|
|
41
|
+
const revision = variables && typeof variables === "object" && "revision" in variables ? variables.revision : void 0;
|
|
42
|
+
if (mutation.options.meta?.universalApiLayer === client.cacheScope && (!staleOnly || revision !== current.revision))
|
|
43
|
+
cache.getMutationCache().remove(mutation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var cacheBindings = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
function bindCache(client, cache) {
|
|
48
|
+
let caches = cacheBindings.get(client);
|
|
49
|
+
if (!caches) {
|
|
50
|
+
caches = /* @__PURE__ */ new WeakMap();
|
|
51
|
+
cacheBindings.set(client, caches);
|
|
52
|
+
}
|
|
53
|
+
let binding = caches.get(cache);
|
|
54
|
+
if (!binding) {
|
|
55
|
+
clearClientCache(client, cache, true);
|
|
56
|
+
binding = {
|
|
57
|
+
count: 0,
|
|
58
|
+
unsubscribe: client.subscribeSession(
|
|
59
|
+
() => clearClientCache(client, cache)
|
|
60
|
+
)
|
|
61
|
+
};
|
|
62
|
+
caches.set(cache, binding);
|
|
63
|
+
}
|
|
64
|
+
binding.count++;
|
|
65
|
+
const current = binding;
|
|
66
|
+
return () => {
|
|
67
|
+
current.count--;
|
|
68
|
+
if (current.count === 0) {
|
|
69
|
+
current.unsubscribe();
|
|
70
|
+
clearClientCache(client, cache);
|
|
71
|
+
caches.delete(cache);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function createApiPlugin({
|
|
76
|
+
client,
|
|
77
|
+
queryClient
|
|
78
|
+
}) {
|
|
79
|
+
return {
|
|
80
|
+
install(app) {
|
|
81
|
+
const cache = queryClient ?? new QueryClient();
|
|
82
|
+
app.provide(clientKey, client);
|
|
83
|
+
app.use(VueQueryPlugin, { queryClient: cache });
|
|
84
|
+
const unsubscribe = bindCache(client, cache);
|
|
85
|
+
const unmount = app.unmount.bind(app);
|
|
86
|
+
app.unmount = () => {
|
|
87
|
+
unsubscribe();
|
|
88
|
+
unmount();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function useApiClient() {
|
|
94
|
+
const client = inject(clientKey);
|
|
95
|
+
if (!client)
|
|
96
|
+
throw new Error("Install createApiPlugin before using API composables");
|
|
97
|
+
return client;
|
|
98
|
+
}
|
|
99
|
+
function useSession(client) {
|
|
100
|
+
const snapshot = shallowRef(client.getSessionSnapshot());
|
|
101
|
+
const unsubscribe = client.subscribeSession(() => {
|
|
102
|
+
snapshot.value = client.getSessionSnapshot();
|
|
103
|
+
});
|
|
104
|
+
onScopeDispose(unsubscribe);
|
|
105
|
+
return computed(() => snapshot.value);
|
|
106
|
+
}
|
|
107
|
+
function useApiSession() {
|
|
108
|
+
return useSession(useApiClient());
|
|
109
|
+
}
|
|
110
|
+
function useApiQuery(config) {
|
|
111
|
+
const client = useApiClient();
|
|
112
|
+
const session = useSession(client);
|
|
113
|
+
const cache = useQueryClient();
|
|
114
|
+
return useQuery(
|
|
115
|
+
computed(() => {
|
|
116
|
+
const url = toValue(config.url), params = toValue(config.params);
|
|
117
|
+
const snapshot = session.value;
|
|
118
|
+
return {
|
|
119
|
+
queryKey: queryIdentity(
|
|
120
|
+
client,
|
|
121
|
+
toValue(config.key),
|
|
122
|
+
url,
|
|
123
|
+
params,
|
|
124
|
+
config.headers,
|
|
125
|
+
toValue(config.cacheScope)
|
|
126
|
+
),
|
|
127
|
+
queryFn: ({ signal }) => {
|
|
128
|
+
client.assertSession(snapshot.revision);
|
|
129
|
+
return client.request(url, {
|
|
130
|
+
params,
|
|
131
|
+
headers: config.headers,
|
|
132
|
+
timeoutMs: config.timeoutMs,
|
|
133
|
+
signal,
|
|
134
|
+
schema: config.schema,
|
|
135
|
+
authRetry: config.authRetry,
|
|
136
|
+
requiresAuth: config.requiresAuth
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
select: config.select ? (data) => invokeCallback(() => config.select(data)) : void 0,
|
|
140
|
+
enabled: config.requiresAuth ? !!snapshot.userId && toValue(config.enabled) !== false : toValue(config.enabled),
|
|
141
|
+
staleTime: config.staleTime,
|
|
142
|
+
retry: config.retry ?? cache.getDefaultOptions().queries?.retry ?? defaultQueryRetry
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function useApiMutation(config) {
|
|
148
|
+
const client = useApiClient(), cache = useQueryClient(), session = useSession(client);
|
|
149
|
+
const mutation = useMutation(
|
|
150
|
+
computed(() => ({
|
|
151
|
+
mutationKey: [
|
|
152
|
+
"universal-api-layer",
|
|
153
|
+
client.cacheScope,
|
|
154
|
+
session.value.revision
|
|
155
|
+
],
|
|
156
|
+
meta: { universalApiLayer: client.cacheScope },
|
|
157
|
+
mutationFn: async (envelope) => {
|
|
158
|
+
client.assertSession(envelope.revision);
|
|
159
|
+
const data = await runMutation(client, config, envelope.input);
|
|
160
|
+
client.assertSession(envelope.revision);
|
|
161
|
+
return data;
|
|
162
|
+
},
|
|
163
|
+
onSuccess: async (data, envelope) => {
|
|
164
|
+
client.assertSession(envelope.revision);
|
|
165
|
+
await Promise.all(
|
|
166
|
+
(config.invalidateKeys ?? []).map(
|
|
167
|
+
(queryKey) => cache.invalidateQueries({
|
|
168
|
+
queryKey,
|
|
169
|
+
predicate: config.invalidateScope === "all" ? void 0 : (query) => client.ownsQuery(query.queryKey)
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
client.assertSession(envelope.revision);
|
|
174
|
+
await invokeAsyncCallback(
|
|
175
|
+
() => config.onSuccess?.(data, envelope.input)
|
|
176
|
+
);
|
|
177
|
+
client.assertSession(envelope.revision);
|
|
178
|
+
}
|
|
179
|
+
}))
|
|
180
|
+
);
|
|
181
|
+
const translate = (options) => options ? {
|
|
182
|
+
onSuccess: (data, envelope, context, mutationContext) => {
|
|
183
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
184
|
+
options.onSuccess?.(
|
|
185
|
+
data,
|
|
186
|
+
envelope.input,
|
|
187
|
+
context,
|
|
188
|
+
mutationContext
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
onError: (error, envelope, context, mutationContext) => {
|
|
192
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
193
|
+
options.onError?.(
|
|
194
|
+
error,
|
|
195
|
+
envelope.input,
|
|
196
|
+
context,
|
|
197
|
+
mutationContext
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
onSettled: (data, error, envelope, context, mutationContext) => {
|
|
201
|
+
if (envelope.revision === client.getSessionSnapshot().revision)
|
|
202
|
+
options.onSettled?.(
|
|
203
|
+
data,
|
|
204
|
+
error,
|
|
205
|
+
envelope.input,
|
|
206
|
+
context,
|
|
207
|
+
mutationContext
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
} : void 0;
|
|
211
|
+
const current = computed(
|
|
212
|
+
() => !mutation.variables.value || mutation.variables.value.revision === session.value.revision
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
...mutation,
|
|
216
|
+
data: computed(() => current.value ? mutation.data.value : void 0),
|
|
217
|
+
error: computed(() => current.value ? mutation.error.value : null),
|
|
218
|
+
variables: computed(
|
|
219
|
+
() => current.value ? mutation.variables.value?.input : void 0
|
|
220
|
+
),
|
|
221
|
+
context: computed(
|
|
222
|
+
() => current.value ? mutation.context.value : void 0
|
|
223
|
+
),
|
|
224
|
+
status: computed(() => current.value ? mutation.status.value : "idle"),
|
|
225
|
+
isPaused: computed(() => current.value && mutation.isPaused.value),
|
|
226
|
+
isIdle: computed(() => !current.value || mutation.isIdle.value),
|
|
227
|
+
isPending: computed(() => current.value && mutation.isPending.value),
|
|
228
|
+
isSuccess: computed(() => current.value && mutation.isSuccess.value),
|
|
229
|
+
isError: computed(() => current.value && mutation.isError.value),
|
|
230
|
+
submittedAt: computed(
|
|
231
|
+
() => current.value ? mutation.submittedAt.value : 0
|
|
232
|
+
),
|
|
233
|
+
failureCount: computed(
|
|
234
|
+
() => current.value ? mutation.failureCount.value : 0
|
|
235
|
+
),
|
|
236
|
+
failureReason: computed(
|
|
237
|
+
() => current.value ? mutation.failureReason.value : null
|
|
238
|
+
),
|
|
239
|
+
mutate: (input, options) => mutation.mutate(
|
|
240
|
+
{ input, revision: client.getSessionSnapshot().revision },
|
|
241
|
+
translate(options)
|
|
242
|
+
),
|
|
243
|
+
mutateAsync: (input, options) => client.withSession(
|
|
244
|
+
() => mutation.mutateAsync(
|
|
245
|
+
{ input, revision: client.getSessionSnapshot().revision },
|
|
246
|
+
translate(options)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
export {
|
|
252
|
+
createApiPlugin,
|
|
253
|
+
useApiClient,
|
|
254
|
+
useApiMutation,
|
|
255
|
+
useApiQuery,
|
|
256
|
+
useApiSession
|
|
257
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Signal } from '@angular/core';
|
|
2
|
+
import { SocketClient, SocketSnapshot } from './index.js';
|
|
3
|
+
import '../client-ByBlZaRg.js';
|
|
4
|
+
|
|
5
|
+
/** Subscription only: the application owns connect/disconnect/dispose. */
|
|
6
|
+
declare function injectSocketState<T>(client: SocketClient<T>): Signal<SocketSnapshot>;
|
|
7
|
+
declare function injectSocketMessage<T>(client: SocketClient<T>, listener: (message: T) => void): void;
|
|
8
|
+
|
|
9
|
+
export { injectSocketMessage, injectSocketState };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/websocket/angular.ts
|
|
2
|
+
import { DestroyRef, inject, signal } from "@angular/core";
|
|
3
|
+
function injectSocketState(client) {
|
|
4
|
+
const destroy = inject(DestroyRef);
|
|
5
|
+
const state = signal(client.getSnapshot());
|
|
6
|
+
destroy.onDestroy(client.subscribe(() => state.set(client.getSnapshot())));
|
|
7
|
+
return state.asReadonly();
|
|
8
|
+
}
|
|
9
|
+
function injectSocketMessage(client, listener) {
|
|
10
|
+
inject(DestroyRef).onDestroy(client.onMessage(listener));
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
injectSocketMessage,
|
|
14
|
+
injectSocketState
|
|
15
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { b as SocketOptions, S as SocketClient } from '../types-CVzD7l4p.js';
|
|
2
|
+
export { c as SocketData, d as SocketEvents, e as SocketSendData, a as SocketSnapshot, f as SocketTransport } from '../types-CVzD7l4p.js';
|
|
3
|
+
import '../client-ByBlZaRg.js';
|
|
4
|
+
|
|
5
|
+
declare function createSocketClient<T = unknown>(config: SocketOptions<T>): SocketClient<T>;
|
|
6
|
+
|
|
7
|
+
export { SocketClient, SocketOptions, createSocketClient };
|