@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 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 path-query batches.
705
- It has **no authoritative revisions today**; keep `read()` for editing revisions.
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 assessed;
475
+ let outcome;
476
476
  let contractsVerified;
477
477
  try {
478
- [assessed, contractsVerified] = await Promise.all([
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
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "file": "mdbase-connect.min.js",
3
- "integrity": "sha384-oeF346B54GbocYns1w4tBu6zmyDtayo5iL/ujiP4qtf4pk1NsES2mMQj/0lIYmBb"
3
+ "integrity": "sha384-23aqw9c9S7YPXCd5zLVLjUFjKy3QggldsE+mDLmLzzRkxaOnQxOkLxoQ2j1oujFh"
4
4
  }