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