@mdbase-dev/connect 0.1.0-beta.121 → 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 +161 -6
- package/dist/application-session.js +51 -27
- 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.d.ts +6 -0
- package/dist/change-events.js +95 -0
- package/dist/collection-client.d.ts +13 -3
- package/dist/collection-client.js +98 -140
- package/dist/connection.d.ts +12 -2
- package/dist/connection.js +57 -117
- package/dist/description-cache.d.ts +14 -0
- package/dist/description-cache.js +46 -0
- package/dist/file-crypto.js +10 -6
- package/dist/file-descriptor.d.ts +14 -0
- package/dist/file-descriptor.js +29 -0
- package/dist/file-metadata.d.ts +21 -0
- package/dist/file-metadata.js +112 -0
- package/dist/file-sha256.js +3 -3
- package/dist/files.d.ts +9 -19
- package/dist/files.js +49 -75
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/local-file-transport.js +18 -16
- package/dist/operation-types.d.ts +155 -14
- package/dist/query-pagination-internals.d.ts +9 -0
- package/dist/query-pagination-internals.js +52 -0
- package/dist/query-pagination.d.ts +7 -3
- package/dist/query-pagination.js +31 -27
- package/dist/query-predicates.d.ts +11 -0
- package/dist/query-predicates.js +15 -0
- package/dist/query-wire.d.ts +61 -0
- package/dist/query-wire.js +110 -0
- package/dist/read-many.d.ts +7 -0
- package/dist/read-many.js +175 -0
- package/dist/record-session.d.ts +3 -2
- package/dist/record-session.js +11 -2
- package/dist/records.d.ts +15 -2
- package/dist/records.js +109 -7
- package/dist/saved-view-pagination.js +20 -20
- package/dist/startup-timing.d.ts +2 -0
- package/dist/startup-timing.js +20 -0
- package/dist/structured-readiness.d.ts +3 -1
- package/dist/structured-readiness.js +21 -1
- package/dist/watch-policy.d.ts +22 -1
- package/dist/watch-policy.js +104 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ if (!watch.ok) {
|
|
|
85
85
|
renderProblem(watch.problem);
|
|
86
86
|
} else {
|
|
87
87
|
const unsubscribeWatch = watch.value.subscribe(
|
|
88
|
-
(change) => console.log(change.
|
|
88
|
+
(change) => console.log(change.kind, change.kind === "record.updated" ? change.path : change.raw),
|
|
89
89
|
renderWatchStatus,
|
|
90
90
|
renderProblem
|
|
91
91
|
);
|
|
@@ -158,9 +158,22 @@ pending mutation, never sent again, even after a reload: `open()` finds the
|
|
|
158
158
|
record's interrupted update (by a digest, so its path is never stored) and
|
|
159
159
|
resumes it as `recovery`. `follow()` refreshes open sessions from the
|
|
160
160
|
watch, follows renames, marks deletions (local text is kept), and re-reads
|
|
161
|
-
everything after a change gap.
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
everything after a change gap. It admits at most four session refreshes at once
|
|
162
|
+
and coalesces repeated events: events during a refresh cause one follow-up read.
|
|
163
|
+
Transient read failures retry three times after 100, 200 and 400 ms; cancellation
|
|
164
|
+
and authorization/input failures do not retry. Reconciliation waits for exact
|
|
165
|
+
write recovery, rather than dropping the missed change. Stopping the last
|
|
166
|
+
follower drops queued refreshes and retries; admitted reads still settle.
|
|
167
|
+
Releasing the last view never cancels a pending save. `snapshot.state` is one of
|
|
168
|
+
`saved`, `unsaved`, `saving`, `conflict`, `recovery`, `error` or `deleted`;
|
|
169
|
+
`snapshot.problem` says why a write, recovery or refresh failed. A failed
|
|
170
|
+
refresh keeps the last accepted record and local edits, but a clean session
|
|
171
|
+
reports `error`, not `saved`, until an authoritative read or acknowledged write
|
|
172
|
+
succeeds (even if the revision is unchanged). No new states or method signatures
|
|
173
|
+
are required; consumers upgrading from earlier SDKs should render `error` for
|
|
174
|
+
read failures as well as write failures. Explicit `session.refresh()` still
|
|
175
|
+
performs one queued read and returns its outcome; only `follow()` schedules
|
|
176
|
+
coalescing and retries.
|
|
164
177
|
The session is an external store, so `externalStore(session)` works with
|
|
165
178
|
`useSyncExternalStore`. Applications whose records live behind their own
|
|
166
179
|
repository construct `MdbaseRecordSession` with an `MdbaseRecordSessionAdapter`
|
|
@@ -198,6 +211,69 @@ versioned capabilities in their manifest; they never maintain a parallel array
|
|
|
198
211
|
of protocol operations. Use `getSnapshot()` and `subscribe()` directly or
|
|
199
212
|
through your framework's external-store integration.
|
|
200
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
|
+
|
|
201
277
|
### Legacy v1 and capability-group v2 declarations
|
|
202
278
|
|
|
203
279
|
The same SDK accepts both declaration versions without translating legacy exact
|
|
@@ -267,6 +343,11 @@ in the background. Route details remain diagnostics; feature UI should read
|
|
|
267
343
|
needs authorization/setup, is temporarily unavailable, or is unsupported by the
|
|
268
344
|
selected authority.
|
|
269
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
|
+
|
|
270
351
|
`MdbaseBrowserSelection` keeps the stable collection ID in
|
|
271
352
|
`?collection=<id>`, preserves unrelated path/query/hash and router state, and
|
|
272
353
|
reports browser back/forward
|
|
@@ -616,8 +697,63 @@ The SDK returns typed outcomes, carries successful mdbase diagnostics alongside
|
|
|
616
697
|
the value, carries revision tokens in typed record results, and accepts
|
|
617
698
|
`ifRevision` on mutations. `describe()` exposes
|
|
618
699
|
JSON Schemas, portable type definitions, canonical collection settings, and
|
|
619
|
-
first-class data contracts. `watch()` resumes from
|
|
700
|
+
first-class data contracts. `watch()` resumes from an authority collection cursor;
|
|
620
701
|
the Connect server does not store the change feed.
|
|
702
|
+
|
|
703
|
+
### Typed changes and description caching
|
|
704
|
+
|
|
705
|
+
`changes()` and `watch()` return the same discriminated `CollectionChange` union.
|
|
706
|
+
Switch on `change.kind`: `record.created`, `record.updated`, `record.deleted`,
|
|
707
|
+
`record.renamed`, `schema.changed`, `config.changed`, `contract.changed`,
|
|
708
|
+
`view.changed`, `file.changed`, `file.put`, `file.removed`, `gap`, or `unknown`.
|
|
709
|
+
Record variants expose `path` (or `from`/`to`) and optional `revision`,
|
|
710
|
+
`previousRevision`, `types`, `previousTypes`, `changedFields`, `bodyChanged`,
|
|
711
|
+
`before`/`after` frontmatter. Metadata is never inferred when an older authority
|
|
712
|
+
omits it. In particular, hosted deletion `types` describes the deleted record,
|
|
713
|
+
whereas local deletion `types` is the empty after-state; use `previousTypes`
|
|
714
|
+
when supplied. `changedFields` preserves authority identifiers (local JSON
|
|
715
|
+
Pointers, hosted top-level keys). A change does not replace an authoritative read.
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
const watched = await connection.watch();
|
|
719
|
+
if (watched.ok) watched.value.subscribe((change) => {
|
|
720
|
+
if (change.kind === "record.updated") {
|
|
721
|
+
console.log(change.path, change.changedFields, change.bodyChanged);
|
|
722
|
+
} else if (change.kind === "schema.changed") {
|
|
723
|
+
console.log("Schema generation", connection.schemaGeneration);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Original `type` and `payload` remain available, and `raw` retains the exact wire
|
|
729
|
+
event. Unrecognized IDs and malformed known payloads become explicit `unknown`
|
|
730
|
+
events; inspect `reason` and `raw` rather than guessing a known variant.
|
|
731
|
+
`mdbase.collection.invalidated` becomes `gap`. A reset page's `events` contains a
|
|
732
|
+
synthetic `reset` variant whose `raw` is the original page, not a wire event.
|
|
733
|
+
The page's `reset` flag and watch's terminal `change_cursor_reset` failure and
|
|
734
|
+
`reset_required` status remain unchanged: refresh state and subscribe again.
|
|
735
|
+
Custom transports/test emitters can use `normalizeCollectionChange(wireEvent)`.
|
|
736
|
+
|
|
737
|
+
`describe()` now caches successful descriptions for at most **60 seconds** per
|
|
738
|
+
connection/client. `describe({ fresh: true })` bypasses the settled cache.
|
|
739
|
+
Concurrent calls share one request; failures are not cached and a failed fresh
|
|
740
|
+
load does not fall back to an old description. Each waiter's cancellation and
|
|
741
|
+
timeout are independent; the shared load uses the connection's request budget.
|
|
742
|
+
Schema/config/contract/view events, unknown events, and feed gaps/reset pages
|
|
743
|
+
observed through this client's `changes()`/`watch()` invalidate the cache before
|
|
744
|
+
publication and advance `schemaGeneration`. Accepted type/setup/view mutations
|
|
745
|
+
also invalidate it. A load spanning an invalidation cannot refill the cache.
|
|
746
|
+
Generation is a local invalidation counter, not an authority revision or a
|
|
747
|
+
cross-connection schema identity. Without an active watch, the 60-second bound
|
|
748
|
+
still applies; use `fresh: true` at correctness-critical refresh boundaries.
|
|
749
|
+
|
|
750
|
+
**Migration:** replace raw-ID regexes and snake-case payload parsing with `kind`
|
|
751
|
+
and typed fields, retaining full refresh/read handling for `unknown`, `gap`, and
|
|
752
|
+
reset. Existing raw consumers can continue using `type`/`payload`. Hand-built
|
|
753
|
+
`CollectionChange` values must go through `normalizeCollectionChange` (or supply
|
|
754
|
+
the typed variant and raw event). Callers requiring every `describe()` to reach
|
|
755
|
+
the authority must now pass `{ fresh: true }`.
|
|
756
|
+
|
|
621
757
|
`queryAll()` treats `timeoutMs` as one deadline for the complete paginated
|
|
622
758
|
query. The caller-driven `queryPages()` iterator instead accepts
|
|
623
759
|
`pageTimeoutMs`, an independent budget for each page, plus a lifetime `signal`.
|
|
@@ -626,7 +762,26 @@ Large hosted pages may omit `meta.totalCount` and return
|
|
|
626
762
|
so page latency is not coupled to a collection-wide count. `meta.hasMore` and the
|
|
627
763
|
opaque cursor remain authoritative for paging. `queryAll()` reports the exact
|
|
628
764
|
number of returned records after it has intentionally consumed the complete
|
|
629
|
-
sequence.
|
|
765
|
+
sequence (unless an explicit `maxResults` cap ends it early).
|
|
766
|
+
|
|
767
|
+
`queryPages()`, `queryAll()` and `executeViewPages()` accept `maxResults` as an
|
|
768
|
+
opt-in total cap; `limit` remains a page-size fallback. `pageSize` sets the initial
|
|
769
|
+
cursor size unless `firstPageSize` overrides it; cursor continuations use that
|
|
770
|
+
pinned size. Abort releases even a paused iterator's cursor without needing
|
|
771
|
+
`return()`. `readMany(paths, { includeBody, frontmatterMode, types })` provides
|
|
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.
|
|
781
|
+
`linksTo(field, path)` builds a guarded server-side `asFile()` predicate (use
|
|
782
|
+
`{multiple: true}` for a link list), not a client resolver. See
|
|
783
|
+
[query helper contracts and consumer migration notes](../../docs/sdk-query-helpers.md).
|
|
784
|
+
|
|
630
785
|
`preflightRename()` and `preflightDelete()` run the canonical collection
|
|
631
786
|
operation without changing records or advancing the change cursor, so an app
|
|
632
787
|
can show authoritative reference impact before asking for confirmation.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { APPLICATION_SETUP_OPERATIONS } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { declaredOperations, validateAuthorizationSelection } from "./application-contract.js";
|
|
3
|
-
import { applicationReadinessContext, collectionSetupInput, publishedStructuredReadiness, verifyStructuredReadiness, verificationKey, verificationValue } from "./structured-readiness.js";
|
|
3
|
+
import { accessRequirementSatisfied, applicationReadinessContext, collectionSetupInput, publishedStructuredReadiness, reviewableTypePackAdoptions, verifyStructuredReadiness, verificationKey, verificationValue } from "./structured-readiness.js";
|
|
4
4
|
import { MdbaseConnectError, connectProblem } from "./errors.js";
|
|
5
5
|
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
6
6
|
import { createRequestBudget, resolveConnectTimeouts, withRequestBudget } from "./request-budget.js";
|
|
7
7
|
import { defaultCallbackUrl } from "./runtime-utils.js";
|
|
8
|
+
import { timeStartupRead } from "./startup-timing.js";
|
|
8
9
|
import { declarationIdFromFamilyIdentity } from "./connect-authorization-helpers.js";
|
|
9
10
|
import { MdbaseSession } from "./session.js";
|
|
10
11
|
export class MdbaseMemoryVerificationStore {
|
|
@@ -129,7 +130,7 @@ export class MdbaseApplicationSession {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
async performStartWithinBudget(options, generation) {
|
|
132
|
-
const registration = await this.connect.register(options);
|
|
133
|
+
const registration = await timeStartupRead("registration", () => this.connect.register(options));
|
|
133
134
|
if (!registration.ok)
|
|
134
135
|
return this.startFailure(registration.problem, generation);
|
|
135
136
|
const manifest = await this.connect.manifest(options);
|
|
@@ -451,9 +452,46 @@ export class MdbaseApplicationSession {
|
|
|
451
452
|
if (!connection || connection.collectionId !== context.collectionId)
|
|
452
453
|
return;
|
|
453
454
|
const initialInput = this.collectionSetupInput();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
const contracts = manifest.requirements?.contracts ?? [];
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
this.readinessController?.abort();
|
|
458
|
+
this.readinessController = controller;
|
|
459
|
+
let readinessFailure = null;
|
|
460
|
+
// Both are authorized reads. Verify live contracts alongside setup, but do
|
|
461
|
+
// not publish ready (or a contract failure) before the setup review decision.
|
|
462
|
+
const verification = context.readiness && contracts.length
|
|
463
|
+
? verifyStructuredReadiness({
|
|
464
|
+
...context, readiness: context.readiness, contracts, connection, controller, options,
|
|
465
|
+
requestMs: this.timeouts.requestMs,
|
|
466
|
+
isCurrent: () => generation === this.verificationGeneration && this.connection() === connection,
|
|
467
|
+
publish: result => { readinessFailure = { ...result, ...context }; }
|
|
468
|
+
})
|
|
469
|
+
: Promise.resolve(true);
|
|
470
|
+
const abortAssessment = () => controller.abort(options?.signal?.reason);
|
|
471
|
+
if (options?.signal?.aborted)
|
|
472
|
+
abortAssessment();
|
|
473
|
+
else
|
|
474
|
+
options?.signal?.addEventListener("abort", abortAssessment, { once: true });
|
|
475
|
+
let outcome;
|
|
476
|
+
let contractsVerified;
|
|
477
|
+
try {
|
|
478
|
+
[outcome, contractsVerified] = await Promise.all([
|
|
479
|
+
timeStartupRead("setup-assessment", () => connection.assessCollectionSetup(initialInput, { ...options, signal: controller.signal })), verification
|
|
480
|
+
]);
|
|
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
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
options?.signal?.removeEventListener("abort", abortAssessment);
|
|
491
|
+
controller.abort();
|
|
492
|
+
if (this.readinessController === controller)
|
|
493
|
+
this.readinessController = undefined;
|
|
494
|
+
}
|
|
457
495
|
if (generation !== this.verificationGeneration)
|
|
458
496
|
return;
|
|
459
497
|
if (!outcome.ok) {
|
|
@@ -494,11 +532,16 @@ export class MdbaseApplicationSession {
|
|
|
494
532
|
this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
|
|
495
533
|
if (context.readiness)
|
|
496
534
|
context.readiness.setup = { state: "current", evidence: [{ source: "authority", fact: "Collection setup assessment is current." }] };
|
|
497
|
-
|
|
535
|
+
if (!contractsVerified) {
|
|
536
|
+
if (readinessFailure)
|
|
537
|
+
this.publish(readinessFailure);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
await this.publishReady(context, generation, options, true);
|
|
498
541
|
}
|
|
499
|
-
async publishReady(context, generation, options) {
|
|
542
|
+
async publishReady(context, generation, options, contractsVerified = false) {
|
|
500
543
|
const contracts = this.requireManifest().requirements?.contracts ?? [];
|
|
501
|
-
if (context.readiness && contracts.length) {
|
|
544
|
+
if (!contractsVerified && context.readiness && contracts.length) {
|
|
502
545
|
const connection = this.connection();
|
|
503
546
|
if (!connection || connection.collectionId !== context.collectionId)
|
|
504
547
|
return;
|
|
@@ -684,25 +727,6 @@ function collectionSetupUpdate(assessment, provisions) {
|
|
|
684
727
|
reason
|
|
685
728
|
};
|
|
686
729
|
}
|
|
687
|
-
function reviewableTypePackAdoptions(assessment) {
|
|
688
|
-
const adoptions = {};
|
|
689
|
-
for (const pack of assessment.typePacks) {
|
|
690
|
-
const resources = Object.fromEntries(pack.resources
|
|
691
|
-
.filter((resource) => resource.action === "conflict"
|
|
692
|
-
&& resource.mode === "managed"
|
|
693
|
-
&& resource.currentDigest !== undefined
|
|
694
|
-
&& resource.installedDigest === undefined)
|
|
695
|
-
.map((resource) => [resource.target, resource.currentDigest]));
|
|
696
|
-
if (Object.keys(resources).length > 0)
|
|
697
|
-
adoptions[pack.desired.id] = resources;
|
|
698
|
-
}
|
|
699
|
-
return adoptions;
|
|
700
|
-
}
|
|
701
|
-
function accessRequirementSatisfied(manifest, connection) {
|
|
702
|
-
return manifest.requirements?.access === "full_collection"
|
|
703
|
-
&& connection.scope.access === "full_collection"
|
|
704
|
-
&& connection.scope.contracts.length === 0;
|
|
705
|
-
}
|
|
706
730
|
function lifecycleProblemMessage(code) {
|
|
707
731
|
if (code === "session_destroyed")
|
|
708
732
|
return "This application session has been destroyed.";
|
|
@@ -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
|
+
}
|