@objectstack/metadata 17.0.0-rc.6 → 17.1.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 +2700 -0
- package/dist/index.cjs +126 -22
- 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 +131 -23
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +126 -22
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +131 -23
- package/dist/node.js.map +1 -1
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -1783,16 +1783,40 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1783
1783
|
// above. The concurrent half is delivered by `inflightListReads` below, which
|
|
1784
1784
|
// is why the two fields are one policy and are documented together.
|
|
1785
1785
|
//
|
|
1786
|
-
// [#5184] That hazard is NOT
|
|
1787
|
-
//
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
//
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
//
|
|
1795
|
-
//
|
|
1786
|
+
// [#5184; re-measured under #7708 on 2026-08-11] That hazard is NOT
|
|
1787
|
+
// historical — and the re-measurement NARROWED it rather than retiring it.
|
|
1788
|
+
// Measured on the current stack: real `ObjectQL` + real `SqlDriver`
|
|
1789
|
+
// (better-sqlite3), a real `DatabaseLoader.list()` with the loader's own
|
|
1790
|
+
// cache off, `knex.client.pool.max === 1` confirmed for the SQLite dialect
|
|
1791
|
+
// and `acquireConnectionTimeout` left at the knex default of 60s.
|
|
1792
|
+
//
|
|
1793
|
+
// • Transaction opened DIRECTLY on the driver (`driver.beginTransaction()`)
|
|
1794
|
+
// → the read STALLS for the full timeout and then throws knex's "Timeout
|
|
1795
|
+
// acquiring a connection" (measured: 60_085ms). `DatabaseLoader._find()`
|
|
1796
|
+
// forwards no options, so nothing threads the caller's transaction, and
|
|
1797
|
+
// `driver-sql` still models SQLite as a single-connection pool
|
|
1798
|
+
// (`activeTransactions`, `assertBareKnexSafe` — the latter a dev/test
|
|
1799
|
+
// guard that is a no-op in production, so production still waits the
|
|
1800
|
+
// timeout out). `list()` catches that throw and degrades, which is
|
|
1801
|
+
// precisely the entry whose TTL this policy is choosing.
|
|
1802
|
+
// • Transaction opened through `engine.transaction()` / `ScopedContext`
|
|
1803
|
+
// → returns immediately (measured: 12ms, with `activeTransactions === 1`
|
|
1804
|
+
// and the driver observably receiving the handle on the call). Those
|
|
1805
|
+
// publish the transaction into the engine's ambient `txStore` (ADR-0034)
|
|
1806
|
+
// and `buildDriverOptions` threads it onto the read for the loader.
|
|
1807
|
+
//
|
|
1808
|
+
// So the stall shape is live but CONDITIONAL: it needs an open transaction
|
|
1809
|
+
// that the engine's ambient store cannot see. `SqlDriver.ensureSequencesTable()`
|
|
1810
|
+
// is the live witness that this is worth designing against — it takes
|
|
1811
|
+
// `parentTrx` and runs its DDL on the caller's transaction for exactly this
|
|
1812
|
+
// reason, with `assertBareKnexSafe` as the tripwire for callers that forget;
|
|
1813
|
+
// `sql-driver-sqlite-tx-guard.test.ts` pins both halves. (Until #7708 the
|
|
1814
|
+
// witness cited here was `plugin-audit`'s `captureBefore`, retired by #6656.
|
|
1815
|
+
// It was REPLACED rather than dropped: the example died, the hazard did not.)
|
|
1816
|
+
//
|
|
1817
|
+
// Hence the policy below keeps caching degraded reads rather than skipping
|
|
1818
|
+
// them: "don't cache a degraded read" would trade one 30s silent window for
|
|
1819
|
+
// a fresh 60s stall per call on every caller in the first bullet.
|
|
1796
1820
|
//
|
|
1797
1821
|
// [#5184] WHAT IS ACTUALLY CACHED, AND FOR HOW LONG — this paragraph is the
|
|
1798
1822
|
// contract, and it describes `cacheListResult()` / `readCachedList()` below.
|
|
@@ -1879,6 +1903,15 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1879
1903
|
* only, so a fresh read that already replaced it keeps its slot. Nothing
|
|
1880
1904
|
* accumulates — a wave of callers arriving after settle finds the cache the
|
|
1881
1905
|
* settle just wrote, and once that lapses it starts one new read.
|
|
1906
|
+
*
|
|
1907
|
+
* [#6504] The shared value is the whole {@link ListReadResult}, not just
|
|
1908
|
+
* `items`. "Sharers share the outcome" above is stated about the answer *and*
|
|
1909
|
+
* its degraded verdict, and while the promise carried only `items` that was
|
|
1910
|
+
* true of `list()` alone: a {@link listDiagnosed} caller joining an in-flight
|
|
1911
|
+
* read had no way to reach the verdict that read had already computed, and
|
|
1912
|
+
* would have had to either re-walk the loaders (defeating this map) or invent
|
|
1913
|
+
* a second, unmemoized answer. `list()` narrows to `.items` at its own return
|
|
1914
|
+
* instead, so every sharer still receives the same array instance.
|
|
1882
1915
|
*/
|
|
1883
1916
|
this.inflightListReads = /* @__PURE__ */ new Map();
|
|
1884
1917
|
// [#5108] Loader names whose read failure has already been reported at
|
|
@@ -2075,6 +2108,8 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2075
2108
|
* {@link MetadataWriteOptions.notify} before doing so.
|
|
2076
2109
|
*/
|
|
2077
2110
|
async register(type, name, data, options) {
|
|
2111
|
+
(0, import_core.assertMetadataRegisterContract)(type, name, data);
|
|
2112
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2078
2113
|
if (this.config.persistence?.writable === false) {
|
|
2079
2114
|
const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
|
|
2080
2115
|
if (this.config.validation?.throwOnError) {
|
|
@@ -2127,6 +2162,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2127
2162
|
* consumers will read the pre-write definition until restart.
|
|
2128
2163
|
*/
|
|
2129
2164
|
registerInMemory(type, name, data) {
|
|
2165
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2130
2166
|
if (!this.registry.has(type)) {
|
|
2131
2167
|
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
2132
2168
|
}
|
|
@@ -2169,6 +2205,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2169
2205
|
* `metadata-manager-get-diagnosed.test.ts`.
|
|
2170
2206
|
*/
|
|
2171
2207
|
async get(type, name) {
|
|
2208
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2172
2209
|
const typeStore = this.registry.get(type);
|
|
2173
2210
|
if (typeStore?.has(name)) {
|
|
2174
2211
|
return typeStore.get(name);
|
|
@@ -2200,6 +2237,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2200
2237
|
* cannot prove the item is absent, so we decline to claim it is.
|
|
2201
2238
|
*/
|
|
2202
2239
|
async getDiagnosed(type, name) {
|
|
2240
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2203
2241
|
const typeStore = this.registry.get(type);
|
|
2204
2242
|
if (typeStore?.has(name)) {
|
|
2205
2243
|
return { data: typeStore.get(name), degraded: false, errors: [] };
|
|
@@ -2221,19 +2259,64 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2221
2259
|
* `listCache`.
|
|
2222
2260
|
*/
|
|
2223
2261
|
async list(type) {
|
|
2262
|
+
return (await this.readList((0, import_core.canonicalMetadataServiceType)(type))).items;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* `list`, plus whether the answer can be trusted as complete.
|
|
2266
|
+
*
|
|
2267
|
+
* [#6504] The plural counterpart of {@link getDiagnosed}, and the same defect
|
|
2268
|
+
* one read over: `readListUncached` has computed this verdict since #5184 and
|
|
2269
|
+
* `list()` spent it entirely on a cache TTL, so a consumer receiving a short
|
|
2270
|
+
* set could not ask whether it was short because that is all anyone declared
|
|
2271
|
+
* or because a loader was down. {@link reportLoaderReadFailure}'s own message
|
|
2272
|
+
* says what that costs — "every list served from now on is a PARTIAL set
|
|
2273
|
+
* presented as a complete one, and the server keeps reporting healthy" — and
|
|
2274
|
+
* until this member existed that sentence was addressed to a log reader only,
|
|
2275
|
+
* because no caller had a way to ask.
|
|
2276
|
+
*
|
|
2277
|
+
* Sharper than the singular case rather than merely analogous: `list` is the
|
|
2278
|
+
* read whose answer carries a **count**, and a consumer restating
|
|
2279
|
+
* `items.length` as "this environment contains N items" makes a positive,
|
|
2280
|
+
* numeric claim about what an author declared out of a read that partly did
|
|
2281
|
+
* not happen.
|
|
2282
|
+
*
|
|
2283
|
+
* Reads through exactly the same cache and single-flight machinery `list()`
|
|
2284
|
+
* does — same entry, same TTLs, same in-flight join — so asking for the
|
|
2285
|
+
* verdict costs no extra loader walk, and `list()` and
|
|
2286
|
+
* `listDiagnosed().items` cannot drift: they are the same read, narrowed at
|
|
2287
|
+
* different points. `degraded` is true when at least one loader threw while
|
|
2288
|
+
* this set was assembled; unlike {@link getDiagnosed} it does NOT additionally
|
|
2289
|
+
* require that nothing answered, because a plural read that lost one loader is
|
|
2290
|
+
* partial even when the others answered plenty — which is the whole fact.
|
|
2291
|
+
*/
|
|
2292
|
+
async listDiagnosed(type) {
|
|
2293
|
+
const { items, degraded, errors } = await this.readList((0, import_core.canonicalMetadataServiceType)(type));
|
|
2294
|
+
return { items, degraded, errors };
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* The cached / single-flight read behind {@link list} and
|
|
2298
|
+
* {@link listDiagnosed}.
|
|
2299
|
+
*
|
|
2300
|
+
* [#6504] Extracted so the two members are one read seen at two widths rather
|
|
2301
|
+
* than two implementations that have to be kept in agreement — the shape
|
|
2302
|
+
* `get`/`getDiagnosed` pay for with a duplicated body and a test pinning them
|
|
2303
|
+
* to each other. Everything below is unchanged in behaviour from when it was
|
|
2304
|
+
* inlined in `list()`; only the verdict now survives the return.
|
|
2305
|
+
*/
|
|
2306
|
+
async readList(type) {
|
|
2224
2307
|
const cached = this.readCachedList(type);
|
|
2225
2308
|
if (cached) {
|
|
2226
|
-
return cached
|
|
2309
|
+
return cached;
|
|
2227
2310
|
}
|
|
2228
2311
|
const joined = this.inflightListReads.get(type);
|
|
2229
2312
|
if (joined) {
|
|
2230
2313
|
return joined;
|
|
2231
2314
|
}
|
|
2232
|
-
const shared = this.readListUncached(type).then((
|
|
2315
|
+
const shared = this.readListUncached(type).then((result) => {
|
|
2233
2316
|
if (this.inflightListReads.get(type) === shared) {
|
|
2234
|
-
this.cacheListResult(type,
|
|
2317
|
+
this.cacheListResult(type, result);
|
|
2235
2318
|
}
|
|
2236
|
-
return
|
|
2319
|
+
return result;
|
|
2237
2320
|
});
|
|
2238
2321
|
this.inflightListReads.set(type, shared);
|
|
2239
2322
|
try {
|
|
@@ -2264,6 +2347,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2264
2347
|
}
|
|
2265
2348
|
}
|
|
2266
2349
|
let degraded = false;
|
|
2350
|
+
const errors = [];
|
|
2267
2351
|
for (const loader of this.loaders.values()) {
|
|
2268
2352
|
try {
|
|
2269
2353
|
const loaderItems = await loader.loadMany(type);
|
|
@@ -2276,10 +2360,11 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2276
2360
|
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2277
2361
|
} catch (e) {
|
|
2278
2362
|
degraded = true;
|
|
2363
|
+
errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2279
2364
|
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
2280
2365
|
}
|
|
2281
2366
|
}
|
|
2282
|
-
return { items: Array.from(items.values()), degraded };
|
|
2367
|
+
return { items: Array.from(items.values()), degraded, errors };
|
|
2283
2368
|
}
|
|
2284
2369
|
/**
|
|
2285
2370
|
* Report — at `error`, once per outage episode — that a loader could not be
|
|
@@ -2341,9 +2426,14 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2341
2426
|
* one thing this cache used to throw away. A result assembled while a loader
|
|
2342
2427
|
* was unreadable is stored, but stored *as* what it is, so it expires on the
|
|
2343
2428
|
* degraded TTL and any reader can tell it apart from a complete answer.
|
|
2429
|
+
*
|
|
2430
|
+
* [#6504] Takes the whole read result rather than its parts for the same
|
|
2431
|
+
* reason: a signature that spreads the verdict across positional arguments is
|
|
2432
|
+
* one a later caller can quietly fill with `false`, which is how the verdict
|
|
2433
|
+
* was lost on the way out in the first place.
|
|
2344
2434
|
*/
|
|
2345
|
-
cacheListResult(type,
|
|
2346
|
-
this.listCache.set(type, { ts: Date.now(),
|
|
2435
|
+
cacheListResult(type, result) {
|
|
2436
|
+
this.listCache.set(type, { ts: Date.now(), ...result });
|
|
2347
2437
|
}
|
|
2348
2438
|
/**
|
|
2349
2439
|
* Read a still-fresh {@link listCache} entry, or `undefined` when there is
|
|
@@ -2496,6 +2586,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2496
2586
|
* before the await would buy nothing and would re-open step 1's window.
|
|
2497
2587
|
*/
|
|
2498
2588
|
async unregister(type, name, options) {
|
|
2589
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2499
2590
|
for (const loader of this.loaders.values()) {
|
|
2500
2591
|
if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
|
|
2501
2592
|
if (typeof loader.delete !== "function") continue;
|
|
@@ -2598,6 +2689,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2598
2689
|
* Check if a metadata item exists
|
|
2599
2690
|
*/
|
|
2600
2691
|
async exists(type, name) {
|
|
2692
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2601
2693
|
if (this.registry.get(type)?.has(name)) {
|
|
2602
2694
|
return true;
|
|
2603
2695
|
}
|
|
@@ -2612,6 +2704,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2612
2704
|
* List all names of metadata items of a given type
|
|
2613
2705
|
*/
|
|
2614
2706
|
async listNames(type) {
|
|
2707
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
2615
2708
|
const names = /* @__PURE__ */ new Set();
|
|
2616
2709
|
const typeStore = this.registry.get(type);
|
|
2617
2710
|
if (typeStore) {
|
|
@@ -2917,11 +3010,21 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2917
3010
|
}
|
|
2918
3011
|
}
|
|
2919
3012
|
if (packageItems.length === 0) {
|
|
2920
|
-
|
|
3013
|
+
const err = new Error(
|
|
3014
|
+
`No metadata items found for package '${packageId}'`
|
|
3015
|
+
);
|
|
3016
|
+
err.code = "RESOURCE_NOT_FOUND";
|
|
3017
|
+
err.status = 404;
|
|
3018
|
+
throw err;
|
|
2921
3019
|
}
|
|
2922
3020
|
const hasPublished = packageItems.some((item) => item.data.publishedDefinition !== void 0);
|
|
2923
3021
|
if (!hasPublished) {
|
|
2924
|
-
|
|
3022
|
+
const err = new Error(
|
|
3023
|
+
`Package '${packageId}' has never been published`
|
|
3024
|
+
);
|
|
3025
|
+
err.code = "RESOURCE_CONFLICT";
|
|
3026
|
+
err.status = 409;
|
|
3027
|
+
throw err;
|
|
2925
3028
|
}
|
|
2926
3029
|
for (const item of packageItems) {
|
|
2927
3030
|
if (item.data.publishedDefinition !== void 0) {
|
|
@@ -3174,6 +3277,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
3174
3277
|
* @returns An unsubscribe function.
|
|
3175
3278
|
*/
|
|
3176
3279
|
subscribe(type, callback) {
|
|
3280
|
+
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
3177
3281
|
this.addWatchCallback(type, callback);
|
|
3178
3282
|
return () => this.removeWatchCallback(type, callback);
|
|
3179
3283
|
}
|
|
@@ -4689,12 +4793,12 @@ var MetadataPlugin = class {
|
|
|
4689
4793
|
this.repository = void 0;
|
|
4690
4794
|
};
|
|
4691
4795
|
this.options = {
|
|
4692
|
-
|
|
4693
|
-
|
|
4796
|
+
...options,
|
|
4797
|
+
watch: options.watch ?? false
|
|
4694
4798
|
};
|
|
4695
4799
|
const rootDir = this.options.rootDir || process.cwd();
|
|
4696
4800
|
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
4697
|
-
const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ??
|
|
4801
|
+
const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? false;
|
|
4698
4802
|
this.manager = new NodeMetadataManager({
|
|
4699
4803
|
rootDir,
|
|
4700
4804
|
watch: effectiveWatch,
|