@objectstack/metadata 17.0.0-rc.6 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2523 -0
- package/dist/index.cjs +123 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +128 -20
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +123 -19
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +128 -20
- package/dist/node.js.map +1 -1
- package/package.json +8 -8
package/dist/index.d.cts
CHANGED
|
@@ -191,6 +191,23 @@ interface MetadataLoader {
|
|
|
191
191
|
* Watch callback function (legacy)
|
|
192
192
|
*/
|
|
193
193
|
type WatchCallback = (event: MetadataWatchEvent) => void | Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* [#6504] What one `list()` read actually produced: the best-effort set, plus
|
|
196
|
+
* whether assembling it lost a loader.
|
|
197
|
+
*
|
|
198
|
+
* The return of {@link MetadataManager.readListUncached}, the value shared
|
|
199
|
+
* through `inflightListReads`, and — minus the cache bookkeeping — what
|
|
200
|
+
* {@link MetadataManager.listDiagnosed} hands to a caller. One shape for all
|
|
201
|
+
* three on purpose: the verdict used to be dropped at each hop outward
|
|
202
|
+
* (`readListUncached` computed it, the in-flight promise kept only `items`,
|
|
203
|
+
* `list()` returned only that), and a single carried record is what makes
|
|
204
|
+
* losing it again take an edit rather than an omission.
|
|
205
|
+
*/
|
|
206
|
+
interface ListReadResult {
|
|
207
|
+
items: unknown[];
|
|
208
|
+
degraded: boolean;
|
|
209
|
+
errors: string[];
|
|
210
|
+
}
|
|
194
211
|
interface MetadataManagerOptions extends MetadataManagerConfig {
|
|
195
212
|
loaders?: MetadataLoader[];
|
|
196
213
|
/** Optional IDataDriver instance. When provided alongside config.datasource, auto-configures DatabaseLoader. */
|
|
@@ -270,6 +287,15 @@ declare class MetadataManager implements IMetadataService {
|
|
|
270
287
|
* only, so a fresh read that already replaced it keeps its slot. Nothing
|
|
271
288
|
* accumulates — a wave of callers arriving after settle finds the cache the
|
|
272
289
|
* settle just wrote, and once that lapses it starts one new read.
|
|
290
|
+
*
|
|
291
|
+
* [#6504] The shared value is the whole {@link ListReadResult}, not just
|
|
292
|
+
* `items`. "Sharers share the outcome" above is stated about the answer *and*
|
|
293
|
+
* its degraded verdict, and while the promise carried only `items` that was
|
|
294
|
+
* true of `list()` alone: a {@link listDiagnosed} caller joining an in-flight
|
|
295
|
+
* read had no way to reach the verdict that read had already computed, and
|
|
296
|
+
* would have had to either re-walk the loaders (defeating this map) or invent
|
|
297
|
+
* a second, unmemoized answer. `list()` narrows to `.items` at its own return
|
|
298
|
+
* instead, so every sharer still receives the same array instance.
|
|
273
299
|
*/
|
|
274
300
|
private readonly inflightListReads;
|
|
275
301
|
private readonly loaderReadFailureReported;
|
|
@@ -457,6 +483,46 @@ declare class MetadataManager implements IMetadataService {
|
|
|
457
483
|
* `listCache`.
|
|
458
484
|
*/
|
|
459
485
|
list(type: string): Promise<unknown[]>;
|
|
486
|
+
/**
|
|
487
|
+
* `list`, plus whether the answer can be trusted as complete.
|
|
488
|
+
*
|
|
489
|
+
* [#6504] The plural counterpart of {@link getDiagnosed}, and the same defect
|
|
490
|
+
* one read over: `readListUncached` has computed this verdict since #5184 and
|
|
491
|
+
* `list()` spent it entirely on a cache TTL, so a consumer receiving a short
|
|
492
|
+
* set could not ask whether it was short because that is all anyone declared
|
|
493
|
+
* or because a loader was down. {@link reportLoaderReadFailure}'s own message
|
|
494
|
+
* says what that costs — "every list served from now on is a PARTIAL set
|
|
495
|
+
* presented as a complete one, and the server keeps reporting healthy" — and
|
|
496
|
+
* until this member existed that sentence was addressed to a log reader only,
|
|
497
|
+
* because no caller had a way to ask.
|
|
498
|
+
*
|
|
499
|
+
* Sharper than the singular case rather than merely analogous: `list` is the
|
|
500
|
+
* read whose answer carries a **count**, and a consumer restating
|
|
501
|
+
* `items.length` as "this environment contains N items" makes a positive,
|
|
502
|
+
* numeric claim about what an author declared out of a read that partly did
|
|
503
|
+
* not happen.
|
|
504
|
+
*
|
|
505
|
+
* Reads through exactly the same cache and single-flight machinery `list()`
|
|
506
|
+
* does — same entry, same TTLs, same in-flight join — so asking for the
|
|
507
|
+
* verdict costs no extra loader walk, and `list()` and
|
|
508
|
+
* `listDiagnosed().items` cannot drift: they are the same read, narrowed at
|
|
509
|
+
* different points. `degraded` is true when at least one loader threw while
|
|
510
|
+
* this set was assembled; unlike {@link getDiagnosed} it does NOT additionally
|
|
511
|
+
* require that nothing answered, because a plural read that lost one loader is
|
|
512
|
+
* partial even when the others answered plenty — which is the whole fact.
|
|
513
|
+
*/
|
|
514
|
+
listDiagnosed(type: string): Promise<ListReadResult>;
|
|
515
|
+
/**
|
|
516
|
+
* The cached / single-flight read behind {@link list} and
|
|
517
|
+
* {@link listDiagnosed}.
|
|
518
|
+
*
|
|
519
|
+
* [#6504] Extracted so the two members are one read seen at two widths rather
|
|
520
|
+
* than two implementations that have to be kept in agreement — the shape
|
|
521
|
+
* `get`/`getDiagnosed` pay for with a duplicated body and a test pinning them
|
|
522
|
+
* to each other. Everything below is unchanged in behaviour from when it was
|
|
523
|
+
* inlined in `list()`; only the verdict now survives the return.
|
|
524
|
+
*/
|
|
525
|
+
private readList;
|
|
460
526
|
/**
|
|
461
527
|
* Assemble the `list()` answer for `type` from the in-memory registry plus
|
|
462
528
|
* every loader, reporting (but not rethrowing) loaders that could not be
|
|
@@ -516,6 +582,11 @@ declare class MetadataManager implements IMetadataService {
|
|
|
516
582
|
* one thing this cache used to throw away. A result assembled while a loader
|
|
517
583
|
* was unreadable is stored, but stored *as* what it is, so it expires on the
|
|
518
584
|
* degraded TTL and any reader can tell it apart from a complete answer.
|
|
585
|
+
*
|
|
586
|
+
* [#6504] Takes the whole read result rather than its parts for the same
|
|
587
|
+
* reason: a signature that spreads the verdict across positional arguments is
|
|
588
|
+
* one a later caller can quietly fill with `false`, which is how the verdict
|
|
589
|
+
* was lost on the way out in the first place.
|
|
519
590
|
*/
|
|
520
591
|
private cacheListResult;
|
|
521
592
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -191,6 +191,23 @@ interface MetadataLoader {
|
|
|
191
191
|
* Watch callback function (legacy)
|
|
192
192
|
*/
|
|
193
193
|
type WatchCallback = (event: MetadataWatchEvent) => void | Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* [#6504] What one `list()` read actually produced: the best-effort set, plus
|
|
196
|
+
* whether assembling it lost a loader.
|
|
197
|
+
*
|
|
198
|
+
* The return of {@link MetadataManager.readListUncached}, the value shared
|
|
199
|
+
* through `inflightListReads`, and — minus the cache bookkeeping — what
|
|
200
|
+
* {@link MetadataManager.listDiagnosed} hands to a caller. One shape for all
|
|
201
|
+
* three on purpose: the verdict used to be dropped at each hop outward
|
|
202
|
+
* (`readListUncached` computed it, the in-flight promise kept only `items`,
|
|
203
|
+
* `list()` returned only that), and a single carried record is what makes
|
|
204
|
+
* losing it again take an edit rather than an omission.
|
|
205
|
+
*/
|
|
206
|
+
interface ListReadResult {
|
|
207
|
+
items: unknown[];
|
|
208
|
+
degraded: boolean;
|
|
209
|
+
errors: string[];
|
|
210
|
+
}
|
|
194
211
|
interface MetadataManagerOptions extends MetadataManagerConfig {
|
|
195
212
|
loaders?: MetadataLoader[];
|
|
196
213
|
/** Optional IDataDriver instance. When provided alongside config.datasource, auto-configures DatabaseLoader. */
|
|
@@ -270,6 +287,15 @@ declare class MetadataManager implements IMetadataService {
|
|
|
270
287
|
* only, so a fresh read that already replaced it keeps its slot. Nothing
|
|
271
288
|
* accumulates — a wave of callers arriving after settle finds the cache the
|
|
272
289
|
* settle just wrote, and once that lapses it starts one new read.
|
|
290
|
+
*
|
|
291
|
+
* [#6504] The shared value is the whole {@link ListReadResult}, not just
|
|
292
|
+
* `items`. "Sharers share the outcome" above is stated about the answer *and*
|
|
293
|
+
* its degraded verdict, and while the promise carried only `items` that was
|
|
294
|
+
* true of `list()` alone: a {@link listDiagnosed} caller joining an in-flight
|
|
295
|
+
* read had no way to reach the verdict that read had already computed, and
|
|
296
|
+
* would have had to either re-walk the loaders (defeating this map) or invent
|
|
297
|
+
* a second, unmemoized answer. `list()` narrows to `.items` at its own return
|
|
298
|
+
* instead, so every sharer still receives the same array instance.
|
|
273
299
|
*/
|
|
274
300
|
private readonly inflightListReads;
|
|
275
301
|
private readonly loaderReadFailureReported;
|
|
@@ -457,6 +483,46 @@ declare class MetadataManager implements IMetadataService {
|
|
|
457
483
|
* `listCache`.
|
|
458
484
|
*/
|
|
459
485
|
list(type: string): Promise<unknown[]>;
|
|
486
|
+
/**
|
|
487
|
+
* `list`, plus whether the answer can be trusted as complete.
|
|
488
|
+
*
|
|
489
|
+
* [#6504] The plural counterpart of {@link getDiagnosed}, and the same defect
|
|
490
|
+
* one read over: `readListUncached` has computed this verdict since #5184 and
|
|
491
|
+
* `list()` spent it entirely on a cache TTL, so a consumer receiving a short
|
|
492
|
+
* set could not ask whether it was short because that is all anyone declared
|
|
493
|
+
* or because a loader was down. {@link reportLoaderReadFailure}'s own message
|
|
494
|
+
* says what that costs — "every list served from now on is a PARTIAL set
|
|
495
|
+
* presented as a complete one, and the server keeps reporting healthy" — and
|
|
496
|
+
* until this member existed that sentence was addressed to a log reader only,
|
|
497
|
+
* because no caller had a way to ask.
|
|
498
|
+
*
|
|
499
|
+
* Sharper than the singular case rather than merely analogous: `list` is the
|
|
500
|
+
* read whose answer carries a **count**, and a consumer restating
|
|
501
|
+
* `items.length` as "this environment contains N items" makes a positive,
|
|
502
|
+
* numeric claim about what an author declared out of a read that partly did
|
|
503
|
+
* not happen.
|
|
504
|
+
*
|
|
505
|
+
* Reads through exactly the same cache and single-flight machinery `list()`
|
|
506
|
+
* does — same entry, same TTLs, same in-flight join — so asking for the
|
|
507
|
+
* verdict costs no extra loader walk, and `list()` and
|
|
508
|
+
* `listDiagnosed().items` cannot drift: they are the same read, narrowed at
|
|
509
|
+
* different points. `degraded` is true when at least one loader threw while
|
|
510
|
+
* this set was assembled; unlike {@link getDiagnosed} it does NOT additionally
|
|
511
|
+
* require that nothing answered, because a plural read that lost one loader is
|
|
512
|
+
* partial even when the others answered plenty — which is the whole fact.
|
|
513
|
+
*/
|
|
514
|
+
listDiagnosed(type: string): Promise<ListReadResult>;
|
|
515
|
+
/**
|
|
516
|
+
* The cached / single-flight read behind {@link list} and
|
|
517
|
+
* {@link listDiagnosed}.
|
|
518
|
+
*
|
|
519
|
+
* [#6504] Extracted so the two members are one read seen at two widths rather
|
|
520
|
+
* than two implementations that have to be kept in agreement — the shape
|
|
521
|
+
* `get`/`getDiagnosed` pay for with a duplicated body and a test pinning them
|
|
522
|
+
* to each other. Everything below is unchanged in behaviour from when it was
|
|
523
|
+
* inlined in `list()`; only the verdict now survives the return.
|
|
524
|
+
*/
|
|
525
|
+
private readList;
|
|
460
526
|
/**
|
|
461
527
|
* Assemble the `list()` answer for `type` from the in-memory registry plus
|
|
462
528
|
* every loader, reporting (but not rethrowing) loaders that could not be
|
|
@@ -516,6 +582,11 @@ declare class MetadataManager implements IMetadataService {
|
|
|
516
582
|
* one thing this cache used to throw away. A result assembled while a loader
|
|
517
583
|
* was unreadable is stored, but stored *as* what it is, so it expires on the
|
|
518
584
|
* degraded TTL and any reader can tell it apart from a complete answer.
|
|
585
|
+
*
|
|
586
|
+
* [#6504] Takes the whole read result rather than its parts for the same
|
|
587
|
+
* reason: a signature that spreads the verdict across positional arguments is
|
|
588
|
+
* one a later caller can quietly fill with `false`, which is how the verdict
|
|
589
|
+
* was lost on the way out in the first place.
|
|
519
590
|
*/
|
|
520
591
|
private cacheListResult;
|
|
521
592
|
/**
|
package/dist/index.js
CHANGED
|
@@ -177,7 +177,11 @@ import {
|
|
|
177
177
|
ApiEndpointSchema as ApiEndpointSchema2,
|
|
178
178
|
validateApiEndpointDeclarations
|
|
179
179
|
} from "@objectstack/spec/api";
|
|
180
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
assertMetadataRegisterContract,
|
|
182
|
+
canonicalMetadataServiceType,
|
|
183
|
+
createLogger
|
|
184
|
+
} from "@objectstack/core";
|
|
181
185
|
|
|
182
186
|
// src/serializers/json-serializer.ts
|
|
183
187
|
var JSONSerializer = class {
|
|
@@ -1750,16 +1754,40 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1750
1754
|
// above. The concurrent half is delivered by `inflightListReads` below, which
|
|
1751
1755
|
// is why the two fields are one policy and are documented together.
|
|
1752
1756
|
//
|
|
1753
|
-
// [#5184] That hazard is NOT
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
//
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
//
|
|
1760
|
-
//
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1757
|
+
// [#5184; re-measured under #7708 on 2026-08-11] That hazard is NOT
|
|
1758
|
+
// historical — and the re-measurement NARROWED it rather than retiring it.
|
|
1759
|
+
// Measured on the current stack: real `ObjectQL` + real `SqlDriver`
|
|
1760
|
+
// (better-sqlite3), a real `DatabaseLoader.list()` with the loader's own
|
|
1761
|
+
// cache off, `knex.client.pool.max === 1` confirmed for the SQLite dialect
|
|
1762
|
+
// and `acquireConnectionTimeout` left at the knex default of 60s.
|
|
1763
|
+
//
|
|
1764
|
+
// • Transaction opened DIRECTLY on the driver (`driver.beginTransaction()`)
|
|
1765
|
+
// → the read STALLS for the full timeout and then throws knex's "Timeout
|
|
1766
|
+
// acquiring a connection" (measured: 60_085ms). `DatabaseLoader._find()`
|
|
1767
|
+
// forwards no options, so nothing threads the caller's transaction, and
|
|
1768
|
+
// `driver-sql` still models SQLite as a single-connection pool
|
|
1769
|
+
// (`activeTransactions`, `assertBareKnexSafe` — the latter a dev/test
|
|
1770
|
+
// guard that is a no-op in production, so production still waits the
|
|
1771
|
+
// timeout out). `list()` catches that throw and degrades, which is
|
|
1772
|
+
// precisely the entry whose TTL this policy is choosing.
|
|
1773
|
+
// • Transaction opened through `engine.transaction()` / `ScopedContext`
|
|
1774
|
+
// → returns immediately (measured: 12ms, with `activeTransactions === 1`
|
|
1775
|
+
// and the driver observably receiving the handle on the call). Those
|
|
1776
|
+
// publish the transaction into the engine's ambient `txStore` (ADR-0034)
|
|
1777
|
+
// and `buildDriverOptions` threads it onto the read for the loader.
|
|
1778
|
+
//
|
|
1779
|
+
// So the stall shape is live but CONDITIONAL: it needs an open transaction
|
|
1780
|
+
// that the engine's ambient store cannot see. `SqlDriver.ensureSequencesTable()`
|
|
1781
|
+
// is the live witness that this is worth designing against — it takes
|
|
1782
|
+
// `parentTrx` and runs its DDL on the caller's transaction for exactly this
|
|
1783
|
+
// reason, with `assertBareKnexSafe` as the tripwire for callers that forget;
|
|
1784
|
+
// `sql-driver-sqlite-tx-guard.test.ts` pins both halves. (Until #7708 the
|
|
1785
|
+
// witness cited here was `plugin-audit`'s `captureBefore`, retired by #6656.
|
|
1786
|
+
// It was REPLACED rather than dropped: the example died, the hazard did not.)
|
|
1787
|
+
//
|
|
1788
|
+
// Hence the policy below keeps caching degraded reads rather than skipping
|
|
1789
|
+
// them: "don't cache a degraded read" would trade one 30s silent window for
|
|
1790
|
+
// a fresh 60s stall per call on every caller in the first bullet.
|
|
1763
1791
|
//
|
|
1764
1792
|
// [#5184] WHAT IS ACTUALLY CACHED, AND FOR HOW LONG — this paragraph is the
|
|
1765
1793
|
// contract, and it describes `cacheListResult()` / `readCachedList()` below.
|
|
@@ -1846,6 +1874,15 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1846
1874
|
* only, so a fresh read that already replaced it keeps its slot. Nothing
|
|
1847
1875
|
* accumulates — a wave of callers arriving after settle finds the cache the
|
|
1848
1876
|
* settle just wrote, and once that lapses it starts one new read.
|
|
1877
|
+
*
|
|
1878
|
+
* [#6504] The shared value is the whole {@link ListReadResult}, not just
|
|
1879
|
+
* `items`. "Sharers share the outcome" above is stated about the answer *and*
|
|
1880
|
+
* its degraded verdict, and while the promise carried only `items` that was
|
|
1881
|
+
* true of `list()` alone: a {@link listDiagnosed} caller joining an in-flight
|
|
1882
|
+
* read had no way to reach the verdict that read had already computed, and
|
|
1883
|
+
* would have had to either re-walk the loaders (defeating this map) or invent
|
|
1884
|
+
* a second, unmemoized answer. `list()` narrows to `.items` at its own return
|
|
1885
|
+
* instead, so every sharer still receives the same array instance.
|
|
1849
1886
|
*/
|
|
1850
1887
|
this.inflightListReads = /* @__PURE__ */ new Map();
|
|
1851
1888
|
// [#5108] Loader names whose read failure has already been reported at
|
|
@@ -2042,6 +2079,8 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2042
2079
|
* {@link MetadataWriteOptions.notify} before doing so.
|
|
2043
2080
|
*/
|
|
2044
2081
|
async register(type, name, data, options) {
|
|
2082
|
+
assertMetadataRegisterContract(type, name, data);
|
|
2083
|
+
type = canonicalMetadataServiceType(type);
|
|
2045
2084
|
if (this.config.persistence?.writable === false) {
|
|
2046
2085
|
const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
|
|
2047
2086
|
if (this.config.validation?.throwOnError) {
|
|
@@ -2094,6 +2133,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2094
2133
|
* consumers will read the pre-write definition until restart.
|
|
2095
2134
|
*/
|
|
2096
2135
|
registerInMemory(type, name, data) {
|
|
2136
|
+
type = canonicalMetadataServiceType(type);
|
|
2097
2137
|
if (!this.registry.has(type)) {
|
|
2098
2138
|
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
2099
2139
|
}
|
|
@@ -2136,6 +2176,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2136
2176
|
* `metadata-manager-get-diagnosed.test.ts`.
|
|
2137
2177
|
*/
|
|
2138
2178
|
async get(type, name) {
|
|
2179
|
+
type = canonicalMetadataServiceType(type);
|
|
2139
2180
|
const typeStore = this.registry.get(type);
|
|
2140
2181
|
if (typeStore?.has(name)) {
|
|
2141
2182
|
return typeStore.get(name);
|
|
@@ -2167,6 +2208,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2167
2208
|
* cannot prove the item is absent, so we decline to claim it is.
|
|
2168
2209
|
*/
|
|
2169
2210
|
async getDiagnosed(type, name) {
|
|
2211
|
+
type = canonicalMetadataServiceType(type);
|
|
2170
2212
|
const typeStore = this.registry.get(type);
|
|
2171
2213
|
if (typeStore?.has(name)) {
|
|
2172
2214
|
return { data: typeStore.get(name), degraded: false, errors: [] };
|
|
@@ -2188,19 +2230,64 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2188
2230
|
* `listCache`.
|
|
2189
2231
|
*/
|
|
2190
2232
|
async list(type) {
|
|
2233
|
+
return (await this.readList(canonicalMetadataServiceType(type))).items;
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* `list`, plus whether the answer can be trusted as complete.
|
|
2237
|
+
*
|
|
2238
|
+
* [#6504] The plural counterpart of {@link getDiagnosed}, and the same defect
|
|
2239
|
+
* one read over: `readListUncached` has computed this verdict since #5184 and
|
|
2240
|
+
* `list()` spent it entirely on a cache TTL, so a consumer receiving a short
|
|
2241
|
+
* set could not ask whether it was short because that is all anyone declared
|
|
2242
|
+
* or because a loader was down. {@link reportLoaderReadFailure}'s own message
|
|
2243
|
+
* says what that costs — "every list served from now on is a PARTIAL set
|
|
2244
|
+
* presented as a complete one, and the server keeps reporting healthy" — and
|
|
2245
|
+
* until this member existed that sentence was addressed to a log reader only,
|
|
2246
|
+
* because no caller had a way to ask.
|
|
2247
|
+
*
|
|
2248
|
+
* Sharper than the singular case rather than merely analogous: `list` is the
|
|
2249
|
+
* read whose answer carries a **count**, and a consumer restating
|
|
2250
|
+
* `items.length` as "this environment contains N items" makes a positive,
|
|
2251
|
+
* numeric claim about what an author declared out of a read that partly did
|
|
2252
|
+
* not happen.
|
|
2253
|
+
*
|
|
2254
|
+
* Reads through exactly the same cache and single-flight machinery `list()`
|
|
2255
|
+
* does — same entry, same TTLs, same in-flight join — so asking for the
|
|
2256
|
+
* verdict costs no extra loader walk, and `list()` and
|
|
2257
|
+
* `listDiagnosed().items` cannot drift: they are the same read, narrowed at
|
|
2258
|
+
* different points. `degraded` is true when at least one loader threw while
|
|
2259
|
+
* this set was assembled; unlike {@link getDiagnosed} it does NOT additionally
|
|
2260
|
+
* require that nothing answered, because a plural read that lost one loader is
|
|
2261
|
+
* partial even when the others answered plenty — which is the whole fact.
|
|
2262
|
+
*/
|
|
2263
|
+
async listDiagnosed(type) {
|
|
2264
|
+
const { items, degraded, errors } = await this.readList(canonicalMetadataServiceType(type));
|
|
2265
|
+
return { items, degraded, errors };
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* The cached / single-flight read behind {@link list} and
|
|
2269
|
+
* {@link listDiagnosed}.
|
|
2270
|
+
*
|
|
2271
|
+
* [#6504] Extracted so the two members are one read seen at two widths rather
|
|
2272
|
+
* than two implementations that have to be kept in agreement — the shape
|
|
2273
|
+
* `get`/`getDiagnosed` pay for with a duplicated body and a test pinning them
|
|
2274
|
+
* to each other. Everything below is unchanged in behaviour from when it was
|
|
2275
|
+
* inlined in `list()`; only the verdict now survives the return.
|
|
2276
|
+
*/
|
|
2277
|
+
async readList(type) {
|
|
2191
2278
|
const cached = this.readCachedList(type);
|
|
2192
2279
|
if (cached) {
|
|
2193
|
-
return cached
|
|
2280
|
+
return cached;
|
|
2194
2281
|
}
|
|
2195
2282
|
const joined = this.inflightListReads.get(type);
|
|
2196
2283
|
if (joined) {
|
|
2197
2284
|
return joined;
|
|
2198
2285
|
}
|
|
2199
|
-
const shared = this.readListUncached(type).then((
|
|
2286
|
+
const shared = this.readListUncached(type).then((result) => {
|
|
2200
2287
|
if (this.inflightListReads.get(type) === shared) {
|
|
2201
|
-
this.cacheListResult(type,
|
|
2288
|
+
this.cacheListResult(type, result);
|
|
2202
2289
|
}
|
|
2203
|
-
return
|
|
2290
|
+
return result;
|
|
2204
2291
|
});
|
|
2205
2292
|
this.inflightListReads.set(type, shared);
|
|
2206
2293
|
try {
|
|
@@ -2231,6 +2318,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2231
2318
|
}
|
|
2232
2319
|
}
|
|
2233
2320
|
let degraded = false;
|
|
2321
|
+
const errors = [];
|
|
2234
2322
|
for (const loader of this.loaders.values()) {
|
|
2235
2323
|
try {
|
|
2236
2324
|
const loaderItems = await loader.loadMany(type);
|
|
@@ -2243,10 +2331,11 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2243
2331
|
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2244
2332
|
} catch (e) {
|
|
2245
2333
|
degraded = true;
|
|
2334
|
+
errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2246
2335
|
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
2247
2336
|
}
|
|
2248
2337
|
}
|
|
2249
|
-
return { items: Array.from(items.values()), degraded };
|
|
2338
|
+
return { items: Array.from(items.values()), degraded, errors };
|
|
2250
2339
|
}
|
|
2251
2340
|
/**
|
|
2252
2341
|
* Report — at `error`, once per outage episode — that a loader could not be
|
|
@@ -2308,9 +2397,14 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2308
2397
|
* one thing this cache used to throw away. A result assembled while a loader
|
|
2309
2398
|
* was unreadable is stored, but stored *as* what it is, so it expires on the
|
|
2310
2399
|
* degraded TTL and any reader can tell it apart from a complete answer.
|
|
2400
|
+
*
|
|
2401
|
+
* [#6504] Takes the whole read result rather than its parts for the same
|
|
2402
|
+
* reason: a signature that spreads the verdict across positional arguments is
|
|
2403
|
+
* one a later caller can quietly fill with `false`, which is how the verdict
|
|
2404
|
+
* was lost on the way out in the first place.
|
|
2311
2405
|
*/
|
|
2312
|
-
cacheListResult(type,
|
|
2313
|
-
this.listCache.set(type, { ts: Date.now(),
|
|
2406
|
+
cacheListResult(type, result) {
|
|
2407
|
+
this.listCache.set(type, { ts: Date.now(), ...result });
|
|
2314
2408
|
}
|
|
2315
2409
|
/**
|
|
2316
2410
|
* Read a still-fresh {@link listCache} entry, or `undefined` when there is
|
|
@@ -2463,6 +2557,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2463
2557
|
* before the await would buy nothing and would re-open step 1's window.
|
|
2464
2558
|
*/
|
|
2465
2559
|
async unregister(type, name, options) {
|
|
2560
|
+
type = canonicalMetadataServiceType(type);
|
|
2466
2561
|
for (const loader of this.loaders.values()) {
|
|
2467
2562
|
if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
|
|
2468
2563
|
if (typeof loader.delete !== "function") continue;
|
|
@@ -2565,6 +2660,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2565
2660
|
* Check if a metadata item exists
|
|
2566
2661
|
*/
|
|
2567
2662
|
async exists(type, name) {
|
|
2663
|
+
type = canonicalMetadataServiceType(type);
|
|
2568
2664
|
if (this.registry.get(type)?.has(name)) {
|
|
2569
2665
|
return true;
|
|
2570
2666
|
}
|
|
@@ -2579,6 +2675,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2579
2675
|
* List all names of metadata items of a given type
|
|
2580
2676
|
*/
|
|
2581
2677
|
async listNames(type) {
|
|
2678
|
+
type = canonicalMetadataServiceType(type);
|
|
2582
2679
|
const names = /* @__PURE__ */ new Set();
|
|
2583
2680
|
const typeStore = this.registry.get(type);
|
|
2584
2681
|
if (typeStore) {
|
|
@@ -2884,11 +2981,21 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2884
2981
|
}
|
|
2885
2982
|
}
|
|
2886
2983
|
if (packageItems.length === 0) {
|
|
2887
|
-
|
|
2984
|
+
const err = new Error(
|
|
2985
|
+
`No metadata items found for package '${packageId}'`
|
|
2986
|
+
);
|
|
2987
|
+
err.code = "RESOURCE_NOT_FOUND";
|
|
2988
|
+
err.status = 404;
|
|
2989
|
+
throw err;
|
|
2888
2990
|
}
|
|
2889
2991
|
const hasPublished = packageItems.some((item) => item.data.publishedDefinition !== void 0);
|
|
2890
2992
|
if (!hasPublished) {
|
|
2891
|
-
|
|
2993
|
+
const err = new Error(
|
|
2994
|
+
`Package '${packageId}' has never been published`
|
|
2995
|
+
);
|
|
2996
|
+
err.code = "RESOURCE_CONFLICT";
|
|
2997
|
+
err.status = 409;
|
|
2998
|
+
throw err;
|
|
2892
2999
|
}
|
|
2893
3000
|
for (const item of packageItems) {
|
|
2894
3001
|
if (item.data.publishedDefinition !== void 0) {
|
|
@@ -3141,6 +3248,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
3141
3248
|
* @returns An unsubscribe function.
|
|
3142
3249
|
*/
|
|
3143
3250
|
subscribe(type, callback) {
|
|
3251
|
+
type = canonicalMetadataServiceType(type);
|
|
3144
3252
|
this.addWatchCallback(type, callback);
|
|
3145
3253
|
return () => this.removeWatchCallback(type, callback);
|
|
3146
3254
|
}
|