@atscript/db-client 0.1.152 → 0.1.154
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/dist/index.cjs +253 -5
- package/dist/index.d.cts +37 -2
- package/dist/index.d.mts +37 -2
- package/dist/index.mjs +252 -6
- package/dist/{validator-CPmmXTAt.d.mts → validator-DRW4WDEX.d.mts} +101 -1
- package/dist/{validator-CVM7eNP7.d.cts → validator-u-SCH8c3.d.cts} +101 -1
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -145,6 +145,131 @@ var ActionUnsupportedError = class extends Error {
|
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
//#endregion
|
|
148
|
+
//#region src/meta-store.ts
|
|
149
|
+
/** ETags remembered per store key (most recent first). */
|
|
150
|
+
const CANDIDATES_PER_KEY = 4;
|
|
151
|
+
/**
|
|
152
|
+
* Content-addressed cache of `GET /meta` responses, keyed by the server's
|
|
153
|
+
* `ETag`, used by {@link Client.meta} to revalidate instead of downloading.
|
|
154
|
+
*
|
|
155
|
+
* For each store key (the meta URL, or `ClientOptions.metaKey`) the store
|
|
156
|
+
* remembers the ETags it has seen. `meta()` sends them as `If-None-Match`;
|
|
157
|
+
* when the server answers `304 Not Modified` the client reuses the body
|
|
158
|
+
* stored under the returned `ETag`. Several keys that serve the same bytes
|
|
159
|
+
* — e.g. one parametric controller mounted at `/api/tenant/:id/todos` —
|
|
160
|
+
* therefore share one stored body, and the first visit to a new key costs a
|
|
161
|
+
* `304` instead of the full `/meta`.
|
|
162
|
+
*
|
|
163
|
+
* It is a pure cache: the server decides on every request whether the body
|
|
164
|
+
* is still valid. Failures are never stored.
|
|
165
|
+
*
|
|
166
|
+
* @since 0.1.153
|
|
167
|
+
*/
|
|
168
|
+
var MetaStore = class {
|
|
169
|
+
_maxEntries;
|
|
170
|
+
_unconditionalMs;
|
|
171
|
+
/** Raw JSON body by opaque ETag (`W/` stripped — `If-None-Match` compares weakly). */
|
|
172
|
+
_bodies = /* @__PURE__ */ new Map();
|
|
173
|
+
_keys = /* @__PURE__ */ new Map();
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this._maxEntries = Math.max(1, options?.maxEntries ?? 50);
|
|
176
|
+
this._unconditionalMs = Math.max(0, options?.unconditionalMs ?? 3e5);
|
|
177
|
+
}
|
|
178
|
+
/** Number of stored `/meta` bodies. */
|
|
179
|
+
get size() {
|
|
180
|
+
return this._bodies.size;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The ETags this store would send as `If-None-Match` for `key` — those seen
|
|
184
|
+
* for it whose body is still stored, most recent first.
|
|
185
|
+
*/
|
|
186
|
+
candidates(key) {
|
|
187
|
+
const entry = this._keys.get(key);
|
|
188
|
+
if (!entry) return [];
|
|
189
|
+
return entry.etags.filter((etag) => this._bodies.has(opaque(etag)));
|
|
190
|
+
}
|
|
191
|
+
/** Drops every stored body and remembered ETag. */
|
|
192
|
+
clear() {
|
|
193
|
+
this._bodies.clear();
|
|
194
|
+
this._keys.clear();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Stored body for `etag` (weak comparison), marking it — and `etag` as the
|
|
198
|
+
* latest candidate for `key` — as recently used.
|
|
199
|
+
*
|
|
200
|
+
* @internal used by `Client`
|
|
201
|
+
*/
|
|
202
|
+
_reuse(key, etag) {
|
|
203
|
+
const id = opaque(etag);
|
|
204
|
+
const text = this._bodies.get(id);
|
|
205
|
+
if (text === void 0) return void 0;
|
|
206
|
+
this._bodies.delete(id);
|
|
207
|
+
this._bodies.set(id, text);
|
|
208
|
+
this._remember(key, etag);
|
|
209
|
+
return text;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Stores a `200` body under its ETag and remembers the ETag for `key`.
|
|
213
|
+
*
|
|
214
|
+
* @internal used by `Client`
|
|
215
|
+
*/
|
|
216
|
+
_put(key, etag, text) {
|
|
217
|
+
const id = opaque(etag);
|
|
218
|
+
this._bodies.delete(id);
|
|
219
|
+
this._bodies.set(id, text);
|
|
220
|
+
while (this._bodies.size > this._maxEntries) this._bodies.delete(this._bodies.keys().next().value);
|
|
221
|
+
this._remember(key, etag);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Whether `key` is in its no-`If-None-Match` window; an expired window is
|
|
225
|
+
* cleared, so the next request is conditional again.
|
|
226
|
+
*
|
|
227
|
+
* @internal used by `Client`
|
|
228
|
+
*/
|
|
229
|
+
_isUnconditional(key) {
|
|
230
|
+
const entry = this._keys.get(key);
|
|
231
|
+
const until = entry?.unconditionalUntil;
|
|
232
|
+
if (until === void 0) return false;
|
|
233
|
+
if (Date.now() < until) return true;
|
|
234
|
+
delete entry.unconditionalUntil;
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
/** @internal used by `Client` */
|
|
238
|
+
_markUnconditional(key) {
|
|
239
|
+
this._touchKey(key).unconditionalUntil = Date.now() + this._unconditionalMs;
|
|
240
|
+
}
|
|
241
|
+
_remember(key, etag) {
|
|
242
|
+
const entry = this._touchKey(key);
|
|
243
|
+
const id = opaque(etag);
|
|
244
|
+
entry.etags = [etag, ...entry.etags.filter((e) => opaque(e) !== id)].slice(0, CANDIDATES_PER_KEY);
|
|
245
|
+
}
|
|
246
|
+
/** The entry for `key`, created if needed and moved to most recently used. */
|
|
247
|
+
_touchKey(key) {
|
|
248
|
+
const entry = this._keys.get(key) ?? { etags: [] };
|
|
249
|
+
this._keys.delete(key);
|
|
250
|
+
this._keys.set(key, entry);
|
|
251
|
+
while (this._keys.size > this._maxEntries * 4) this._keys.delete(this._keys.keys().next().value);
|
|
252
|
+
return entry;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
function opaque(etag) {
|
|
256
|
+
return etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The store every {@link Client} uses unless `ClientOptions.metaStore` says
|
|
260
|
+
* otherwise, so all clients in a page share revalidated `/meta` bodies.
|
|
261
|
+
*/
|
|
262
|
+
const defaultMetaStore = new MetaStore();
|
|
263
|
+
/**
|
|
264
|
+
* Clears the shared default {@link MetaStore}. Call it on sign-out or any
|
|
265
|
+
* identity change (together with `Client.invalidateMeta()` on live clients).
|
|
266
|
+
*
|
|
267
|
+
* @since 0.1.153
|
|
268
|
+
*/
|
|
269
|
+
function clearMetaStore() {
|
|
270
|
+
defaultMetaStore.clear();
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
148
273
|
//#region src/client.ts
|
|
149
274
|
/**
|
|
150
275
|
* HTTP client for moost-db REST endpoints.
|
|
@@ -185,7 +310,11 @@ var Client = class {
|
|
|
185
310
|
_headers;
|
|
186
311
|
_navigate;
|
|
187
312
|
_lenientWrites;
|
|
313
|
+
_metaStore;
|
|
314
|
+
_metaKey;
|
|
188
315
|
_metaPromise;
|
|
316
|
+
/** The `ETag` of the `/meta` body {@link _metaPromise} resolved to. */
|
|
317
|
+
_metaEtag;
|
|
189
318
|
_validatorPromise;
|
|
190
319
|
/** Cached deserialized form schemas keyed by resolved URL. */
|
|
191
320
|
_formCache = /* @__PURE__ */ new Map();
|
|
@@ -196,6 +325,8 @@ var Client = class {
|
|
|
196
325
|
this._headers = opts?.headers;
|
|
197
326
|
this._navigate = opts?.navigate;
|
|
198
327
|
this._lenientWrites = opts?.lenientWrites;
|
|
328
|
+
this._metaStore = opts?.metaStore === false ? void 0 : opts?.metaStore ?? defaultMetaStore;
|
|
329
|
+
this._metaKey = opts?.metaKey;
|
|
199
330
|
}
|
|
200
331
|
/**
|
|
201
332
|
* `GET /query` — query records with typed filter, sort, select, and relations.
|
|
@@ -322,10 +453,17 @@ var Client = class {
|
|
|
322
453
|
}
|
|
323
454
|
/**
|
|
324
455
|
* `GET /meta` — table/view metadata (cached after first call).
|
|
456
|
+
*
|
|
457
|
+
* The response is revalidated through the client's meta store
|
|
458
|
+
* (`ClientOptions.metaStore`): ETags seen before for this store key go out
|
|
459
|
+
* as `If-None-Match`, and a `304 Not Modified` reuses the stored body.
|
|
325
460
|
*/
|
|
326
461
|
async meta() {
|
|
327
462
|
if (!this._metaPromise) {
|
|
328
|
-
const p = this.
|
|
463
|
+
const p = this._loadMeta().then(({ body, etag }) => {
|
|
464
|
+
if (this._metaPromise === p) this._metaEtag = etag;
|
|
465
|
+
return body;
|
|
466
|
+
}, (err) => {
|
|
329
467
|
if (this._metaPromise === p) this._metaPromise = void 0;
|
|
330
468
|
throw err;
|
|
331
469
|
});
|
|
@@ -334,6 +472,19 @@ var Client = class {
|
|
|
334
472
|
return this._metaPromise;
|
|
335
473
|
}
|
|
336
474
|
/**
|
|
475
|
+
* The `ETag` of the `/meta` body {@link meta} resolved to — `undefined`
|
|
476
|
+
* before it resolves, after {@link invalidateMeta}, or when the server sent
|
|
477
|
+
* no readable `ETag` (cross-origin without `Access-Control-Expose-Headers:
|
|
478
|
+
* ETag`). Equal ETags mean byte-identical `/meta` bodies, so it can key
|
|
479
|
+
* anything derived from them (deserialized types, validators, column
|
|
480
|
+
* definitions) across clients and route params.
|
|
481
|
+
*
|
|
482
|
+
* @since 0.1.153
|
|
483
|
+
*/
|
|
484
|
+
metaEtag() {
|
|
485
|
+
return this._metaEtag;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
337
488
|
* Drops everything this client cached from the server's metadata — the
|
|
338
489
|
* `/meta` response, the validator built from it and the loaded action
|
|
339
490
|
* forms — so the next call fetches them again. Call it when the viewer's
|
|
@@ -341,10 +492,15 @@ var Client = class {
|
|
|
341
492
|
* user, and a client reused across identities would otherwise keep serving
|
|
342
493
|
* the previous user's columns, actions and write rules.
|
|
343
494
|
*
|
|
495
|
+
* The next `meta()` still revalidates through the meta store, so an
|
|
496
|
+
* unchanged `/meta` costs a `304`, not a download. On an identity change
|
|
497
|
+
* also clear the store (`clearMetaStore()` / `metaStore.clear()`).
|
|
498
|
+
*
|
|
344
499
|
* @since 0.1.151
|
|
345
500
|
*/
|
|
346
501
|
invalidateMeta() {
|
|
347
502
|
this._metaPromise = void 0;
|
|
503
|
+
this._metaEtag = void 0;
|
|
348
504
|
this._validatorPromise = void 0;
|
|
349
505
|
this._formCache.clear();
|
|
350
506
|
}
|
|
@@ -600,6 +756,83 @@ var Client = class {
|
|
|
600
756
|
if (typeof this._headers === "function") return await this._headers();
|
|
601
757
|
return this._headers;
|
|
602
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* `GET /meta` through the meta store: conditional when the store holds
|
|
761
|
+
* bodies for this key, falling back to a plain request whenever the
|
|
762
|
+
* conditional one cannot be answered from the store.
|
|
763
|
+
*/
|
|
764
|
+
async _loadMeta() {
|
|
765
|
+
const url = this._endpointUrl("meta");
|
|
766
|
+
const store = this._metaStore;
|
|
767
|
+
if (!store) {
|
|
768
|
+
const res = await this._fetchUrl(url, {
|
|
769
|
+
method: "GET",
|
|
770
|
+
headers: await this._resolveHeaders()
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
body: await this._readResponse(res, url, "GET", false),
|
|
774
|
+
etag: res.ok ? readEtag(res) : void 0
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const key = this._metaKey ?? url;
|
|
778
|
+
const headers = await this._resolveHeaders();
|
|
779
|
+
const candidates = store._isUnconditional(key) ? [] : store.candidates(key);
|
|
780
|
+
if (candidates.length > 0) {
|
|
781
|
+
const init = {
|
|
782
|
+
method: "GET",
|
|
783
|
+
headers: {
|
|
784
|
+
...headers,
|
|
785
|
+
"If-None-Match": candidates.join(", ")
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
let res;
|
|
789
|
+
try {
|
|
790
|
+
res = await this._fetchUrl(url, init);
|
|
791
|
+
} catch {}
|
|
792
|
+
if (res && res.status === 304) {
|
|
793
|
+
const etag = readEtag(res);
|
|
794
|
+
const text = etag === void 0 ? void 0 : store._reuse(key, etag);
|
|
795
|
+
if (text !== void 0) return {
|
|
796
|
+
body: JSON.parse(text),
|
|
797
|
+
etag
|
|
798
|
+
};
|
|
799
|
+
} else if (res) return this._storeMeta(store, key, url, res);
|
|
800
|
+
else {
|
|
801
|
+
const fallback = await this._fetchUrl(url, {
|
|
802
|
+
method: "GET",
|
|
803
|
+
headers: { ...headers }
|
|
804
|
+
});
|
|
805
|
+
store._markUnconditional(key);
|
|
806
|
+
return this._storeMeta(store, key, url, fallback);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const res = await this._fetchUrl(url, {
|
|
810
|
+
method: "GET",
|
|
811
|
+
headers: { ...headers }
|
|
812
|
+
});
|
|
813
|
+
return this._storeMeta(store, key, url, res);
|
|
814
|
+
}
|
|
815
|
+
/** Reads a `/meta` response; a `2xx` carrying a readable `ETag` goes into the store. */
|
|
816
|
+
async _storeMeta(store, key, url, res) {
|
|
817
|
+
const etag = res.ok ? readEtag(res) : void 0;
|
|
818
|
+
if (etag === void 0 || typeof res.text !== "function") return {
|
|
819
|
+
body: await this._readResponse(res, url, "GET", false),
|
|
820
|
+
etag
|
|
821
|
+
};
|
|
822
|
+
let body;
|
|
823
|
+
let text;
|
|
824
|
+
try {
|
|
825
|
+
text = await res.text();
|
|
826
|
+
body = JSON.parse(text);
|
|
827
|
+
} catch (cause) {
|
|
828
|
+
throw new TransportError("GET", url, `HTTP ${res.status} body is not JSON (${describeCause(cause)})`, cause);
|
|
829
|
+
}
|
|
830
|
+
store._put(key, etag, text);
|
|
831
|
+
return {
|
|
832
|
+
body,
|
|
833
|
+
etag
|
|
834
|
+
};
|
|
835
|
+
}
|
|
603
836
|
_request(method, endpoint, body) {
|
|
604
837
|
return this._requestUrl(method, this._endpointUrl(endpoint), body);
|
|
605
838
|
}
|
|
@@ -622,13 +855,19 @@ var Client = class {
|
|
|
622
855
|
return this._send(url, init, allowEmpty);
|
|
623
856
|
}
|
|
624
857
|
async _send(url, init, allowEmpty) {
|
|
625
|
-
const
|
|
626
|
-
|
|
858
|
+
const res = await this._fetchUrl(url, init);
|
|
859
|
+
return this._readResponse(res, url, init.method ?? "GET", allowEmpty);
|
|
860
|
+
}
|
|
861
|
+
/** `fetch` with a network failure surfaced as {@link TransportError}. */
|
|
862
|
+
async _fetchUrl(url, init) {
|
|
627
863
|
try {
|
|
628
|
-
|
|
864
|
+
return await this._fetch(url, init);
|
|
629
865
|
} catch (cause) {
|
|
630
|
-
throw new TransportError(method, url, describeCause(cause), cause);
|
|
866
|
+
throw new TransportError(init.method ?? "GET", url, describeCause(cause), cause);
|
|
631
867
|
}
|
|
868
|
+
}
|
|
869
|
+
/** A response's JSON body, or the matching error for a non-2xx status. */
|
|
870
|
+
async _readResponse(res, url, method, allowEmpty) {
|
|
632
871
|
if (!res.ok) {
|
|
633
872
|
let errorBody;
|
|
634
873
|
try {
|
|
@@ -657,6 +896,13 @@ var Client = class {
|
|
|
657
896
|
}
|
|
658
897
|
}
|
|
659
898
|
};
|
|
899
|
+
/**
|
|
900
|
+
* The response's `ETag`, when readable — a cross-origin response hides it
|
|
901
|
+
* unless the server sends `Access-Control-Expose-Headers: ETag`.
|
|
902
|
+
*/
|
|
903
|
+
function readEtag(res) {
|
|
904
|
+
return res.headers?.get?.("etag") || void 0;
|
|
905
|
+
}
|
|
660
906
|
/** One-line rendering of a thrown value for a `TransportError` message. */
|
|
661
907
|
function describeCause(cause) {
|
|
662
908
|
if (cause instanceof Error) return cause.message || cause.name;
|
|
@@ -758,6 +1004,7 @@ exports.ActionTargetError = ActionTargetError;
|
|
|
758
1004
|
exports.ActionUnsupportedError = ActionUnsupportedError;
|
|
759
1005
|
exports.Client = Client;
|
|
760
1006
|
exports.ClientError = ClientError;
|
|
1007
|
+
exports.MetaStore = MetaStore;
|
|
761
1008
|
exports.TransportError = TransportError;
|
|
762
1009
|
exports.VersionMismatchError = VersionMismatchError;
|
|
763
1010
|
exports.actionIdentifier = actionIdentifier;
|
|
@@ -773,6 +1020,7 @@ Object.defineProperty(exports, "bucketStartInstant", {
|
|
|
773
1020
|
return _uniqu_core.bucketStartInstant;
|
|
774
1021
|
}
|
|
775
1022
|
});
|
|
1023
|
+
exports.clearMetaStore = clearMetaStore;
|
|
776
1024
|
exports.encodeNavigateId = encodeNavigateId;
|
|
777
1025
|
exports.formatIdentifier = formatIdentifier;
|
|
778
1026
|
exports.formatIdentifierField = formatIdentifierField;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, H as ValidGroupBy, I as TDbInsertResult, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-
|
|
1
|
+
import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, G as MetaStoreOptions, H as ValidGroupBy, I as TDbInsertResult, K as clearMetaStore, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, W as MetaStore, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-u-SCH8c3.cjs";
|
|
2
2
|
import { AggregateExpr, AggregateOfExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketSeriesOptions, NextBucketOptions, SelectArithExpr, Uniquery as Uniquery$1, ValidGroupBy as ValidGroupBy$1, bucketSeries, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
|
|
3
3
|
import { TAtscriptAnnotatedType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
|
|
@@ -49,7 +49,11 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
49
49
|
private readonly _headers?;
|
|
50
50
|
private readonly _navigate?;
|
|
51
51
|
private readonly _lenientWrites?;
|
|
52
|
+
private readonly _metaStore;
|
|
53
|
+
private readonly _metaKey?;
|
|
52
54
|
private _metaPromise?;
|
|
55
|
+
/** The `ETag` of the `/meta` body {@link _metaPromise} resolved to. */
|
|
56
|
+
private _metaEtag?;
|
|
53
57
|
private _validatorPromise?;
|
|
54
58
|
/** Cached deserialized form schemas keyed by resolved URL. */
|
|
55
59
|
private _formCache;
|
|
@@ -163,8 +167,23 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
163
167
|
remove(id: Id<T>): Promise<TDbDeleteResult$1>;
|
|
164
168
|
/**
|
|
165
169
|
* `GET /meta` — table/view metadata (cached after first call).
|
|
170
|
+
*
|
|
171
|
+
* The response is revalidated through the client's meta store
|
|
172
|
+
* (`ClientOptions.metaStore`): ETags seen before for this store key go out
|
|
173
|
+
* as `If-None-Match`, and a `304 Not Modified` reuses the stored body.
|
|
166
174
|
*/
|
|
167
175
|
meta(): Promise<MetaResponse>;
|
|
176
|
+
/**
|
|
177
|
+
* The `ETag` of the `/meta` body {@link meta} resolved to — `undefined`
|
|
178
|
+
* before it resolves, after {@link invalidateMeta}, or when the server sent
|
|
179
|
+
* no readable `ETag` (cross-origin without `Access-Control-Expose-Headers:
|
|
180
|
+
* ETag`). Equal ETags mean byte-identical `/meta` bodies, so it can key
|
|
181
|
+
* anything derived from them (deserialized types, validators, column
|
|
182
|
+
* definitions) across clients and route params.
|
|
183
|
+
*
|
|
184
|
+
* @since 0.1.153
|
|
185
|
+
*/
|
|
186
|
+
metaEtag(): string | undefined;
|
|
168
187
|
/**
|
|
169
188
|
* Drops everything this client cached from the server's metadata — the
|
|
170
189
|
* `/meta` response, the validator built from it and the loaded action
|
|
@@ -173,6 +192,10 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
173
192
|
* user, and a client reused across identities would otherwise keep serving
|
|
174
193
|
* the previous user's columns, actions and write rules.
|
|
175
194
|
*
|
|
195
|
+
* The next `meta()` still revalidates through the meta store, so an
|
|
196
|
+
* unchanged `/meta` costs a `304`, not a download. On an identity change
|
|
197
|
+
* also clear the store (`clearMetaStore()` / `metaStore.clear()`).
|
|
198
|
+
*
|
|
176
199
|
* @since 0.1.151
|
|
177
200
|
*/
|
|
178
201
|
invalidateMeta(): void;
|
|
@@ -299,12 +322,24 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
299
322
|
private _getOrNull;
|
|
300
323
|
private _get;
|
|
301
324
|
private _resolveHeaders;
|
|
325
|
+
/**
|
|
326
|
+
* `GET /meta` through the meta store: conditional when the store holds
|
|
327
|
+
* bodies for this key, falling back to a plain request whenever the
|
|
328
|
+
* conditional one cannot be answered from the store.
|
|
329
|
+
*/
|
|
330
|
+
private _loadMeta;
|
|
331
|
+
/** Reads a `/meta` response; a `2xx` carrying a readable `ETag` goes into the store. */
|
|
332
|
+
private _storeMeta;
|
|
302
333
|
private _request;
|
|
303
334
|
/** `baseUrl + path` joined with an endpoint relative to this controller (`""`, `meta`, `?q`…). */
|
|
304
335
|
private _endpointUrl;
|
|
305
336
|
/** Sends one request to an absolute URL with the client's headers and a JSON body. */
|
|
306
337
|
private _requestUrl;
|
|
307
338
|
private _send;
|
|
339
|
+
/** `fetch` with a network failure surfaced as {@link TransportError}. */
|
|
340
|
+
private _fetchUrl;
|
|
341
|
+
/** A response's JSON body, or the matching error for a non-2xx status. */
|
|
342
|
+
private _readResponse;
|
|
308
343
|
}
|
|
309
344
|
/**
|
|
310
345
|
* The identifier `action` takes for one row (or identifier) of the
|
|
@@ -508,4 +543,4 @@ declare class ActionUnsupportedError extends Error {
|
|
|
508
543
|
constructor(action: string, processor: string, message: string);
|
|
509
544
|
}
|
|
510
545
|
//#endregion
|
|
511
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
|
546
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, MetaStore, type MetaStoreOptions, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, clearMetaStore, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, H as ValidGroupBy, I as TDbInsertResult, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-
|
|
1
|
+
import { A as SearchIndexInfo, B as Uniquery, C as MetaResponse, D as PatchOf, E as PageResult, F as TDbInsertManyResult, G as MetaStoreOptions, H as ValidGroupBy, I as TDbInsertResult, K as clearMetaStore, L as TDbQueryTarget, M as TDbDeleteResult, N as TDbInsertIgnoreResult, O as RelationInfo, P as TDbInsertManyIgnoreResult, R as TDbUpdateResult, S as IdOf, T as OwnOf, U as WeekStart, V as UniqueryControls, W as MetaStore, _ as DbRow, a as ValidatorMode, b as FieldMeta, c as AggregateResult, d as BucketUnit, f as CalendarBucketLabel, g as DbPatch, h as DataOf, j as ServerError, k as RowOf, l as AtscriptClientShape, m as ClientResponse, n as ClientValidator, p as ClientOptions, r as ClientValidatorOptions, s as AggregateQuery, t as ClientValidationError, u as BucketExpr, v as DecoratedControls, w as NavOf, x as FilterExpr, y as DecoratedQuery, z as TypedWithRelation } from "./validator-DRW4WDEX.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType, TSerializedAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
import { AggregateExpr, AggregateOfExpr, AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$1, BucketExpr as BucketExpr$1, BucketSeriesOptions, NextBucketOptions, SelectArithExpr, Uniquery as Uniquery$1, ValidGroupBy as ValidGroupBy$1, bucketSeries, bucketStartInstant, nextBucketLabel } from "@uniqu/core";
|
|
4
4
|
import { TCrudOp, TCrudPermissions, TDbActionInfo, TDbActionInfo as TDbActionInfo$1, TDbActionIntent, TDbActionLevel, TDbActionProcessor, TDbActionTargetSummary, TDbActionTargetSummary as TDbActionTargetSummary$1, TDbAvailableActions, TDbAvailableActions as TDbAvailableActions$1, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1 } from "@atscript/db";
|
|
@@ -49,7 +49,11 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
49
49
|
private readonly _headers?;
|
|
50
50
|
private readonly _navigate?;
|
|
51
51
|
private readonly _lenientWrites?;
|
|
52
|
+
private readonly _metaStore;
|
|
53
|
+
private readonly _metaKey?;
|
|
52
54
|
private _metaPromise?;
|
|
55
|
+
/** The `ETag` of the `/meta` body {@link _metaPromise} resolved to. */
|
|
56
|
+
private _metaEtag?;
|
|
53
57
|
private _validatorPromise?;
|
|
54
58
|
/** Cached deserialized form schemas keyed by resolved URL. */
|
|
55
59
|
private _formCache;
|
|
@@ -163,8 +167,23 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
163
167
|
remove(id: Id<T>): Promise<TDbDeleteResult$1>;
|
|
164
168
|
/**
|
|
165
169
|
* `GET /meta` — table/view metadata (cached after first call).
|
|
170
|
+
*
|
|
171
|
+
* The response is revalidated through the client's meta store
|
|
172
|
+
* (`ClientOptions.metaStore`): ETags seen before for this store key go out
|
|
173
|
+
* as `If-None-Match`, and a `304 Not Modified` reuses the stored body.
|
|
166
174
|
*/
|
|
167
175
|
meta(): Promise<MetaResponse>;
|
|
176
|
+
/**
|
|
177
|
+
* The `ETag` of the `/meta` body {@link meta} resolved to — `undefined`
|
|
178
|
+
* before it resolves, after {@link invalidateMeta}, or when the server sent
|
|
179
|
+
* no readable `ETag` (cross-origin without `Access-Control-Expose-Headers:
|
|
180
|
+
* ETag`). Equal ETags mean byte-identical `/meta` bodies, so it can key
|
|
181
|
+
* anything derived from them (deserialized types, validators, column
|
|
182
|
+
* definitions) across clients and route params.
|
|
183
|
+
*
|
|
184
|
+
* @since 0.1.153
|
|
185
|
+
*/
|
|
186
|
+
metaEtag(): string | undefined;
|
|
168
187
|
/**
|
|
169
188
|
* Drops everything this client cached from the server's metadata — the
|
|
170
189
|
* `/meta` response, the validator built from it and the loaded action
|
|
@@ -173,6 +192,10 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
173
192
|
* user, and a client reused across identities would otherwise keep serving
|
|
174
193
|
* the previous user's columns, actions and write rules.
|
|
175
194
|
*
|
|
195
|
+
* The next `meta()` still revalidates through the meta store, so an
|
|
196
|
+
* unchanged `/meta` costs a `304`, not a download. On an identity change
|
|
197
|
+
* also clear the store (`clearMetaStore()` / `metaStore.clear()`).
|
|
198
|
+
*
|
|
176
199
|
* @since 0.1.151
|
|
177
200
|
*/
|
|
178
201
|
invalidateMeta(): void;
|
|
@@ -299,12 +322,24 @@ declare class Client<T extends AtscriptClientShape = AtscriptClientShape, D exte
|
|
|
299
322
|
private _getOrNull;
|
|
300
323
|
private _get;
|
|
301
324
|
private _resolveHeaders;
|
|
325
|
+
/**
|
|
326
|
+
* `GET /meta` through the meta store: conditional when the store holds
|
|
327
|
+
* bodies for this key, falling back to a plain request whenever the
|
|
328
|
+
* conditional one cannot be answered from the store.
|
|
329
|
+
*/
|
|
330
|
+
private _loadMeta;
|
|
331
|
+
/** Reads a `/meta` response; a `2xx` carrying a readable `ETag` goes into the store. */
|
|
332
|
+
private _storeMeta;
|
|
302
333
|
private _request;
|
|
303
334
|
/** `baseUrl + path` joined with an endpoint relative to this controller (`""`, `meta`, `?q`…). */
|
|
304
335
|
private _endpointUrl;
|
|
305
336
|
/** Sends one request to an absolute URL with the client's headers and a JSON body. */
|
|
306
337
|
private _requestUrl;
|
|
307
338
|
private _send;
|
|
339
|
+
/** `fetch` with a network failure surfaced as {@link TransportError}. */
|
|
340
|
+
private _fetchUrl;
|
|
341
|
+
/** A response's JSON body, or the matching error for a non-2xx status. */
|
|
342
|
+
private _readResponse;
|
|
308
343
|
}
|
|
309
344
|
/**
|
|
310
345
|
* The identifier `action` takes for one row (or identifier) of the
|
|
@@ -508,4 +543,4 @@ declare class ActionUnsupportedError extends Error {
|
|
|
508
543
|
constructor(action: string, processor: string, message: string);
|
|
509
544
|
}
|
|
510
545
|
//#endregion
|
|
511
|
-
export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
|
546
|
+
export { ActionDisabledError, type ActionDisabledErrorBody, ActionNotFoundError, ActionTargetError, type ActionTargetErrorBody, ActionUnsupportedError, type AggregateQuery, type AggregateResult, type AtscriptClientShape, type BucketExpr, type BucketSeriesOptions, type BucketUnit, type CalendarBucketLabel, Client, ClientError, type ClientOptions, type ClientResponse, type ClientValidationError, type ClientValidator, type ClientValidatorOptions, type DataOf, type DbPatch, type DbRow, type FieldMeta, type FilterExpr, type IdOf, type MetaResponse, MetaStore, type MetaStoreOptions, type NavOf, type NextBucketOptions, type OwnOf, type PageResult, type PatchOf, type RelationInfo, type RowOf, type SearchIndexInfo, type ServerError, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbActionTargetSummary, type TDbAvailableActions, type TDbDeleteResult, type TDbInsertIgnoreResult, type TDbInsertManyIgnoreResult, type TDbInsertManyResult, type TDbInsertResult, type TDbQueryTarget, type TDbUpdateResult, type TSerializedAnnotatedType, TransportError, type TypedWithRelation, type Uniquery, type UniqueryControls, type ValidGroupBy, type ValidatorMode, VersionMismatchError, type VersionMismatchErrorBody, type WeekStart, actionIdentifier, bucketSeries, bucketStartInstant, clearMetaStore, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
package/dist/index.mjs
CHANGED
|
@@ -144,6 +144,131 @@ var ActionUnsupportedError = class extends Error {
|
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
146
|
//#endregion
|
|
147
|
+
//#region src/meta-store.ts
|
|
148
|
+
/** ETags remembered per store key (most recent first). */
|
|
149
|
+
const CANDIDATES_PER_KEY = 4;
|
|
150
|
+
/**
|
|
151
|
+
* Content-addressed cache of `GET /meta` responses, keyed by the server's
|
|
152
|
+
* `ETag`, used by {@link Client.meta} to revalidate instead of downloading.
|
|
153
|
+
*
|
|
154
|
+
* For each store key (the meta URL, or `ClientOptions.metaKey`) the store
|
|
155
|
+
* remembers the ETags it has seen. `meta()` sends them as `If-None-Match`;
|
|
156
|
+
* when the server answers `304 Not Modified` the client reuses the body
|
|
157
|
+
* stored under the returned `ETag`. Several keys that serve the same bytes
|
|
158
|
+
* — e.g. one parametric controller mounted at `/api/tenant/:id/todos` —
|
|
159
|
+
* therefore share one stored body, and the first visit to a new key costs a
|
|
160
|
+
* `304` instead of the full `/meta`.
|
|
161
|
+
*
|
|
162
|
+
* It is a pure cache: the server decides on every request whether the body
|
|
163
|
+
* is still valid. Failures are never stored.
|
|
164
|
+
*
|
|
165
|
+
* @since 0.1.153
|
|
166
|
+
*/
|
|
167
|
+
var MetaStore = class {
|
|
168
|
+
_maxEntries;
|
|
169
|
+
_unconditionalMs;
|
|
170
|
+
/** Raw JSON body by opaque ETag (`W/` stripped — `If-None-Match` compares weakly). */
|
|
171
|
+
_bodies = /* @__PURE__ */ new Map();
|
|
172
|
+
_keys = /* @__PURE__ */ new Map();
|
|
173
|
+
constructor(options) {
|
|
174
|
+
this._maxEntries = Math.max(1, options?.maxEntries ?? 50);
|
|
175
|
+
this._unconditionalMs = Math.max(0, options?.unconditionalMs ?? 3e5);
|
|
176
|
+
}
|
|
177
|
+
/** Number of stored `/meta` bodies. */
|
|
178
|
+
get size() {
|
|
179
|
+
return this._bodies.size;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The ETags this store would send as `If-None-Match` for `key` — those seen
|
|
183
|
+
* for it whose body is still stored, most recent first.
|
|
184
|
+
*/
|
|
185
|
+
candidates(key) {
|
|
186
|
+
const entry = this._keys.get(key);
|
|
187
|
+
if (!entry) return [];
|
|
188
|
+
return entry.etags.filter((etag) => this._bodies.has(opaque(etag)));
|
|
189
|
+
}
|
|
190
|
+
/** Drops every stored body and remembered ETag. */
|
|
191
|
+
clear() {
|
|
192
|
+
this._bodies.clear();
|
|
193
|
+
this._keys.clear();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Stored body for `etag` (weak comparison), marking it — and `etag` as the
|
|
197
|
+
* latest candidate for `key` — as recently used.
|
|
198
|
+
*
|
|
199
|
+
* @internal used by `Client`
|
|
200
|
+
*/
|
|
201
|
+
_reuse(key, etag) {
|
|
202
|
+
const id = opaque(etag);
|
|
203
|
+
const text = this._bodies.get(id);
|
|
204
|
+
if (text === void 0) return void 0;
|
|
205
|
+
this._bodies.delete(id);
|
|
206
|
+
this._bodies.set(id, text);
|
|
207
|
+
this._remember(key, etag);
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Stores a `200` body under its ETag and remembers the ETag for `key`.
|
|
212
|
+
*
|
|
213
|
+
* @internal used by `Client`
|
|
214
|
+
*/
|
|
215
|
+
_put(key, etag, text) {
|
|
216
|
+
const id = opaque(etag);
|
|
217
|
+
this._bodies.delete(id);
|
|
218
|
+
this._bodies.set(id, text);
|
|
219
|
+
while (this._bodies.size > this._maxEntries) this._bodies.delete(this._bodies.keys().next().value);
|
|
220
|
+
this._remember(key, etag);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Whether `key` is in its no-`If-None-Match` window; an expired window is
|
|
224
|
+
* cleared, so the next request is conditional again.
|
|
225
|
+
*
|
|
226
|
+
* @internal used by `Client`
|
|
227
|
+
*/
|
|
228
|
+
_isUnconditional(key) {
|
|
229
|
+
const entry = this._keys.get(key);
|
|
230
|
+
const until = entry?.unconditionalUntil;
|
|
231
|
+
if (until === void 0) return false;
|
|
232
|
+
if (Date.now() < until) return true;
|
|
233
|
+
delete entry.unconditionalUntil;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
/** @internal used by `Client` */
|
|
237
|
+
_markUnconditional(key) {
|
|
238
|
+
this._touchKey(key).unconditionalUntil = Date.now() + this._unconditionalMs;
|
|
239
|
+
}
|
|
240
|
+
_remember(key, etag) {
|
|
241
|
+
const entry = this._touchKey(key);
|
|
242
|
+
const id = opaque(etag);
|
|
243
|
+
entry.etags = [etag, ...entry.etags.filter((e) => opaque(e) !== id)].slice(0, CANDIDATES_PER_KEY);
|
|
244
|
+
}
|
|
245
|
+
/** The entry for `key`, created if needed and moved to most recently used. */
|
|
246
|
+
_touchKey(key) {
|
|
247
|
+
const entry = this._keys.get(key) ?? { etags: [] };
|
|
248
|
+
this._keys.delete(key);
|
|
249
|
+
this._keys.set(key, entry);
|
|
250
|
+
while (this._keys.size > this._maxEntries * 4) this._keys.delete(this._keys.keys().next().value);
|
|
251
|
+
return entry;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
function opaque(etag) {
|
|
255
|
+
return etag.startsWith("W/") ? etag.slice(2) : etag;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The store every {@link Client} uses unless `ClientOptions.metaStore` says
|
|
259
|
+
* otherwise, so all clients in a page share revalidated `/meta` bodies.
|
|
260
|
+
*/
|
|
261
|
+
const defaultMetaStore = new MetaStore();
|
|
262
|
+
/**
|
|
263
|
+
* Clears the shared default {@link MetaStore}. Call it on sign-out or any
|
|
264
|
+
* identity change (together with `Client.invalidateMeta()` on live clients).
|
|
265
|
+
*
|
|
266
|
+
* @since 0.1.153
|
|
267
|
+
*/
|
|
268
|
+
function clearMetaStore() {
|
|
269
|
+
defaultMetaStore.clear();
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
147
272
|
//#region src/client.ts
|
|
148
273
|
/**
|
|
149
274
|
* HTTP client for moost-db REST endpoints.
|
|
@@ -184,7 +309,11 @@ var Client = class {
|
|
|
184
309
|
_headers;
|
|
185
310
|
_navigate;
|
|
186
311
|
_lenientWrites;
|
|
312
|
+
_metaStore;
|
|
313
|
+
_metaKey;
|
|
187
314
|
_metaPromise;
|
|
315
|
+
/** The `ETag` of the `/meta` body {@link _metaPromise} resolved to. */
|
|
316
|
+
_metaEtag;
|
|
188
317
|
_validatorPromise;
|
|
189
318
|
/** Cached deserialized form schemas keyed by resolved URL. */
|
|
190
319
|
_formCache = /* @__PURE__ */ new Map();
|
|
@@ -195,6 +324,8 @@ var Client = class {
|
|
|
195
324
|
this._headers = opts?.headers;
|
|
196
325
|
this._navigate = opts?.navigate;
|
|
197
326
|
this._lenientWrites = opts?.lenientWrites;
|
|
327
|
+
this._metaStore = opts?.metaStore === false ? void 0 : opts?.metaStore ?? defaultMetaStore;
|
|
328
|
+
this._metaKey = opts?.metaKey;
|
|
198
329
|
}
|
|
199
330
|
/**
|
|
200
331
|
* `GET /query` — query records with typed filter, sort, select, and relations.
|
|
@@ -321,10 +452,17 @@ var Client = class {
|
|
|
321
452
|
}
|
|
322
453
|
/**
|
|
323
454
|
* `GET /meta` — table/view metadata (cached after first call).
|
|
455
|
+
*
|
|
456
|
+
* The response is revalidated through the client's meta store
|
|
457
|
+
* (`ClientOptions.metaStore`): ETags seen before for this store key go out
|
|
458
|
+
* as `If-None-Match`, and a `304 Not Modified` reuses the stored body.
|
|
324
459
|
*/
|
|
325
460
|
async meta() {
|
|
326
461
|
if (!this._metaPromise) {
|
|
327
|
-
const p = this.
|
|
462
|
+
const p = this._loadMeta().then(({ body, etag }) => {
|
|
463
|
+
if (this._metaPromise === p) this._metaEtag = etag;
|
|
464
|
+
return body;
|
|
465
|
+
}, (err) => {
|
|
328
466
|
if (this._metaPromise === p) this._metaPromise = void 0;
|
|
329
467
|
throw err;
|
|
330
468
|
});
|
|
@@ -333,6 +471,19 @@ var Client = class {
|
|
|
333
471
|
return this._metaPromise;
|
|
334
472
|
}
|
|
335
473
|
/**
|
|
474
|
+
* The `ETag` of the `/meta` body {@link meta} resolved to — `undefined`
|
|
475
|
+
* before it resolves, after {@link invalidateMeta}, or when the server sent
|
|
476
|
+
* no readable `ETag` (cross-origin without `Access-Control-Expose-Headers:
|
|
477
|
+
* ETag`). Equal ETags mean byte-identical `/meta` bodies, so it can key
|
|
478
|
+
* anything derived from them (deserialized types, validators, column
|
|
479
|
+
* definitions) across clients and route params.
|
|
480
|
+
*
|
|
481
|
+
* @since 0.1.153
|
|
482
|
+
*/
|
|
483
|
+
metaEtag() {
|
|
484
|
+
return this._metaEtag;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
336
487
|
* Drops everything this client cached from the server's metadata — the
|
|
337
488
|
* `/meta` response, the validator built from it and the loaded action
|
|
338
489
|
* forms — so the next call fetches them again. Call it when the viewer's
|
|
@@ -340,10 +491,15 @@ var Client = class {
|
|
|
340
491
|
* user, and a client reused across identities would otherwise keep serving
|
|
341
492
|
* the previous user's columns, actions and write rules.
|
|
342
493
|
*
|
|
494
|
+
* The next `meta()` still revalidates through the meta store, so an
|
|
495
|
+
* unchanged `/meta` costs a `304`, not a download. On an identity change
|
|
496
|
+
* also clear the store (`clearMetaStore()` / `metaStore.clear()`).
|
|
497
|
+
*
|
|
343
498
|
* @since 0.1.151
|
|
344
499
|
*/
|
|
345
500
|
invalidateMeta() {
|
|
346
501
|
this._metaPromise = void 0;
|
|
502
|
+
this._metaEtag = void 0;
|
|
347
503
|
this._validatorPromise = void 0;
|
|
348
504
|
this._formCache.clear();
|
|
349
505
|
}
|
|
@@ -599,6 +755,83 @@ var Client = class {
|
|
|
599
755
|
if (typeof this._headers === "function") return await this._headers();
|
|
600
756
|
return this._headers;
|
|
601
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* `GET /meta` through the meta store: conditional when the store holds
|
|
760
|
+
* bodies for this key, falling back to a plain request whenever the
|
|
761
|
+
* conditional one cannot be answered from the store.
|
|
762
|
+
*/
|
|
763
|
+
async _loadMeta() {
|
|
764
|
+
const url = this._endpointUrl("meta");
|
|
765
|
+
const store = this._metaStore;
|
|
766
|
+
if (!store) {
|
|
767
|
+
const res = await this._fetchUrl(url, {
|
|
768
|
+
method: "GET",
|
|
769
|
+
headers: await this._resolveHeaders()
|
|
770
|
+
});
|
|
771
|
+
return {
|
|
772
|
+
body: await this._readResponse(res, url, "GET", false),
|
|
773
|
+
etag: res.ok ? readEtag(res) : void 0
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
const key = this._metaKey ?? url;
|
|
777
|
+
const headers = await this._resolveHeaders();
|
|
778
|
+
const candidates = store._isUnconditional(key) ? [] : store.candidates(key);
|
|
779
|
+
if (candidates.length > 0) {
|
|
780
|
+
const init = {
|
|
781
|
+
method: "GET",
|
|
782
|
+
headers: {
|
|
783
|
+
...headers,
|
|
784
|
+
"If-None-Match": candidates.join(", ")
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
let res;
|
|
788
|
+
try {
|
|
789
|
+
res = await this._fetchUrl(url, init);
|
|
790
|
+
} catch {}
|
|
791
|
+
if (res && res.status === 304) {
|
|
792
|
+
const etag = readEtag(res);
|
|
793
|
+
const text = etag === void 0 ? void 0 : store._reuse(key, etag);
|
|
794
|
+
if (text !== void 0) return {
|
|
795
|
+
body: JSON.parse(text),
|
|
796
|
+
etag
|
|
797
|
+
};
|
|
798
|
+
} else if (res) return this._storeMeta(store, key, url, res);
|
|
799
|
+
else {
|
|
800
|
+
const fallback = await this._fetchUrl(url, {
|
|
801
|
+
method: "GET",
|
|
802
|
+
headers: { ...headers }
|
|
803
|
+
});
|
|
804
|
+
store._markUnconditional(key);
|
|
805
|
+
return this._storeMeta(store, key, url, fallback);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const res = await this._fetchUrl(url, {
|
|
809
|
+
method: "GET",
|
|
810
|
+
headers: { ...headers }
|
|
811
|
+
});
|
|
812
|
+
return this._storeMeta(store, key, url, res);
|
|
813
|
+
}
|
|
814
|
+
/** Reads a `/meta` response; a `2xx` carrying a readable `ETag` goes into the store. */
|
|
815
|
+
async _storeMeta(store, key, url, res) {
|
|
816
|
+
const etag = res.ok ? readEtag(res) : void 0;
|
|
817
|
+
if (etag === void 0 || typeof res.text !== "function") return {
|
|
818
|
+
body: await this._readResponse(res, url, "GET", false),
|
|
819
|
+
etag
|
|
820
|
+
};
|
|
821
|
+
let body;
|
|
822
|
+
let text;
|
|
823
|
+
try {
|
|
824
|
+
text = await res.text();
|
|
825
|
+
body = JSON.parse(text);
|
|
826
|
+
} catch (cause) {
|
|
827
|
+
throw new TransportError("GET", url, `HTTP ${res.status} body is not JSON (${describeCause(cause)})`, cause);
|
|
828
|
+
}
|
|
829
|
+
store._put(key, etag, text);
|
|
830
|
+
return {
|
|
831
|
+
body,
|
|
832
|
+
etag
|
|
833
|
+
};
|
|
834
|
+
}
|
|
602
835
|
_request(method, endpoint, body) {
|
|
603
836
|
return this._requestUrl(method, this._endpointUrl(endpoint), body);
|
|
604
837
|
}
|
|
@@ -621,13 +854,19 @@ var Client = class {
|
|
|
621
854
|
return this._send(url, init, allowEmpty);
|
|
622
855
|
}
|
|
623
856
|
async _send(url, init, allowEmpty) {
|
|
624
|
-
const
|
|
625
|
-
|
|
857
|
+
const res = await this._fetchUrl(url, init);
|
|
858
|
+
return this._readResponse(res, url, init.method ?? "GET", allowEmpty);
|
|
859
|
+
}
|
|
860
|
+
/** `fetch` with a network failure surfaced as {@link TransportError}. */
|
|
861
|
+
async _fetchUrl(url, init) {
|
|
626
862
|
try {
|
|
627
|
-
|
|
863
|
+
return await this._fetch(url, init);
|
|
628
864
|
} catch (cause) {
|
|
629
|
-
throw new TransportError(method, url, describeCause(cause), cause);
|
|
865
|
+
throw new TransportError(init.method ?? "GET", url, describeCause(cause), cause);
|
|
630
866
|
}
|
|
867
|
+
}
|
|
868
|
+
/** A response's JSON body, or the matching error for a non-2xx status. */
|
|
869
|
+
async _readResponse(res, url, method, allowEmpty) {
|
|
631
870
|
if (!res.ok) {
|
|
632
871
|
let errorBody;
|
|
633
872
|
try {
|
|
@@ -656,6 +895,13 @@ var Client = class {
|
|
|
656
895
|
}
|
|
657
896
|
}
|
|
658
897
|
};
|
|
898
|
+
/**
|
|
899
|
+
* The response's `ETag`, when readable — a cross-origin response hides it
|
|
900
|
+
* unless the server sends `Access-Control-Expose-Headers: ETag`.
|
|
901
|
+
*/
|
|
902
|
+
function readEtag(res) {
|
|
903
|
+
return res.headers?.get?.("etag") || void 0;
|
|
904
|
+
}
|
|
659
905
|
/** One-line rendering of a thrown value for a `TransportError` message. */
|
|
660
906
|
function describeCause(cause) {
|
|
661
907
|
if (cause instanceof Error) return cause.message || cause.name;
|
|
@@ -751,4 +997,4 @@ function describeShape(value) {
|
|
|
751
997
|
return typeof value;
|
|
752
998
|
}
|
|
753
999
|
//#endregion
|
|
754
|
-
export { ActionDisabledError, ActionNotFoundError, ActionTargetError, ActionUnsupportedError, Client, ClientError, TransportError, VersionMismatchError, actionIdentifier, bucketSeries, bucketStartInstant, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
|
1000
|
+
export { ActionDisabledError, ActionNotFoundError, ActionTargetError, ActionUnsupportedError, Client, ClientError, MetaStore, TransportError, VersionMismatchError, actionIdentifier, bucketSeries, bucketStartInstant, clearMetaStore, encodeNavigateId, formatIdentifier, formatIdentifierField, nextBucketLabel };
|
|
@@ -3,6 +3,89 @@ import { AggregateQuery as AggregateQuery$1, AggregateResult as AggregateResult$
|
|
|
3
3
|
import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
|
|
4
4
|
import { DbPatch, DbResponse, DbRow, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
|
|
5
5
|
|
|
6
|
+
//#region src/meta-store.d.ts
|
|
7
|
+
/** Options for {@link MetaStore}. */
|
|
8
|
+
interface MetaStoreOptions {
|
|
9
|
+
/**
|
|
10
|
+
* How many `/meta` bodies the store keeps (least recently used are dropped
|
|
11
|
+
* first). Default `50`.
|
|
12
|
+
*/
|
|
13
|
+
maxEntries?: number;
|
|
14
|
+
/**
|
|
15
|
+
* After a conditional `/meta` request fails at the transport level (e.g. a
|
|
16
|
+
* CORS preflight refusing `If-None-Match`) while the plain retry succeeds,
|
|
17
|
+
* how long (ms) that store key is loaded without `If-None-Match` before a
|
|
18
|
+
* conditional request is tried again. Default `300000` (5 minutes).
|
|
19
|
+
*/
|
|
20
|
+
unconditionalMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Content-addressed cache of `GET /meta` responses, keyed by the server's
|
|
24
|
+
* `ETag`, used by {@link Client.meta} to revalidate instead of downloading.
|
|
25
|
+
*
|
|
26
|
+
* For each store key (the meta URL, or `ClientOptions.metaKey`) the store
|
|
27
|
+
* remembers the ETags it has seen. `meta()` sends them as `If-None-Match`;
|
|
28
|
+
* when the server answers `304 Not Modified` the client reuses the body
|
|
29
|
+
* stored under the returned `ETag`. Several keys that serve the same bytes
|
|
30
|
+
* — e.g. one parametric controller mounted at `/api/tenant/:id/todos` —
|
|
31
|
+
* therefore share one stored body, and the first visit to a new key costs a
|
|
32
|
+
* `304` instead of the full `/meta`.
|
|
33
|
+
*
|
|
34
|
+
* It is a pure cache: the server decides on every request whether the body
|
|
35
|
+
* is still valid. Failures are never stored.
|
|
36
|
+
*
|
|
37
|
+
* @since 0.1.153
|
|
38
|
+
*/
|
|
39
|
+
declare class MetaStore {
|
|
40
|
+
private readonly _maxEntries;
|
|
41
|
+
private readonly _unconditionalMs;
|
|
42
|
+
/** Raw JSON body by opaque ETag (`W/` stripped — `If-None-Match` compares weakly). */
|
|
43
|
+
private readonly _bodies;
|
|
44
|
+
private readonly _keys;
|
|
45
|
+
constructor(options?: MetaStoreOptions);
|
|
46
|
+
/** Number of stored `/meta` bodies. */
|
|
47
|
+
get size(): number;
|
|
48
|
+
/**
|
|
49
|
+
* The ETags this store would send as `If-None-Match` for `key` — those seen
|
|
50
|
+
* for it whose body is still stored, most recent first.
|
|
51
|
+
*/
|
|
52
|
+
candidates(key: string): string[];
|
|
53
|
+
/** Drops every stored body and remembered ETag. */
|
|
54
|
+
clear(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Stored body for `etag` (weak comparison), marking it — and `etag` as the
|
|
57
|
+
* latest candidate for `key` — as recently used.
|
|
58
|
+
*
|
|
59
|
+
* @internal used by `Client`
|
|
60
|
+
*/
|
|
61
|
+
_reuse(key: string, etag: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Stores a `200` body under its ETag and remembers the ETag for `key`.
|
|
64
|
+
*
|
|
65
|
+
* @internal used by `Client`
|
|
66
|
+
*/
|
|
67
|
+
_put(key: string, etag: string, text: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `key` is in its no-`If-None-Match` window; an expired window is
|
|
70
|
+
* cleared, so the next request is conditional again.
|
|
71
|
+
*
|
|
72
|
+
* @internal used by `Client`
|
|
73
|
+
*/
|
|
74
|
+
_isUnconditional(key: string): boolean;
|
|
75
|
+
/** @internal used by `Client` */
|
|
76
|
+
_markUnconditional(key: string): void;
|
|
77
|
+
private _remember;
|
|
78
|
+
/** The entry for `key`, created if needed and moved to most recently used. */
|
|
79
|
+
private _touchKey;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clears the shared default {@link MetaStore}. Call it on sign-out or any
|
|
83
|
+
* identity change (together with `Client.invalidateMeta()` on live clients).
|
|
84
|
+
*
|
|
85
|
+
* @since 0.1.153
|
|
86
|
+
*/
|
|
87
|
+
declare function clearMetaStore(): void;
|
|
88
|
+
//#endregion
|
|
6
89
|
//#region src/types.d.ts
|
|
7
90
|
/**
|
|
8
91
|
* Write payload for `insert()` / `update()`: every key optional, optional
|
|
@@ -48,6 +131,23 @@ interface ClientOptions {
|
|
|
48
131
|
* preflight catches typos.
|
|
49
132
|
*/
|
|
50
133
|
lenientWrites?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Where `meta()` keeps revalidatable `/meta` bodies (since 0.1.153).
|
|
136
|
+
* Defaults to one store shared by every client in the page, so clients for
|
|
137
|
+
* the same controller — or for one parametric controller under different
|
|
138
|
+
* route params — revalidate with `If-None-Match` and reuse the body on a
|
|
139
|
+
* `304`. Pass your own `new MetaStore()` to isolate a group of clients, or
|
|
140
|
+
* `false` to always download `/meta`.
|
|
141
|
+
*/
|
|
142
|
+
metaStore?: MetaStore | false;
|
|
143
|
+
/**
|
|
144
|
+
* Key under which `metaStore` remembers this client's ETags (since
|
|
145
|
+
* 0.1.153). Defaults to the `/meta` URL. Give clients of a parametric
|
|
146
|
+
* mount one shared key (e.g. the route template
|
|
147
|
+
* `"/api/tenant/:tenantId/todos"`) so a new route param revalidates against
|
|
148
|
+
* the body already loaded for another one.
|
|
149
|
+
*/
|
|
150
|
+
metaKey?: string;
|
|
51
151
|
}
|
|
52
152
|
/** Search index metadata from the server. */
|
|
53
153
|
type SearchIndexInfo = TSearchIndexInfo;
|
|
@@ -248,4 +348,4 @@ declare class ClientValidationError extends Error {
|
|
|
248
348
|
*/
|
|
249
349
|
declare function createClientValidator(meta: MetaResponse, opts?: ClientValidatorOptions): ClientValidator;
|
|
250
350
|
//#endregion
|
|
251
|
-
export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, ValidGroupBy$1 as H, TDbInsertResult$1 as I, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
|
|
351
|
+
export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, MetaStoreOptions as G, ValidGroupBy$1 as H, TDbInsertResult$1 as I, clearMetaStore as K, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, MetaStore as W, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
|
|
@@ -3,6 +3,89 @@ import { TAtscriptAnnotatedType, TAtscriptTypeObject } from "@atscript/typescrip
|
|
|
3
3
|
import { DbPatch, DbResponse, DbRow, TDbDeleteResult as TDbDeleteResult$1, TDbInsertIgnoreResult as TDbInsertIgnoreResult$1, TDbInsertManyIgnoreResult as TDbInsertManyIgnoreResult$1, TDbInsertManyResult as TDbInsertManyResult$1, TDbInsertResult as TDbInsertResult$1, TDbUpdateResult as TDbUpdateResult$1, TFieldMeta, TMetaResponse, TRelationInfo, TSearchIndexInfo } from "@atscript/db";
|
|
4
4
|
import { DbValidationContext, ValidatorMode, ValidatorMode as ValidatorMode$1 } from "@atscript/db/validator";
|
|
5
5
|
|
|
6
|
+
//#region src/meta-store.d.ts
|
|
7
|
+
/** Options for {@link MetaStore}. */
|
|
8
|
+
interface MetaStoreOptions {
|
|
9
|
+
/**
|
|
10
|
+
* How many `/meta` bodies the store keeps (least recently used are dropped
|
|
11
|
+
* first). Default `50`.
|
|
12
|
+
*/
|
|
13
|
+
maxEntries?: number;
|
|
14
|
+
/**
|
|
15
|
+
* After a conditional `/meta` request fails at the transport level (e.g. a
|
|
16
|
+
* CORS preflight refusing `If-None-Match`) while the plain retry succeeds,
|
|
17
|
+
* how long (ms) that store key is loaded without `If-None-Match` before a
|
|
18
|
+
* conditional request is tried again. Default `300000` (5 minutes).
|
|
19
|
+
*/
|
|
20
|
+
unconditionalMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Content-addressed cache of `GET /meta` responses, keyed by the server's
|
|
24
|
+
* `ETag`, used by {@link Client.meta} to revalidate instead of downloading.
|
|
25
|
+
*
|
|
26
|
+
* For each store key (the meta URL, or `ClientOptions.metaKey`) the store
|
|
27
|
+
* remembers the ETags it has seen. `meta()` sends them as `If-None-Match`;
|
|
28
|
+
* when the server answers `304 Not Modified` the client reuses the body
|
|
29
|
+
* stored under the returned `ETag`. Several keys that serve the same bytes
|
|
30
|
+
* — e.g. one parametric controller mounted at `/api/tenant/:id/todos` —
|
|
31
|
+
* therefore share one stored body, and the first visit to a new key costs a
|
|
32
|
+
* `304` instead of the full `/meta`.
|
|
33
|
+
*
|
|
34
|
+
* It is a pure cache: the server decides on every request whether the body
|
|
35
|
+
* is still valid. Failures are never stored.
|
|
36
|
+
*
|
|
37
|
+
* @since 0.1.153
|
|
38
|
+
*/
|
|
39
|
+
declare class MetaStore {
|
|
40
|
+
private readonly _maxEntries;
|
|
41
|
+
private readonly _unconditionalMs;
|
|
42
|
+
/** Raw JSON body by opaque ETag (`W/` stripped — `If-None-Match` compares weakly). */
|
|
43
|
+
private readonly _bodies;
|
|
44
|
+
private readonly _keys;
|
|
45
|
+
constructor(options?: MetaStoreOptions);
|
|
46
|
+
/** Number of stored `/meta` bodies. */
|
|
47
|
+
get size(): number;
|
|
48
|
+
/**
|
|
49
|
+
* The ETags this store would send as `If-None-Match` for `key` — those seen
|
|
50
|
+
* for it whose body is still stored, most recent first.
|
|
51
|
+
*/
|
|
52
|
+
candidates(key: string): string[];
|
|
53
|
+
/** Drops every stored body and remembered ETag. */
|
|
54
|
+
clear(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Stored body for `etag` (weak comparison), marking it — and `etag` as the
|
|
57
|
+
* latest candidate for `key` — as recently used.
|
|
58
|
+
*
|
|
59
|
+
* @internal used by `Client`
|
|
60
|
+
*/
|
|
61
|
+
_reuse(key: string, etag: string): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Stores a `200` body under its ETag and remembers the ETag for `key`.
|
|
64
|
+
*
|
|
65
|
+
* @internal used by `Client`
|
|
66
|
+
*/
|
|
67
|
+
_put(key: string, etag: string, text: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `key` is in its no-`If-None-Match` window; an expired window is
|
|
70
|
+
* cleared, so the next request is conditional again.
|
|
71
|
+
*
|
|
72
|
+
* @internal used by `Client`
|
|
73
|
+
*/
|
|
74
|
+
_isUnconditional(key: string): boolean;
|
|
75
|
+
/** @internal used by `Client` */
|
|
76
|
+
_markUnconditional(key: string): void;
|
|
77
|
+
private _remember;
|
|
78
|
+
/** The entry for `key`, created if needed and moved to most recently used. */
|
|
79
|
+
private _touchKey;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clears the shared default {@link MetaStore}. Call it on sign-out or any
|
|
83
|
+
* identity change (together with `Client.invalidateMeta()` on live clients).
|
|
84
|
+
*
|
|
85
|
+
* @since 0.1.153
|
|
86
|
+
*/
|
|
87
|
+
declare function clearMetaStore(): void;
|
|
88
|
+
//#endregion
|
|
6
89
|
//#region src/types.d.ts
|
|
7
90
|
/**
|
|
8
91
|
* Write payload for `insert()` / `update()`: every key optional, optional
|
|
@@ -48,6 +131,23 @@ interface ClientOptions {
|
|
|
48
131
|
* preflight catches typos.
|
|
49
132
|
*/
|
|
50
133
|
lenientWrites?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Where `meta()` keeps revalidatable `/meta` bodies (since 0.1.153).
|
|
136
|
+
* Defaults to one store shared by every client in the page, so clients for
|
|
137
|
+
* the same controller — or for one parametric controller under different
|
|
138
|
+
* route params — revalidate with `If-None-Match` and reuse the body on a
|
|
139
|
+
* `304`. Pass your own `new MetaStore()` to isolate a group of clients, or
|
|
140
|
+
* `false` to always download `/meta`.
|
|
141
|
+
*/
|
|
142
|
+
metaStore?: MetaStore | false;
|
|
143
|
+
/**
|
|
144
|
+
* Key under which `metaStore` remembers this client's ETags (since
|
|
145
|
+
* 0.1.153). Defaults to the `/meta` URL. Give clients of a parametric
|
|
146
|
+
* mount one shared key (e.g. the route template
|
|
147
|
+
* `"/api/tenant/:tenantId/todos"`) so a new route param revalidates against
|
|
148
|
+
* the body already loaded for another one.
|
|
149
|
+
*/
|
|
150
|
+
metaKey?: string;
|
|
51
151
|
}
|
|
52
152
|
/** Search index metadata from the server. */
|
|
53
153
|
type SearchIndexInfo = TSearchIndexInfo;
|
|
@@ -248,4 +348,4 @@ declare class ClientValidationError extends Error {
|
|
|
248
348
|
*/
|
|
249
349
|
declare function createClientValidator(meta: MetaResponse, opts?: ClientValidatorOptions): ClientValidator;
|
|
250
350
|
//#endregion
|
|
251
|
-
export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, ValidGroupBy$1 as H, TDbInsertResult$1 as I, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
|
|
351
|
+
export { SearchIndexInfo as A, Uniquery$1 as B, MetaResponse as C, PatchOf as D, PageResult as E, TDbInsertManyResult$1 as F, MetaStoreOptions as G, ValidGroupBy$1 as H, TDbInsertResult$1 as I, clearMetaStore as K, TDbQueryTarget as L, TDbDeleteResult$1 as M, TDbInsertIgnoreResult$1 as N, RelationInfo as O, TDbInsertManyIgnoreResult$1 as P, TDbUpdateResult$1 as R, IdOf as S, OwnOf as T, WeekStart as U, UniqueryControls as V, MetaStore as W, DbRow as _, ValidatorMode$1 as a, FieldMeta as b, AggregateResult$1 as c, BucketUnit as d, CalendarBucketLabel as f, DbPatch as g, DataOf as h, DbValidationContext as i, ServerError as j, RowOf as k, AtscriptClientShape as l, ClientResponse as m, ClientValidator as n, createClientValidator as o, ClientOptions as p, ClientValidatorOptions as r, AggregateQuery$1 as s, ClientValidationError as t, BucketExpr$1 as u, DecoratedControls as v, NavOf as w, FilterExpr as x, DecoratedQuery as y, TypedWithRelation as z };
|
package/dist/validator.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-
|
|
1
|
+
import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-u-SCH8c3.cjs";
|
|
2
2
|
export { ClientValidationError, ClientValidator, ClientValidatorOptions, type DbValidationContext, type ValidatorMode, createClientValidator };
|
package/dist/validator.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-
|
|
1
|
+
import { a as ValidatorMode, i as DbValidationContext, n as ClientValidator, o as createClientValidator, r as ClientValidatorOptions, t as ClientValidationError } from "./validator-DRW4WDEX.mjs";
|
|
2
2
|
export { ClientValidationError, ClientValidator, ClientValidatorOptions, type DbValidationContext, type ValidatorMode, createClientValidator };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.154",
|
|
4
4
|
"description": "Browser-compatible HTTP client for @atscript/moost-db REST endpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -43,18 +43,18 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@uniqu/core": "^0.1.
|
|
47
|
-
"@uniqu/url": "^0.1.
|
|
46
|
+
"@uniqu/core": "^0.1.15",
|
|
47
|
+
"@uniqu/url": "^0.1.15"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@atscript/core": "^0.1.
|
|
51
|
-
"@atscript/typescript": "^0.1.
|
|
52
|
-
"unplugin-atscript": "^0.1.
|
|
53
|
-
"@atscript/db": "0.1.
|
|
50
|
+
"@atscript/core": "^0.1.103",
|
|
51
|
+
"@atscript/typescript": "^0.1.103",
|
|
52
|
+
"unplugin-atscript": "^0.1.103",
|
|
53
|
+
"@atscript/db": "0.1.154"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@atscript/db": "^0.1.44",
|
|
57
|
-
"@atscript/typescript": "^0.1.
|
|
57
|
+
"@atscript/typescript": "^0.1.103"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"postinstall": "asc -f dts",
|