@mdbase-dev/connect 0.1.0-beta.123 → 0.1.0-beta.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -2
- package/dist/application-session.js +9 -3
- package/dist/authority-features.d.ts +29 -0
- package/dist/authority-features.js +84 -0
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +4 -4
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/change-events.js +2 -9
- package/dist/collection-client.d.ts +7 -2
- package/dist/collection-client.js +73 -96
- package/dist/connection.d.ts +8 -1
- package/dist/connection.js +55 -116
- package/dist/description-cache.js +1 -28
- package/dist/file-descriptor.d.ts +2 -0
- package/dist/file-descriptor.js +17 -0
- package/dist/file-metadata.d.ts +21 -0
- package/dist/file-metadata.js +112 -0
- package/dist/files.d.ts +7 -8
- package/dist/files.js +13 -45
- package/dist/index.d.ts +2 -2
- package/dist/local-file-transport.js +18 -16
- package/dist/operation-types.d.ts +35 -15
- package/dist/query-pagination.d.ts +7 -3
- package/dist/query-pagination.js +3 -1
- package/dist/query-wire.d.ts +61 -0
- package/dist/query-wire.js +110 -0
- package/dist/read-many.d.ts +4 -3
- package/dist/read-many.js +160 -55
- package/dist/watch-policy.d.ts +22 -1
- package/dist/watch-policy.js +104 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -211,6 +211,69 @@ versioned capabilities in their manifest; they never maintain a parallel array
|
|
|
211
211
|
of protocol operations. Use `getSnapshot()` and `subscribe()` directly or
|
|
212
212
|
through your framework's external-store integration.
|
|
213
213
|
|
|
214
|
+
### Authority implementation features
|
|
215
|
+
|
|
216
|
+
`await connection.supportsAuthorityFeature(id, options)` returns a
|
|
217
|
+
`ConnectOutcome<boolean>`, not permission. Discovery uses the current authority's
|
|
218
|
+
approved `describe`, or a one-item file listing within the approved scope for
|
|
219
|
+
file-only connections. Concurrent calls share discovery; each caller retains its
|
|
220
|
+
own cancellation/deadline. Missing or unknown advertisements mean unsupported.
|
|
221
|
+
Discovery failures remain failures, never evidence for a legacy fallback.
|
|
222
|
+
|
|
223
|
+
`connection.authorityCapabilities` is an immutable, connection-lifetime cache;
|
|
224
|
+
before discovery it is empty. Normalized `describe()` responses also expose
|
|
225
|
+
immutable `authorityCapabilities`. Reauthorization, reconnect, authority
|
|
226
|
+
replacement and direct/relay route changes discard cached support. Nothing is
|
|
227
|
+
persisted, and relay/control-plane capabilities are not authority evidence.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const support = await connection.supportsAuthorityFeature("query-metadata-v1");
|
|
231
|
+
if (!support.ok) renderProblem(support.problem);
|
|
232
|
+
else if (support.value) {
|
|
233
|
+
const result = await connection.query({
|
|
234
|
+
output: "metadata", select: ["source", "file.path"]
|
|
235
|
+
});
|
|
236
|
+
if (result.ok) {
|
|
237
|
+
// Required path, types, revision and values; no frontmatter/file/body/document.
|
|
238
|
+
for (const row of result.value.results) useSummary(row.path, row.revision, row.values);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Late-updated authorities: keep the ordinary query, not a metadata cast.
|
|
242
|
+
const result = await connection.query({ select: ["source", "file.path"] });
|
|
243
|
+
if (result.ok) useLegacyRows(result.value.results);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const stat = await connection.files.stat({ path: "Assets/book.pdf" });
|
|
247
|
+
// Or: connection.files.stat({ fileId: descriptor.fileId }, { signal });
|
|
248
|
+
if (!stat.ok) renderProblem(stat.problem);
|
|
249
|
+
else if (stat.value) useFile(stat.value); // null means missing or invisible.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`query`, `queryPages` and `queryAll` discriminate metadata rows in their return
|
|
253
|
+
and progress types. Explicit metadata calls require `query-metadata-v1` and fail
|
|
254
|
+
with `unsupported_operation` **before dispatch** when absent. They reject
|
|
255
|
+
`includeBody:true`. Ordinary queries and their inputs remain unchanged; their
|
|
256
|
+
optional `revision` is the authority's exact-source token, not a semantic-cache
|
|
257
|
+
freshness guarantee. Never attach a query token to a later point-read body.
|
|
258
|
+
Advanced `MdbaseCollectionClient` transports may supply the constructor's feature
|
|
259
|
+
helper; without one, metadata is unsupported.
|
|
260
|
+
|
|
261
|
+
`files.stat` requires existing file **list** approval and returns
|
|
262
|
+
`ConnectOutcome<CollectionFileDescriptor|null>`. When `files-stat-v1` is absent,
|
|
263
|
+
it uses the existing paginated listing: the known folder for a path, the approved
|
|
264
|
+
scope for a file ID, comparing portable file identity. This legacy path is not
|
|
265
|
+
point-cost or an atomic snapshot; listing-reset (`file_list_changed`), denial,
|
|
266
|
+
cancellation and timeout outcomes remain visible. A stat does not reserve bytes:
|
|
267
|
+
downloads still need revision/digest validation.
|
|
268
|
+
|
|
269
|
+
**Migration:** existing query/list callers need no changes. Writer, Reader,
|
|
270
|
+
TaskNotes and editor can replace point-discovery folder scans with `files.stat`;
|
|
271
|
+
metadata consumers must branch on successful discovery and keep their ordinary
|
|
272
|
+
query path for old authorities. Remove those fallbacks only after the minimum
|
|
273
|
+
supported authority implements the feature, all consumer pins adopt it, and
|
|
274
|
+
N-1/rollback/connection-cache windows close. Feature support never broadens the
|
|
275
|
+
exact local grant. Authorities advertise only after their own qualification.
|
|
276
|
+
|
|
214
277
|
### Legacy v1 and capability-group v2 declarations
|
|
215
278
|
|
|
216
279
|
The same SDK accepts both declaration versions without translating legacy exact
|
|
@@ -280,6 +343,11 @@ in the background. Route details remain diagnostics; feature UI should read
|
|
|
280
343
|
needs authorization/setup, is temporarily unavailable, or is unsupported by the
|
|
281
344
|
selected authority.
|
|
282
345
|
|
|
346
|
+
A route or selection refresh supersedes its previous setup verification.
|
|
347
|
+
Cancellation of that obsolete generation must not fail startup or disconnect
|
|
348
|
+
its replacement; genuine exceptions and current assessment failures still
|
|
349
|
+
surface normally.
|
|
350
|
+
|
|
283
351
|
`MdbaseBrowserSelection` keeps the stable collection ID in
|
|
284
352
|
`?collection=<id>`, preserves unrelated path/query/hash and router state, and
|
|
285
353
|
reports browser back/forward
|
|
@@ -701,8 +769,15 @@ opt-in total cap; `limit` remains a page-size fallback. `pageSize` sets the init
|
|
|
701
769
|
cursor size unless `firstPageSize` overrides it; cursor continuations use that
|
|
702
770
|
pinned size. Abort releases even a paused iterator's cursor without needing
|
|
703
771
|
`return()`. `readMany(paths, { includeBody, frontmatterMode, types })` provides
|
|
704
|
-
ordered found/missing/error entries over bounded independent
|
|
705
|
-
|
|
772
|
+
ordered found/missing/error entries over bounded independent batches. When
|
|
773
|
+
`read-many-documents-v1` is advertised it uses revision-bearing document reads;
|
|
774
|
+
otherwise it retains typed path queries without sending extended inputs. The
|
|
775
|
+
signature and query-shaped result stay unchanged (`includeBody` defaults false,
|
|
776
|
+
`frontmatterMode` defaults effective). A `types` filter is evaluated by an
|
|
777
|
+
authority query before hydration; selection and reads are not one snapshot.
|
|
778
|
+
Qualified reads require existing `read` approval, plus `query` for type selection.
|
|
779
|
+
Legacy revisions remain optional; never infer them from mtime/size or combine a
|
|
780
|
+
query token with later content. Keep `read()` for exact Markdown/full documents.
|
|
706
781
|
`linksTo(field, path)` builds a guarded server-side `asFile()` predicate (use
|
|
707
782
|
`{multiple: true}` for a link list), not a client resolver. See
|
|
708
783
|
[query helper contracts and consumer migration notes](../../docs/sdk-query-helpers.md).
|
|
@@ -472,20 +472,26 @@ export class MdbaseApplicationSession {
|
|
|
472
472
|
abortAssessment();
|
|
473
473
|
else
|
|
474
474
|
options?.signal?.addEventListener("abort", abortAssessment, { once: true });
|
|
475
|
-
let
|
|
475
|
+
let outcome;
|
|
476
476
|
let contractsVerified;
|
|
477
477
|
try {
|
|
478
|
-
[
|
|
478
|
+
[outcome, contractsVerified] = await Promise.all([
|
|
479
479
|
timeStartupRead("setup-assessment", () => connection.assessCollectionSetup(initialInput, { ...options, signal: controller.signal })), verification
|
|
480
480
|
]);
|
|
481
481
|
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
// Only expected cancellation of an obsolete verification is not a startup fault.
|
|
484
|
+
if (generation !== this.verificationGeneration && controller.signal.aborted
|
|
485
|
+
&& (error === controller.signal.reason || (error instanceof MdbaseConnectError && error.code === "operation_cancelled")))
|
|
486
|
+
return;
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
482
489
|
finally {
|
|
483
490
|
options?.signal?.removeEventListener("abort", abortAssessment);
|
|
484
491
|
controller.abort();
|
|
485
492
|
if (this.readinessController === controller)
|
|
486
493
|
this.readinessController = undefined;
|
|
487
494
|
}
|
|
488
|
-
let outcome = assessed;
|
|
489
495
|
if (generation !== this.verificationGeneration)
|
|
490
496
|
return;
|
|
491
497
|
if (!outcome.ok) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FileCapability } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { CollectionDescription, ConnectRequestOptions } from "./operation-types.js";
|
|
3
|
+
import { type ConnectOutcome } from "./outcomes.js";
|
|
4
|
+
/** Optional response member: absence is legacy; malformed presence is a failure. */
|
|
5
|
+
export declare function authorityCapabilities(value: unknown): readonly string[];
|
|
6
|
+
interface Discovery {
|
|
7
|
+
/** Changes with authorization/authority replacement and direct/relay route. Never persisted. */
|
|
8
|
+
lifetime(): string;
|
|
9
|
+
operations(): readonly string[];
|
|
10
|
+
fileCapability(): FileCapability | null;
|
|
11
|
+
describe(options: ConnectRequestOptions): Promise<ConnectOutcome<CollectionDescription>>;
|
|
12
|
+
filesPage(folder: string | undefined, signal: AbortSignal): Promise<readonly string[]>;
|
|
13
|
+
}
|
|
14
|
+
/** One authority discovery cache per connection, never a global or persisted registry. */
|
|
15
|
+
export declare class AuthorityFeatures {
|
|
16
|
+
private readonly discovery;
|
|
17
|
+
private readonly timeoutMs;
|
|
18
|
+
private lifetime;
|
|
19
|
+
private generation;
|
|
20
|
+
private cached;
|
|
21
|
+
private readonly pending;
|
|
22
|
+
constructor(discovery: Discovery, timeoutMs: number | null);
|
|
23
|
+
get capabilities(): readonly string[];
|
|
24
|
+
supports(id: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<boolean>>;
|
|
25
|
+
invalidate(): void;
|
|
26
|
+
refreshLifetime(): void;
|
|
27
|
+
private discover;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { connectError, MdbaseConnectError } from "./errors.js";
|
|
2
|
+
import { ALL_CONNECT_PROBLEM_CODES, captureConnectOutcome } from "./outcomes.js";
|
|
3
|
+
import { withRequestBudget } from "./request-budget.js";
|
|
4
|
+
import { SharedOperationMap } from "./shared-operation.js";
|
|
5
|
+
const AUTHORITY_FEATURES = new Set([
|
|
6
|
+
"files-stat-v1", "query-record-revisions-v1", "read-many-documents-v1",
|
|
7
|
+
"query-metadata-v1", "contract-query-v1", "link-resolution-options-v1"
|
|
8
|
+
]);
|
|
9
|
+
const EMPTY_CAPABILITIES = Object.freeze([]);
|
|
10
|
+
/** Optional response member: absence is legacy; malformed presence is a failure. */
|
|
11
|
+
export function authorityCapabilities(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return EMPTY_CAPABILITIES;
|
|
14
|
+
if (!Array.isArray(value) || value.some(id => typeof id !== "string")) {
|
|
15
|
+
throw connectError("invalid_operation_response", "The authority returned invalid feature capabilities.");
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze([...new Set(value.filter(id => AUTHORITY_FEATURES.has(id)))]);
|
|
18
|
+
}
|
|
19
|
+
/** One authority discovery cache per connection, never a global or persisted registry. */
|
|
20
|
+
export class AuthorityFeatures {
|
|
21
|
+
discovery;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
lifetime;
|
|
24
|
+
generation = 0;
|
|
25
|
+
cached;
|
|
26
|
+
pending = new SharedOperationMap();
|
|
27
|
+
constructor(discovery, timeoutMs) {
|
|
28
|
+
this.discovery = discovery;
|
|
29
|
+
this.timeoutMs = timeoutMs;
|
|
30
|
+
}
|
|
31
|
+
get capabilities() {
|
|
32
|
+
this.refreshLifetime();
|
|
33
|
+
return this.cached ?? EMPTY_CAPABILITIES;
|
|
34
|
+
}
|
|
35
|
+
supports(id, options) {
|
|
36
|
+
return captureConnectOutcome(() => withRequestBudget(options, this.timeoutMs, async (budget) => {
|
|
37
|
+
if (!AUTHORITY_FEATURES.has(id))
|
|
38
|
+
return false;
|
|
39
|
+
while (true) {
|
|
40
|
+
this.refreshLifetime();
|
|
41
|
+
const generation = this.generation;
|
|
42
|
+
const capabilities = this.cached ?? await this.pending.run(String(generation), { signal: budget.signal, timeoutMs: null }, null, signal => this.discover(signal));
|
|
43
|
+
this.refreshLifetime();
|
|
44
|
+
// A successful discovery can itself select a direct route. Rediscover on
|
|
45
|
+
// the current route, rather than borrowing evidence from a retired one.
|
|
46
|
+
if (generation !== this.generation)
|
|
47
|
+
continue;
|
|
48
|
+
if (!budget.signal.aborted)
|
|
49
|
+
this.cached = capabilities;
|
|
50
|
+
return capabilities.includes(id);
|
|
51
|
+
}
|
|
52
|
+
}), ALL_CONNECT_PROBLEM_CODES);
|
|
53
|
+
}
|
|
54
|
+
invalidate() {
|
|
55
|
+
this.cached = undefined;
|
|
56
|
+
this.lifetime = undefined;
|
|
57
|
+
}
|
|
58
|
+
refreshLifetime() {
|
|
59
|
+
const lifetime = this.discovery.lifetime();
|
|
60
|
+
if (lifetime === this.lifetime)
|
|
61
|
+
return;
|
|
62
|
+
this.lifetime = lifetime;
|
|
63
|
+
this.generation += 1;
|
|
64
|
+
this.cached = undefined;
|
|
65
|
+
}
|
|
66
|
+
async discover(signal) {
|
|
67
|
+
if (this.discovery.operations().includes("describe")) {
|
|
68
|
+
const outcome = await this.discovery.describe({ signal, timeoutMs: null, coordination: { coalesce: false } });
|
|
69
|
+
if (!outcome.ok)
|
|
70
|
+
throw new MdbaseConnectError(outcome.problem);
|
|
71
|
+
return authorityCapabilities(outcome.value.authorityCapabilities);
|
|
72
|
+
}
|
|
73
|
+
const capability = this.discovery.fileCapability();
|
|
74
|
+
if (capability?.actions.includes("list")) {
|
|
75
|
+
const folder = capability.scope.kind === "selected_folders" ? capability.scope.folders[0] : undefined;
|
|
76
|
+
const capabilities = await this.discovery.filesPage(folder, signal);
|
|
77
|
+
// File-only discovery is evidence for file features, never native queries.
|
|
78
|
+
return Object.freeze(capabilities.filter(id => id === "files-stat-v1"));
|
|
79
|
+
}
|
|
80
|
+
// Consumer: legacy grants without describe/file-list permission. Remove when
|
|
81
|
+
// the minimum authority/consumer pins and N-1 rollback/cache windows close.
|
|
82
|
+
return EMPTY_CAPABILITIES;
|
|
83
|
+
}
|
|
84
|
+
}
|