lazypock 0.1.5 → 0.1.6

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 CHANGED
@@ -180,6 +180,12 @@ The main client class.
180
180
  | `authStore` | `AuthStore` | auto-created | Explicit auth store instance |
181
181
  | `realtime` | `RealtimeService` | auto-created | Real-time service for WebSocket subscriptions |
182
182
 
183
+ #### Auto-Cancellation Methods
184
+
185
+ - `autoCancellation(enable)` — Globally enable/disable auto-cancellation of duplicated pending requests
186
+ - `cancelRequest(requestKey)` — Abort a single pending request by key (default `HTTP_METHOD + path`)
187
+ - `cancelAllRequests()` — Abort all pending requests
188
+
183
189
  #### Authentication Methods
184
190
 
185
191
  - `login(email, password, collection?)` — Login as superuser or auth collection user
@@ -291,6 +297,58 @@ interface RequestOptions {
291
297
  }
292
298
  ```
293
299
 
300
+ ## Auto Cancellation
301
+
302
+ The SDK auto-cancels duplicated pending requests for you (PocketBase-compatible
303
+ behaviour). When a new request is issued with the same request key as a
304
+ still-pending request, the previous one is aborted — only the last request
305
+ executes:
306
+
307
+ ```typescript
308
+ // Only the last call will execute; the first two are auto-cancelled
309
+ await client.collection('posts').getList(1, 20); // cancelled
310
+ await client.collection('posts').getList(2, 20); // cancelled
311
+ await client.collection('posts').getList(3, 20); // executed
312
+ ```
313
+
314
+ By default the request key is `HTTP_METHOD + path` (e.g. `"GET /api/posts?page=1"`), so
315
+ duplicate calls with identical URLs cancel each other. Cancelled requests reject
316
+ with an `ApiError` whose `isAbort` is `true`:
317
+
318
+ ```typescript
319
+ try {
320
+ await client.collection('posts').getList(1, 20);
321
+ } catch (err) {
322
+ if (err instanceof ApiError && err.isAbort) {
323
+ // superseded by a newer request — safe to ignore
324
+ }
325
+ }
326
+ ```
327
+
328
+ #### Per-request control
329
+
330
+ Pass `requestKey` in the request options to customize the key, or disable
331
+ auto-cancellation for a specific request:
332
+
333
+ ```typescript
334
+ await client.collection('posts').getList(1, 20, { requestKey: 'my-list' }); // cancelled
335
+ await client.collection('posts').getList(1, 20, { requestKey: 'my-list' }); // executed
336
+
337
+ await client.collection('posts').getList(1, 20, { requestKey: null }); // executed
338
+ await client.collection('posts').getList(1, 20, { requestKey: null }); // executed
339
+ ```
340
+
341
+ #### Global control
342
+
343
+ ```typescript
344
+ // Disable auto-cancellation globally
345
+ client.autoCancellation(false);
346
+
347
+ // Manually cancel pending requests
348
+ client.cancelRequest('GET /api/posts?page=1');
349
+ client.cancelAllRequests();
350
+ ```
351
+
294
352
  ## Error Handling
295
353
 
296
354
  The SDK throws `ApiError` on non-2xx responses:
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  CollectionService: () => CollectionService,
26
26
  CollectionsService: () => CollectionsService,
27
27
  FilesService: () => FilesService,
28
+ HttpClient: () => HttpClient,
28
29
  LazypockClient: () => LazypockClient,
29
30
  RealtimeService: () => RealtimeService,
30
31
  TypedClient: () => TypedClient,
@@ -34,6 +35,8 @@ __export(index_exports, {
34
35
  fieldTypeScriptType: () => fieldTypeScriptType,
35
36
  generateTypes: () => generateTypes,
36
37
  getFileUrl: () => getFileUrl,
38
+ getScaleUrl: () => getScaleUrl,
39
+ getThumbUrl: () => getThumbUrl,
37
40
  schemaFieldType: () => schemaFieldType,
38
41
  wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
39
42
  });
@@ -41,21 +44,33 @@ module.exports = __toCommonJS(index_exports);
41
44
 
42
45
  // src/types.ts
43
46
  var ApiError = class extends Error {
44
- constructor(message, data, status) {
47
+ constructor(message, data, status, isAbort = false) {
45
48
  super(message);
46
49
  this.name = "ApiError";
47
50
  this.data = data;
48
51
  this.status = status;
52
+ this.isAbort = isAbort;
49
53
  }
50
54
  };
51
55
 
52
56
  // src/http.ts
57
+ function isAbortError(err) {
58
+ return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
59
+ }
53
60
  var HttpClient = class {
54
61
  /**
55
62
  * @param baseUrl The API base URL (e.g. `http://localhost:4000/api`). Trailing slash stripped.
56
63
  * @param authStore The auth store providing the token for Authorization headers.
57
64
  */
58
65
  constructor(baseUrl, authStore) {
66
+ /**
67
+ * Abort controllers for in-flight requests, keyed by their cancellation key
68
+ * (default `METHOD path`). A new request with the same key aborts the
69
+ * previous one — PocketBase-style auto-cancellation of duplicated requests.
70
+ */
71
+ this.cancelControllers = {};
72
+ /** Global toggle for the auto-cancellation behaviour (default: on). */
73
+ this.enableAutoCancellation = true;
59
74
  this.baseUrl = baseUrl.replace(/\/+$/, "");
60
75
  this.authStore = authStore;
61
76
  this.defaultFetch = globalThis.fetch.bind(globalThis);
@@ -92,20 +107,73 @@ var HttpClient = class {
92
107
  return null;
93
108
  }
94
109
  }
110
+ /**
111
+ * Globally enable or disable auto-cancellation of duplicated pending requests.
112
+ * Fluent — returns `this` for chaining.
113
+ */
114
+ autoCancellation(enable) {
115
+ this.enableAutoCancellation = !!enable;
116
+ return this;
117
+ }
118
+ /**
119
+ * Abort a pending request identified by its cancellation key
120
+ * (default `METHOD path`, e.g. `"GET /api/posts"`). No-op if not pending.
121
+ */
122
+ cancelRequest(requestKey) {
123
+ const controller = this.cancelControllers[requestKey];
124
+ if (controller) {
125
+ controller.abort();
126
+ delete this.cancelControllers[requestKey];
127
+ }
128
+ return this;
129
+ }
130
+ /** Abort all pending requests. */
131
+ cancelAllRequests() {
132
+ for (const key in this.cancelControllers) {
133
+ this.cancelControllers[key].abort();
134
+ }
135
+ this.cancelControllers = {};
136
+ return this;
137
+ }
95
138
  /**
96
139
  * Make an HTTP request with automatic auth token injection and optional auto-refresh.
97
140
  *
141
+ * Auto-cancellation: a request keyed by `options.requestKey` (default
142
+ * `METHOD path`) aborts any previous pending request with the same key,
143
+ * so only the last duplicate executes. Set `requestKey: null` or
144
+ * `autoCancel: false` to opt out per request.
145
+ *
98
146
  * @param method HTTP method.
99
147
  * @param path URL path (appended to baseUrl).
100
148
  * @param body JSON-serializable body, or FormData for file uploads.
101
149
  * @param options Optional request options.
102
150
  * @returns Parsed JSON response, or null for 204 No Content.
103
- * @throws {ApiError} On non-2xx responses.
151
+ * @throws {ApiError} On non-2xx responses or when the request is aborted
152
+ * (aborted requests throw an `ApiError` with `isAbort === true`).
104
153
  */
105
154
  async request(method, path, body, options) {
106
155
  if (this.authStore.isExpired && this.authStore.collectionName) {
107
156
  await this.refreshAuth();
108
157
  }
158
+ let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
159
+ if (options?.autoCancel === false) requestKey = null;
160
+ let controller = null;
161
+ const externalSignal = options?.signal;
162
+ if (requestKey !== null) {
163
+ if (this.enableAutoCancellation) {
164
+ this.cancelRequest(requestKey);
165
+ }
166
+ controller = new AbortController();
167
+ this.cancelControllers[requestKey] = controller;
168
+ if (externalSignal?.aborted) {
169
+ controller.abort();
170
+ } else if (externalSignal) {
171
+ externalSignal.addEventListener("abort", () => controller?.abort(), {
172
+ once: true
173
+ });
174
+ }
175
+ }
176
+ const signal = controller?.signal ?? externalSignal;
109
177
  let url = this.baseUrl + path;
110
178
  if (options?.params) {
111
179
  const qs = new URLSearchParams(options.params).toString();
@@ -125,7 +193,7 @@ var HttpClient = class {
125
193
  const init = {
126
194
  method,
127
195
  headers,
128
- signal: options?.signal
196
+ signal
129
197
  };
130
198
  if (body != null && method !== "GET" && method !== "DELETE") {
131
199
  if (body instanceof FormData) {
@@ -134,8 +202,25 @@ var HttpClient = class {
134
202
  init.body = JSON.stringify(body);
135
203
  }
136
204
  }
205
+ let res = null;
137
206
  const fetcher = options?.fetch ?? this.defaultFetch;
138
- const res = await fetcher(url, init);
207
+ try {
208
+ res = await fetcher(url, init);
209
+ } catch (err) {
210
+ if (isAbortError(err)) {
211
+ throw new ApiError(
212
+ "The request was aborted (most likely auto-cancelled by a newer request with the same requestKey)",
213
+ {},
214
+ 0,
215
+ true
216
+ );
217
+ }
218
+ throw err;
219
+ } finally {
220
+ if (requestKey !== null && this.cancelControllers[requestKey] === controller) {
221
+ delete this.cancelControllers[requestKey];
222
+ }
223
+ }
139
224
  if (res.status === 204) return null;
140
225
  let bodyText = "";
141
226
  let data = {};
@@ -361,17 +446,19 @@ var CollectionService = class {
361
446
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
362
447
  */
363
448
  getList(page = 1, perPage = 30, options) {
364
- const { ...rest } = options ?? {};
449
+ const { requestKey, autoCancel, cancelKey, ...rest } = options ?? {};
365
450
  const qs = new URLSearchParams(
366
451
  Object.fromEntries(
367
- Object.entries({ page: String(page), perPage: String(perPage), ...rest }).map(
368
- ([k, v]) => [k, String(v)]
369
- )
452
+ Object.entries({
453
+ page: String(page),
454
+ perPage: String(perPage),
455
+ ...rest
456
+ }).map(([k, v]) => [k, String(v)])
370
457
  )
371
458
  ).toString();
372
459
  return this.http.get(
373
460
  "/" + this.encodeId(this.collectionName) + "?" + qs,
374
- options
461
+ { requestKey, autoCancel, cancelKey }
375
462
  );
376
463
  }
377
464
  /**
@@ -388,7 +475,11 @@ var CollectionService = class {
388
475
  const res = await this.getList(
389
476
  page,
390
477
  batch,
391
- rest
478
+ {
479
+ // disable auto-cancellation across pages — each page request is unique
480
+ ...rest,
481
+ requestKey: null
482
+ }
392
483
  );
393
484
  if (!res || !res.items || res.items.length === 0) break;
394
485
  items.push(...res.items);
@@ -794,6 +885,12 @@ var RealtimeService = class {
794
885
  function getFileUrl(baseUrl, fileId) {
795
886
  return baseUrl.replace(/\/+$/, "") + "/files/" + encodeURIComponent(fileId);
796
887
  }
888
+ function getThumbUrl(baseUrl, fileId, size) {
889
+ return baseUrl.replace(/\/+$/, "") + "/files/" + encodeURIComponent(fileId) + "/thumbs/" + encodeURIComponent(size);
890
+ }
891
+ function getScaleUrl(baseUrl, fileId, size) {
892
+ return baseUrl.replace(/\/+$/, "") + "/files/" + encodeURIComponent(fileId) + "/scale/" + encodeURIComponent(size);
893
+ }
797
894
  var FilesService = class {
798
895
  constructor(http) {
799
896
  this.http = http;
@@ -825,6 +922,25 @@ var FilesService = class {
825
922
  );
826
923
  return data;
827
924
  }
925
+ /**
926
+ * List uploaded files (newest first), with optional filters.
927
+ *
928
+ * @param options Filters and pagination.
929
+ */
930
+ async list(options) {
931
+ const params = {};
932
+ if (options?.page !== void 0) params["page"] = String(options.page);
933
+ if (options?.perPage !== void 0) params["perPage"] = String(options.perPage);
934
+ if (options?.collectionName) params["collectionName"] = options.collectionName;
935
+ if (options?.fieldName) params["fieldName"] = options.fieldName;
936
+ if (options?.mime) params["mime"] = options.mime;
937
+ const data = await this.http.request("GET", "/files", void 0, { params });
938
+ return data ?? { items: [], page: 1, perPage: 50, total: 0 };
939
+ }
940
+ /**
941
+ * Fetch file metadata including URL.
942
+ * @param fileId The file ID.
943
+ */
828
944
  /**
829
945
  * Fetch file metadata including URL.
830
946
  * @param fileId The file ID.
@@ -1233,6 +1349,37 @@ var LazypockClient = class {
1233
1349
  const schemas = this.schemaByName ? [...this.schemaByName.values()] : [];
1234
1350
  return generateTypes(schemas, options);
1235
1351
  }
1352
+ // ── Auto-cancellation (PocketBase `autoCancellation` parity) ──
1353
+ /**
1354
+ * Globally enable or disable auto-cancellation of duplicated pending requests.
1355
+ *
1356
+ * When enabled (default), a new request whose `requestKey` (default
1357
+ * `HTTP_METHOD + path`) matches a still-pending request aborts the previous
1358
+ * one — only the last duplicate executes.
1359
+ *
1360
+ * @example
1361
+ * ```ts
1362
+ * client.autoCancellation(false); // keep every request
1363
+ * ```
1364
+ */
1365
+ autoCancellation(enable) {
1366
+ this.http.autoCancellation(enable);
1367
+ return this;
1368
+ }
1369
+ /**
1370
+ * Abort a single pending request by its cancellation key
1371
+ * (default `HTTP_METHOD + path`, e.g. `"GET /api/posts?page=1"`).
1372
+ * The request rejects with an `ApiError` whose `isAbort` is `true`.
1373
+ */
1374
+ cancelRequest(requestKey) {
1375
+ this.http.cancelRequest(requestKey);
1376
+ return this;
1377
+ }
1378
+ /** Abort all pending requests. */
1379
+ cancelAllRequests() {
1380
+ this.http.cancelAllRequests();
1381
+ return this;
1382
+ }
1236
1383
  // ── Auth ──
1237
1384
  /** Check whether any superuser exists (for login vs setup screen routing). */
1238
1385
  async checkSuperuser() {
@@ -1362,6 +1509,7 @@ function createClient(options) {
1362
1509
  CollectionService,
1363
1510
  CollectionsService,
1364
1511
  FilesService,
1512
+ HttpClient,
1365
1513
  LazypockClient,
1366
1514
  RealtimeService,
1367
1515
  TypedClient,
@@ -1371,6 +1519,8 @@ function createClient(options) {
1371
1519
  fieldTypeScriptType,
1372
1520
  generateTypes,
1373
1521
  getFileUrl,
1522
+ getScaleUrl,
1523
+ getThumbUrl,
1374
1524
  schemaFieldType,
1375
1525
  wsUrlFromBaseUrl
1376
1526
  });