lazypock 0.1.5 → 0.2.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/README.md +58 -0
- package/dist/cli.cjs +9 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.global.js +9 -2
- package/dist/cli.global.js.map +1 -1
- package/dist/cli.js +9 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +177 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -8
- package/dist/index.d.ts +120 -8
- package/dist/index.global.js +174 -16
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +174 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +12 -3
- package/src/collection.ts +12 -7
- package/src/files.ts +75 -0
- package/src/http.ts +120 -8
- package/src/index.ts +3 -0
- package/src/lazypock.ts +69 -8
- package/src/types.ts +30 -1
package/src/http.ts
CHANGED
|
@@ -8,11 +8,29 @@ import type { AuthStore } from "./auth";
|
|
|
8
8
|
* Low-level HTTP client wrapping `fetch` with automatic auth token injection.
|
|
9
9
|
* Only relies on `globalThis.fetch` — works in browser, React Native, and Node 18+.
|
|
10
10
|
*/
|
|
11
|
+
/** Detect whether an unknown thrown value is an abort/`AbortError`-style error. */
|
|
12
|
+
function isAbortError(err: unknown): boolean {
|
|
13
|
+
return (
|
|
14
|
+
err instanceof Error &&
|
|
15
|
+
(err.name === "AbortError" || err.message === "Aborted")
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
export class HttpClient {
|
|
12
20
|
private baseUrl: string;
|
|
13
21
|
private authStore: AuthStore;
|
|
14
22
|
private defaultFetch: typeof globalThis.fetch;
|
|
15
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Abort controllers for in-flight requests, keyed by their cancellation key
|
|
26
|
+
* (default `METHOD path`). A new request with the same key aborts the
|
|
27
|
+
* previous one — PocketBase-style auto-cancellation of duplicated requests.
|
|
28
|
+
*/
|
|
29
|
+
private cancelControllers: Record<string, AbortController> = {};
|
|
30
|
+
|
|
31
|
+
/** Global toggle for the auto-cancellation behaviour (default: on). */
|
|
32
|
+
private enableAutoCancellation = true;
|
|
33
|
+
|
|
16
34
|
/**
|
|
17
35
|
* @param baseUrl The API base URL (e.g. `http://localhost:4000/api`). Trailing slash stripped.
|
|
18
36
|
* @param authStore The auth store providing the token for Authorization headers.
|
|
@@ -60,15 +78,52 @@ export class HttpClient {
|
|
|
60
78
|
}
|
|
61
79
|
}
|
|
62
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Globally enable or disable auto-cancellation of duplicated pending requests.
|
|
83
|
+
* Fluent — returns `this` for chaining.
|
|
84
|
+
*/
|
|
85
|
+
autoCancellation(enable: boolean): this {
|
|
86
|
+
this.enableAutoCancellation = !!enable;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Abort a pending request identified by its cancellation key
|
|
92
|
+
* (default `METHOD path`, e.g. `"GET /api/posts"`). No-op if not pending.
|
|
93
|
+
*/
|
|
94
|
+
cancelRequest(requestKey: string): this {
|
|
95
|
+
const controller = this.cancelControllers[requestKey];
|
|
96
|
+
if (controller) {
|
|
97
|
+
controller.abort();
|
|
98
|
+
delete this.cancelControllers[requestKey];
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Abort all pending requests. */
|
|
104
|
+
cancelAllRequests(): this {
|
|
105
|
+
for (const key in this.cancelControllers) {
|
|
106
|
+
this.cancelControllers[key].abort();
|
|
107
|
+
}
|
|
108
|
+
this.cancelControllers = {};
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
63
112
|
/**
|
|
64
113
|
* Make an HTTP request with automatic auth token injection and optional auto-refresh.
|
|
65
114
|
*
|
|
115
|
+
* Auto-cancellation: a request keyed by `options.requestKey` (default
|
|
116
|
+
* `METHOD path`) aborts any previous pending request with the same key,
|
|
117
|
+
* so only the last duplicate executes. Set `requestKey: null` or
|
|
118
|
+
* `autoCancel: false` to opt out per request.
|
|
119
|
+
*
|
|
66
120
|
* @param method HTTP method.
|
|
67
121
|
* @param path URL path (appended to baseUrl).
|
|
68
122
|
* @param body JSON-serializable body, or FormData for file uploads.
|
|
69
123
|
* @param options Optional request options.
|
|
70
124
|
* @returns Parsed JSON response, or null for 204 No Content.
|
|
71
|
-
* @throws {ApiError} On non-2xx responses
|
|
125
|
+
* @throws {ApiError} On non-2xx responses or when the request is aborted
|
|
126
|
+
* (aborted requests throw an `ApiError` with `isAbort === true`).
|
|
72
127
|
*/
|
|
73
128
|
async request<T = unknown>(
|
|
74
129
|
method: Method,
|
|
@@ -81,6 +136,39 @@ export class HttpClient {
|
|
|
81
136
|
await this.refreshAuth();
|
|
82
137
|
}
|
|
83
138
|
|
|
139
|
+
// Resolve the auto-cancellation key (PocketBase `requestKey` semantics):
|
|
140
|
+
// - options.requestKey null → disabled for this request
|
|
141
|
+
// - options.requestKey string → use it verbatim
|
|
142
|
+
// - options.autoCancel false → disabled (legacy compat)
|
|
143
|
+
// - options.cancelKey string → use it verbatim (legacy compat)
|
|
144
|
+
// - otherwise → default to `${method} ${path}`
|
|
145
|
+
let requestKey: string | null =
|
|
146
|
+
options?.requestKey === undefined
|
|
147
|
+
? (options?.cancelKey ?? `${method} ${path}`)
|
|
148
|
+
: options.requestKey;
|
|
149
|
+
if (options?.autoCancel === false) requestKey = null;
|
|
150
|
+
|
|
151
|
+
// Wire a fresh AbortController for this request, merging any caller signal.
|
|
152
|
+
// When auto-cancellation is enabled, the previous pending request sharing
|
|
153
|
+
// our key is aborted first (only the last duplicate executes).
|
|
154
|
+
let controller: AbortController | null = null;
|
|
155
|
+
const externalSignal = options?.signal;
|
|
156
|
+
if (requestKey !== null) {
|
|
157
|
+
if (this.enableAutoCancellation) {
|
|
158
|
+
this.cancelRequest(requestKey);
|
|
159
|
+
}
|
|
160
|
+
controller = new AbortController();
|
|
161
|
+
this.cancelControllers[requestKey] = controller;
|
|
162
|
+
if (externalSignal?.aborted) {
|
|
163
|
+
controller.abort();
|
|
164
|
+
} else if (externalSignal) {
|
|
165
|
+
externalSignal.addEventListener("abort", () => controller?.abort(), {
|
|
166
|
+
once: true,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const signal = controller?.signal ?? externalSignal;
|
|
171
|
+
|
|
84
172
|
let url = this.baseUrl + path;
|
|
85
173
|
if (options?.params) {
|
|
86
174
|
const qs = new URLSearchParams(options.params).toString();
|
|
@@ -104,7 +192,7 @@ export class HttpClient {
|
|
|
104
192
|
const init: RequestInit = {
|
|
105
193
|
method,
|
|
106
194
|
headers,
|
|
107
|
-
signal
|
|
195
|
+
signal,
|
|
108
196
|
};
|
|
109
197
|
|
|
110
198
|
if (body != null && method !== "GET" && method !== "DELETE") {
|
|
@@ -115,10 +203,34 @@ export class HttpClient {
|
|
|
115
203
|
}
|
|
116
204
|
}
|
|
117
205
|
|
|
206
|
+
let res: Response | null = null;
|
|
118
207
|
const fetcher = options?.fetch ?? this.defaultFetch;
|
|
119
|
-
|
|
208
|
+
try {
|
|
209
|
+
res = await fetcher(url, init);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
// Aborted (auto-cancelled duplicate, manual cancel, or external signal)
|
|
212
|
+
// → normalized ApiError with isAbort === true, like PocketBase.
|
|
213
|
+
if (isAbortError(err)) {
|
|
214
|
+
throw new ApiError(
|
|
215
|
+
"The request was aborted (most likely auto-cancelled by a newer request with the same requestKey)",
|
|
216
|
+
{},
|
|
217
|
+
0,
|
|
218
|
+
true,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
throw err;
|
|
222
|
+
} finally {
|
|
223
|
+
// The request has settled — no longer pending, so drop the controller
|
|
224
|
+
// unless a newer request already replaced it (same key).
|
|
225
|
+
if (
|
|
226
|
+
requestKey !== null &&
|
|
227
|
+
this.cancelControllers[requestKey] === controller
|
|
228
|
+
) {
|
|
229
|
+
delete this.cancelControllers[requestKey];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
120
232
|
|
|
121
|
-
if (res
|
|
233
|
+
if (res!.status === 204) return null;
|
|
122
234
|
|
|
123
235
|
// Safely parse JSON — some errored responses may have empty or non-JSON bodies
|
|
124
236
|
let bodyText = "";
|
|
@@ -132,12 +244,12 @@ export class HttpClient {
|
|
|
132
244
|
// Not JSON — keep data as empty object
|
|
133
245
|
}
|
|
134
246
|
|
|
135
|
-
if (!res
|
|
247
|
+
if (!res!.ok) {
|
|
136
248
|
throw new ApiError(
|
|
137
|
-
(typeof data.message === "string" ? data.message : res
|
|
138
|
-
`Request failed with status ${res
|
|
249
|
+
(typeof data.message === "string" ? data.message : res!.statusText) ||
|
|
250
|
+
`Request failed with status ${res!.status}`,
|
|
139
251
|
data,
|
|
140
|
-
res
|
|
252
|
+
res!.status,
|
|
141
253
|
);
|
|
142
254
|
}
|
|
143
255
|
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,13 @@ export {
|
|
|
8
8
|
// LazypockClient + options + everything it re-exports
|
|
9
9
|
AuthStore,
|
|
10
10
|
ApiError,
|
|
11
|
+
HttpClient,
|
|
11
12
|
RealtimeService,
|
|
12
13
|
wsUrlFromBaseUrl,
|
|
13
14
|
FilesService,
|
|
14
15
|
getFileUrl,
|
|
16
|
+
getThumbUrl,
|
|
17
|
+
getScaleUrl,
|
|
15
18
|
LazypockClient,
|
|
16
19
|
CollectionService,
|
|
17
20
|
CollectionsService,
|
package/src/lazypock.ts
CHANGED
|
@@ -21,7 +21,13 @@ import {
|
|
|
21
21
|
type RequestOptions,
|
|
22
22
|
} from "./types";
|
|
23
23
|
import { RealtimeService, wsUrlFromBaseUrl } from "./realtime";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
FilesService,
|
|
26
|
+
getFileUrl,
|
|
27
|
+
getThumbUrl,
|
|
28
|
+
getScaleUrl,
|
|
29
|
+
type FileRecord,
|
|
30
|
+
} from "./files";
|
|
25
31
|
import { CollectionsService } from "./collections";
|
|
26
32
|
import type { CollectionSchema, SchemaField } from "./schema";
|
|
27
33
|
import { generateTypes, collectionTypeName } from "./codegen";
|
|
@@ -30,10 +36,13 @@ import { fieldTypeScriptType, fieldTypeKind, schemaFieldType } from "./typegen";
|
|
|
30
36
|
export {
|
|
31
37
|
AuthStore,
|
|
32
38
|
ApiError,
|
|
39
|
+
HttpClient,
|
|
33
40
|
RealtimeService,
|
|
34
41
|
wsUrlFromBaseUrl,
|
|
35
42
|
FilesService,
|
|
36
43
|
getFileUrl,
|
|
44
|
+
getThumbUrl,
|
|
45
|
+
getScaleUrl,
|
|
37
46
|
CollectionService,
|
|
38
47
|
CollectionsService,
|
|
39
48
|
generateTypes,
|
|
@@ -171,6 +180,41 @@ export class LazypockClient {
|
|
|
171
180
|
return generateTypes(schemas, options);
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
// ── Auto-cancellation (PocketBase `autoCancellation` parity) ──
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Globally enable or disable auto-cancellation of duplicated pending requests.
|
|
187
|
+
*
|
|
188
|
+
* When enabled (default), a new request whose `requestKey` (default
|
|
189
|
+
* `HTTP_METHOD + path`) matches a still-pending request aborts the previous
|
|
190
|
+
* one — only the last duplicate executes.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* client.autoCancellation(false); // keep every request
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
autoCancellation(enable: boolean): this {
|
|
198
|
+
this.http.autoCancellation(enable);
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Abort a single pending request by its cancellation key
|
|
204
|
+
* (default `HTTP_METHOD + path`, e.g. `"GET /api/posts?page=1"`).
|
|
205
|
+
* The request rejects with an `ApiError` whose `isAbort` is `true`.
|
|
206
|
+
*/
|
|
207
|
+
cancelRequest(requestKey: string): this {
|
|
208
|
+
this.http.cancelRequest(requestKey);
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Abort all pending requests. */
|
|
213
|
+
cancelAllRequests(): this {
|
|
214
|
+
this.http.cancelAllRequests();
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
174
218
|
// ── Auth ──
|
|
175
219
|
|
|
176
220
|
/** Check whether any superuser exists (for login vs setup screen routing). */
|
|
@@ -230,11 +274,24 @@ export class LazypockClient {
|
|
|
230
274
|
this.authStore.setCollectionName(collection);
|
|
231
275
|
this.authStore.set(data.token, data.record as unknown as AuthModel);
|
|
232
276
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
277
|
+
} else {
|
|
278
|
+
// Superuser login — PocketBase parity: `_superusers` is an auth collection.
|
|
279
|
+
// Falls back to the legacy /superusers/login for older servers.
|
|
280
|
+
try {
|
|
281
|
+
data = await this.http.post<
|
|
282
|
+
{ token: string; record: ApiRecord } & Record<string, unknown>
|
|
283
|
+
>("/_superusers/auth-with-password", {
|
|
284
|
+
identity: email,
|
|
285
|
+
password,
|
|
286
|
+
});
|
|
287
|
+
} catch {
|
|
288
|
+
data = null;
|
|
289
|
+
}
|
|
290
|
+
if (!data) {
|
|
291
|
+
data = await this.http.post<
|
|
292
|
+
{ token: string } & Record<string, unknown>
|
|
293
|
+
>("/superusers/login", { email, password });
|
|
294
|
+
}
|
|
238
295
|
if (data) {
|
|
239
296
|
this.authStore.setCollectionName(null);
|
|
240
297
|
this.authStore.set(data.token, null);
|
|
@@ -243,9 +300,13 @@ export class LazypockClient {
|
|
|
243
300
|
return data;
|
|
244
301
|
}
|
|
245
302
|
|
|
246
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* Fetch the current authenticated identity (superuser OR auth collection user).
|
|
305
|
+
* Uses `GET /api/me` (PocketBase parity) — works with both superuser tokens
|
|
306
|
+
* and auth collection user tokens.
|
|
307
|
+
*/
|
|
247
308
|
async me<T = ApiRecord>(options?: RequestOptions): Promise<T | null> {
|
|
248
|
-
const data = await this.http.get<T>("/
|
|
309
|
+
const data = await this.http.get<T>("/me", options);
|
|
249
310
|
if (data) {
|
|
250
311
|
// Update the auth model with fresh data
|
|
251
312
|
this.authStore.set(this.authStore.token, data as unknown as AuthModel);
|
package/src/types.ts
CHANGED
|
@@ -65,16 +65,45 @@ export interface RequestOptions {
|
|
|
65
65
|
signal?: AbortSignal;
|
|
66
66
|
/** Custom fetch implementation (for RN or test mocking) */
|
|
67
67
|
fetch?: typeof globalThis.fetch;
|
|
68
|
+
/**
|
|
69
|
+
* Request identifier used by the auto-cancellation mechanism.
|
|
70
|
+
*
|
|
71
|
+
* Pending requests sharing the same key cancel each other — only the
|
|
72
|
+
* last one is executed (PocketBase `requestKey` semantics).
|
|
73
|
+
*
|
|
74
|
+
* - `string` — use this exact key instead of the default `METHOD + path`.
|
|
75
|
+
* - `null` — disable auto-cancellation for this request (never auto-cancelled).
|
|
76
|
+
*
|
|
77
|
+
* @default `${method} ${path}`
|
|
78
|
+
*/
|
|
79
|
+
requestKey?: string | null;
|
|
80
|
+
/**
|
|
81
|
+
* Disable auto-cancellation for this request.
|
|
82
|
+
* Alias of `requestKey: null` (PocketBase `$autoCancel: false` compat).
|
|
83
|
+
*/
|
|
84
|
+
autoCancel?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Custom request key used for auto-cancellation.
|
|
87
|
+
* Alias of `requestKey` (PocketBase `$cancelKey` compat).
|
|
88
|
+
*/
|
|
89
|
+
cancelKey?: string;
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
export class ApiError extends Error {
|
|
71
93
|
readonly data: unknown;
|
|
72
94
|
readonly status: number;
|
|
95
|
+
/**
|
|
96
|
+
* `true` when this error was caused by an aborted/cancelled request
|
|
97
|
+
* (auto-cancelled duplicate, or manually via `cancelRequest()` /
|
|
98
|
+
* `cancelAllRequests()` / an external AbortSignal).
|
|
99
|
+
*/
|
|
100
|
+
readonly isAbort: boolean;
|
|
73
101
|
|
|
74
|
-
constructor(message: string, data: unknown, status: number) {
|
|
102
|
+
constructor(message: string, data: unknown, status: number, isAbort = false) {
|
|
75
103
|
super(message);
|
|
76
104
|
this.name = "ApiError";
|
|
77
105
|
this.data = data;
|
|
78
106
|
this.status = status;
|
|
107
|
+
this.isAbort = isAbort;
|
|
79
108
|
}
|
|
80
109
|
}
|