@mdbase-dev/connect 0.1.0-beta.121 → 0.1.0-beta.123
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 +86 -6
- package/dist/application-session.js +45 -27
- 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 +102 -0
- package/dist/collection-client.d.ts +7 -2
- package/dist/collection-client.js +35 -54
- package/dist/connection.d.ts +5 -2
- package/dist/connection.js +4 -3
- package/dist/description-cache.d.ts +14 -0
- package/dist/description-cache.js +73 -0
- package/dist/file-crypto.js +10 -6
- package/dist/file-descriptor.d.ts +12 -0
- package/dist/file-descriptor.js +12 -0
- package/dist/file-sha256.js +3 -3
- package/dist/files.d.ts +2 -11
- package/dist/files.js +36 -30
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/operation-types.d.ts +127 -6
- package/dist/query-pagination-internals.d.ts +9 -0
- package/dist/query-pagination-internals.js +52 -0
- package/dist/query-pagination.js +28 -26
- package/dist/query-predicates.d.ts +11 -0
- package/dist/query-predicates.js +15 -0
- package/dist/read-many.d.ts +6 -0
- package/dist/read-many.js +70 -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/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`
|
|
@@ -616,8 +629,63 @@ The SDK returns typed outcomes, carries successful mdbase diagnostics alongside
|
|
|
616
629
|
the value, carries revision tokens in typed record results, and accepts
|
|
617
630
|
`ifRevision` on mutations. `describe()` exposes
|
|
618
631
|
JSON Schemas, portable type definitions, canonical collection settings, and
|
|
619
|
-
first-class data contracts. `watch()` resumes from
|
|
632
|
+
first-class data contracts. `watch()` resumes from an authority collection cursor;
|
|
620
633
|
the Connect server does not store the change feed.
|
|
634
|
+
|
|
635
|
+
### Typed changes and description caching
|
|
636
|
+
|
|
637
|
+
`changes()` and `watch()` return the same discriminated `CollectionChange` union.
|
|
638
|
+
Switch on `change.kind`: `record.created`, `record.updated`, `record.deleted`,
|
|
639
|
+
`record.renamed`, `schema.changed`, `config.changed`, `contract.changed`,
|
|
640
|
+
`view.changed`, `file.changed`, `file.put`, `file.removed`, `gap`, or `unknown`.
|
|
641
|
+
Record variants expose `path` (or `from`/`to`) and optional `revision`,
|
|
642
|
+
`previousRevision`, `types`, `previousTypes`, `changedFields`, `bodyChanged`,
|
|
643
|
+
`before`/`after` frontmatter. Metadata is never inferred when an older authority
|
|
644
|
+
omits it. In particular, hosted deletion `types` describes the deleted record,
|
|
645
|
+
whereas local deletion `types` is the empty after-state; use `previousTypes`
|
|
646
|
+
when supplied. `changedFields` preserves authority identifiers (local JSON
|
|
647
|
+
Pointers, hosted top-level keys). A change does not replace an authoritative read.
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
const watched = await connection.watch();
|
|
651
|
+
if (watched.ok) watched.value.subscribe((change) => {
|
|
652
|
+
if (change.kind === "record.updated") {
|
|
653
|
+
console.log(change.path, change.changedFields, change.bodyChanged);
|
|
654
|
+
} else if (change.kind === "schema.changed") {
|
|
655
|
+
console.log("Schema generation", connection.schemaGeneration);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Original `type` and `payload` remain available, and `raw` retains the exact wire
|
|
661
|
+
event. Unrecognized IDs and malformed known payloads become explicit `unknown`
|
|
662
|
+
events; inspect `reason` and `raw` rather than guessing a known variant.
|
|
663
|
+
`mdbase.collection.invalidated` becomes `gap`. A reset page's `events` contains a
|
|
664
|
+
synthetic `reset` variant whose `raw` is the original page, not a wire event.
|
|
665
|
+
The page's `reset` flag and watch's terminal `change_cursor_reset` failure and
|
|
666
|
+
`reset_required` status remain unchanged: refresh state and subscribe again.
|
|
667
|
+
Custom transports/test emitters can use `normalizeCollectionChange(wireEvent)`.
|
|
668
|
+
|
|
669
|
+
`describe()` now caches successful descriptions for at most **60 seconds** per
|
|
670
|
+
connection/client. `describe({ fresh: true })` bypasses the settled cache.
|
|
671
|
+
Concurrent calls share one request; failures are not cached and a failed fresh
|
|
672
|
+
load does not fall back to an old description. Each waiter's cancellation and
|
|
673
|
+
timeout are independent; the shared load uses the connection's request budget.
|
|
674
|
+
Schema/config/contract/view events, unknown events, and feed gaps/reset pages
|
|
675
|
+
observed through this client's `changes()`/`watch()` invalidate the cache before
|
|
676
|
+
publication and advance `schemaGeneration`. Accepted type/setup/view mutations
|
|
677
|
+
also invalidate it. A load spanning an invalidation cannot refill the cache.
|
|
678
|
+
Generation is a local invalidation counter, not an authority revision or a
|
|
679
|
+
cross-connection schema identity. Without an active watch, the 60-second bound
|
|
680
|
+
still applies; use `fresh: true` at correctness-critical refresh boundaries.
|
|
681
|
+
|
|
682
|
+
**Migration:** replace raw-ID regexes and snake-case payload parsing with `kind`
|
|
683
|
+
and typed fields, retaining full refresh/read handling for `unknown`, `gap`, and
|
|
684
|
+
reset. Existing raw consumers can continue using `type`/`payload`. Hand-built
|
|
685
|
+
`CollectionChange` values must go through `normalizeCollectionChange` (or supply
|
|
686
|
+
the typed variant and raw event). Callers requiring every `describe()` to reach
|
|
687
|
+
the authority must now pass `{ fresh: true }`.
|
|
688
|
+
|
|
621
689
|
`queryAll()` treats `timeoutMs` as one deadline for the complete paginated
|
|
622
690
|
query. The caller-driven `queryPages()` iterator instead accepts
|
|
623
691
|
`pageTimeoutMs`, an independent budget for each page, plus a lifetime `signal`.
|
|
@@ -626,7 +694,19 @@ Large hosted pages may omit `meta.totalCount` and return
|
|
|
626
694
|
so page latency is not coupled to a collection-wide count. `meta.hasMore` and the
|
|
627
695
|
opaque cursor remain authoritative for paging. `queryAll()` reports the exact
|
|
628
696
|
number of returned records after it has intentionally consumed the complete
|
|
629
|
-
sequence.
|
|
697
|
+
sequence (unless an explicit `maxResults` cap ends it early).
|
|
698
|
+
|
|
699
|
+
`queryPages()`, `queryAll()` and `executeViewPages()` accept `maxResults` as an
|
|
700
|
+
opt-in total cap; `limit` remains a page-size fallback. `pageSize` sets the initial
|
|
701
|
+
cursor size unless `firstPageSize` overrides it; cursor continuations use that
|
|
702
|
+
pinned size. Abort releases even a paused iterator's cursor without needing
|
|
703
|
+
`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.
|
|
706
|
+
`linksTo(field, path)` builds a guarded server-side `asFile()` predicate (use
|
|
707
|
+
`{multiple: true}` for a link list), not a client resolver. See
|
|
708
|
+
[query helper contracts and consumer migration notes](../../docs/sdk-query-helpers.md).
|
|
709
|
+
|
|
630
710
|
`preflightRename()` and `preflightDelete()` run the canonical collection
|
|
631
711
|
operation without changing records or advancing the change cursor, so an app
|
|
632
712
|
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,40 @@ 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 assessed;
|
|
476
|
+
let contractsVerified;
|
|
477
|
+
try {
|
|
478
|
+
[assessed, contractsVerified] = await Promise.all([
|
|
479
|
+
timeStartupRead("setup-assessment", () => connection.assessCollectionSetup(initialInput, { ...options, signal: controller.signal })), verification
|
|
480
|
+
]);
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
options?.signal?.removeEventListener("abort", abortAssessment);
|
|
484
|
+
controller.abort();
|
|
485
|
+
if (this.readinessController === controller)
|
|
486
|
+
this.readinessController = undefined;
|
|
487
|
+
}
|
|
488
|
+
let outcome = assessed;
|
|
457
489
|
if (generation !== this.verificationGeneration)
|
|
458
490
|
return;
|
|
459
491
|
if (!outcome.ok) {
|
|
@@ -494,11 +526,16 @@ export class MdbaseApplicationSession {
|
|
|
494
526
|
this.verificationStore.write(verificationKey(manifest, context.collectionId), verificationValue(manifest));
|
|
495
527
|
if (context.readiness)
|
|
496
528
|
context.readiness.setup = { state: "current", evidence: [{ source: "authority", fact: "Collection setup assessment is current." }] };
|
|
497
|
-
|
|
529
|
+
if (!contractsVerified) {
|
|
530
|
+
if (readinessFailure)
|
|
531
|
+
this.publish(readinessFailure);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await this.publishReady(context, generation, options, true);
|
|
498
535
|
}
|
|
499
|
-
async publishReady(context, generation, options) {
|
|
536
|
+
async publishReady(context, generation, options, contractsVerified = false) {
|
|
500
537
|
const contracts = this.requireManifest().requirements?.contracts ?? [];
|
|
501
|
-
if (context.readiness && contracts.length) {
|
|
538
|
+
if (!contractsVerified && context.readiness && contracts.length) {
|
|
502
539
|
const connection = this.connection();
|
|
503
540
|
if (!connection || connection.collectionId !== context.collectionId)
|
|
504
541
|
return;
|
|
@@ -684,25 +721,6 @@ function collectionSetupUpdate(assessment, provisions) {
|
|
|
684
721
|
reason
|
|
685
722
|
};
|
|
686
723
|
}
|
|
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
724
|
function lifecycleProblemMessage(code) {
|
|
707
725
|
if (code === "session_destroyed")
|
|
708
726
|
return "This application session has been destroyed.";
|