@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.
Files changed (49) hide show
  1. package/README.md +161 -6
  2. package/dist/application-session.js +51 -27
  3. package/dist/authority-features.d.ts +29 -0
  4. package/dist/authority-features.js +84 -0
  5. package/dist/browser/integrity.json +1 -1
  6. package/dist/browser/mdbase-connect.min.js +4 -4
  7. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  8. package/dist/change-events.d.ts +6 -0
  9. package/dist/change-events.js +95 -0
  10. package/dist/collection-client.d.ts +13 -3
  11. package/dist/collection-client.js +98 -140
  12. package/dist/connection.d.ts +12 -2
  13. package/dist/connection.js +57 -117
  14. package/dist/description-cache.d.ts +14 -0
  15. package/dist/description-cache.js +46 -0
  16. package/dist/file-crypto.js +10 -6
  17. package/dist/file-descriptor.d.ts +14 -0
  18. package/dist/file-descriptor.js +29 -0
  19. package/dist/file-metadata.d.ts +21 -0
  20. package/dist/file-metadata.js +112 -0
  21. package/dist/file-sha256.js +3 -3
  22. package/dist/files.d.ts +9 -19
  23. package/dist/files.js +49 -75
  24. package/dist/index.d.ts +5 -2
  25. package/dist/index.js +2 -0
  26. package/dist/local-file-transport.js +18 -16
  27. package/dist/operation-types.d.ts +155 -14
  28. package/dist/query-pagination-internals.d.ts +9 -0
  29. package/dist/query-pagination-internals.js +52 -0
  30. package/dist/query-pagination.d.ts +7 -3
  31. package/dist/query-pagination.js +31 -27
  32. package/dist/query-predicates.d.ts +11 -0
  33. package/dist/query-predicates.js +15 -0
  34. package/dist/query-wire.d.ts +61 -0
  35. package/dist/query-wire.js +110 -0
  36. package/dist/read-many.d.ts +7 -0
  37. package/dist/read-many.js +175 -0
  38. package/dist/record-session.d.ts +3 -2
  39. package/dist/record-session.js +11 -2
  40. package/dist/records.d.ts +15 -2
  41. package/dist/records.js +109 -7
  42. package/dist/saved-view-pagination.js +20 -20
  43. package/dist/startup-timing.d.ts +2 -0
  44. package/dist/startup-timing.js +20 -0
  45. package/dist/structured-readiness.d.ts +3 -1
  46. package/dist/structured-readiness.js +21 -1
  47. package/dist/watch-policy.d.ts +22 -1
  48. package/dist/watch-policy.js +104 -0
  49. package/package.json +2 -2
@@ -1 +1 @@
1
- sha384-Nv0Du948fR/kSC/QXiny8mY1SZ2akUFBT7QmsPb3irgnUGNN0LgsWDubjxozn5cU
1
+ sha384-23aqw9c9S7YPXCd5zLVLjUFjKy3QggldsE+mDLmLzzRkxaOnQxOkLxoQ2j1oujFh
@@ -0,0 +1,6 @@
1
+ import { type CollectionChange as WireChange, type CollectionChangesPage as WirePage } from "@mdbase-dev/connect-protocol";
2
+ import type { CollectionChange, CollectionChangesPage } from "./operation-types.js";
3
+ /** Normalize authority events once, without inferring metadata missing on older authorities. */
4
+ export declare function normalizeCollectionChange(raw: WireChange): CollectionChange;
5
+ export declare function normalizeChangesPage(raw: WirePage): CollectionChangesPage;
6
+ export declare function invalidatesDescription(change: CollectionChange): boolean;
@@ -0,0 +1,95 @@
1
+ import { CHANGE_EVENT_KINDS } from "@mdbase-dev/connect-protocol";
2
+ import { clientFileDescriptor, validFileDescriptor } from "./file-descriptor.js";
3
+ const RECORD_FIELDS = Object.entries({
4
+ path: string, from: string, to: string, revision: string, previous_revision: string,
5
+ types: strings, previous_types: strings, changed_fields: strings, body_changed: boolean,
6
+ before: object, after: object
7
+ });
8
+ const RESOURCE_FIELDS = Object.entries({ path: string, name: string, revision: string, previous_revision: string, reason: string });
9
+ const REMOVED_FILE_FIELDS = Object.entries({ previous_path: string, revision: string });
10
+ /** Normalize authority events once, without inferring metadata missing on older authorities. */
11
+ export function normalizeCollectionChange(raw) {
12
+ const base = { cursor: raw.cursor, type: raw.type, occurredAt: raw.occurred_at, payload: raw.payload, raw };
13
+ const unknown = (reason) => ({ ...base, kind: "unknown", reason });
14
+ const kind = Object.hasOwn(CHANGE_EVENT_KINDS, raw.type)
15
+ ? CHANGE_EVENT_KINDS[raw.type]
16
+ : undefined;
17
+ if (!kind)
18
+ return unknown("unrecognized_type");
19
+ const p = raw.payload;
20
+ if (!object(p))
21
+ return unknown("invalid_payload");
22
+ if (kind.startsWith("record.")) {
23
+ if (!validFields(p, RECORD_FIELDS))
24
+ return unknown("invalid_payload");
25
+ const data = p;
26
+ const metadata = {
27
+ ...(data.revision == null ? {} : { revision: data.revision }),
28
+ ...(data.previous_revision == null ? {} : { previousRevision: data.previous_revision }),
29
+ ...(data.types == null ? {} : { types: data.types }),
30
+ ...(data.previous_types == null ? {} : { previousTypes: data.previous_types }),
31
+ ...(data.changed_fields == null ? {} : { changedFields: data.changed_fields }),
32
+ ...(data.body_changed == null ? {} : { bodyChanged: data.body_changed }),
33
+ ...(data.before == null ? {} : { before: data.before }),
34
+ ...(data.after == null ? {} : { after: data.after })
35
+ };
36
+ if (kind === "record.renamed") {
37
+ return string(p.from) && string(p.to)
38
+ ? { ...base, ...metadata, kind, from: p.from, to: p.to }
39
+ : unknown("invalid_payload");
40
+ }
41
+ if (kind === "record.created" || kind === "record.updated" || kind === "record.deleted") {
42
+ return string(p.path) ? { ...base, ...metadata, kind, path: p.path } : unknown("invalid_payload");
43
+ }
44
+ }
45
+ if (kind === "file.put") {
46
+ return validFileDescriptor(p.file)
47
+ ? { ...base, kind, file: clientFileDescriptor(p.file) }
48
+ : unknown("invalid_payload");
49
+ }
50
+ if (kind === "file.removed") {
51
+ if (!string(p.file_id) || !validFields(p, REMOVED_FILE_FIELDS))
52
+ return unknown("invalid_payload");
53
+ const data = p;
54
+ return { ...base, kind, fileId: p.file_id,
55
+ ...(data.previous_path == null ? {} : { previousPath: data.previous_path }),
56
+ ...(data.revision == null ? {} : { revision: data.revision }) };
57
+ }
58
+ if (!validFields(p, RESOURCE_FIELDS))
59
+ return unknown("invalid_payload");
60
+ const data = p;
61
+ if (kind === "gap")
62
+ return { ...base, kind, ...(data.reason == null ? {} : { reason: data.reason }) };
63
+ const metadata = {
64
+ ...(data.revision == null ? {} : { revision: data.revision }),
65
+ ...(data.previous_revision == null ? {} : { previousRevision: data.previous_revision })
66
+ };
67
+ if (kind === "file.changed")
68
+ return string(p.path) ? { ...base, ...metadata, kind, path: p.path } : unknown("invalid_payload");
69
+ if (kind === "schema.changed" || kind === "config.changed" || kind === "contract.changed" || kind === "view.changed") {
70
+ return { ...base, ...metadata, kind,
71
+ ...(data.path == null ? {} : { path: data.path }),
72
+ ...(data.name == null ? {} : { name: data.name }) };
73
+ }
74
+ throw new Error(`Unhandled change kind: ${kind}`);
75
+ }
76
+ export function normalizeChangesPage(raw) {
77
+ return {
78
+ events: raw.reset ? [{ kind: "reset", type: "reset", cursor: raw.cursor, occurredAt: null, payload: {}, raw }] : raw.events.map(normalizeCollectionChange),
79
+ cursor: raw.cursor,
80
+ hasMore: raw.has_more,
81
+ reset: raw.reset
82
+ };
83
+ }
84
+ export function invalidatesDescription(change) {
85
+ return change.kind === "schema.changed" || change.kind === "config.changed"
86
+ || change.kind === "contract.changed" || change.kind === "view.changed"
87
+ || change.kind === "gap" || change.kind === "reset" || change.kind === "unknown";
88
+ }
89
+ function string(value) { return typeof value === "string"; }
90
+ function boolean(value) { return typeof value === "boolean"; }
91
+ function strings(value) { return Array.isArray(value) && value.every(string); }
92
+ function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
93
+ function validFields(value, fields) {
94
+ return fields.every(([key, valid]) => value[key] == null || valid(value[key]));
95
+ }
@@ -1,6 +1,6 @@
1
1
  import type { CollectionOperation, CollectionTypeDocument, JsonObject } from "@mdbase-dev/connect-protocol";
2
2
  import { type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome } from "./outcomes.js";
3
- import type { ChangesInput, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, AssessCollectionSetupInput, AssessTypePackInput, ApplyCollectionSetupInput, ApplyTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseCollectionTransport, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, QueryAllOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, WatchOptions } from "./operation-types.js";
3
+ import type { ChangesInput, DescribeOptions, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, AssessCollectionSetupInput, AssessTypePackInput, ApplyCollectionSetupInput, ApplyTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseCollectionTransport, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, QueryAllOptions, QueryInput, QueryMetadataInput, QueryMetadataRecord, QueryMetadataResult, QueryMetadataPage, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadManyOptions, ReadManyResult, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, WatchOptions } from "./operation-types.js";
4
4
  /**
5
5
  * Typed collection operations independent of OAuth, HTTP, or storage.
6
6
  *
@@ -10,13 +10,23 @@ import type { ChangesInput, CollectionChange, CollectionChangesPage, CollectionD
10
10
  export declare class MdbaseCollectionClient<Frontmatter extends JsonObject = JsonObject> {
11
11
  private readonly transport;
12
12
  private readonly requestTimeoutMs;
13
- constructor(transport: MdbaseCollectionTransport, requestTimeoutMs?: number | null);
13
+ private readonly supportsAuthorityFeature?;
14
+ private readonly descriptions;
15
+ private lastSchemaCursor;
16
+ /** Local cache generation, not an authority revision or cross-connection identity. */
17
+ get schemaGeneration(): number;
18
+ constructor(transport: MdbaseCollectionTransport, requestTimeoutMs?: number | null, supportsAuthorityFeature?: ((id: string, options?: ConnectRequestOptions) => Promise<ConnectOutcome<boolean>>) | undefined);
14
19
  operation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<ConnectOutcome<Result>>;
15
- describe(options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
20
+ describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
16
21
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
17
22
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
23
+ readMany(paths: readonly string[], options?: ReadManyOptions): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
24
+ query(input: QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
18
25
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
26
+ query(input: QueryInput | QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter> | QueryMetadataResult, CollectionQueryProblemCode>>;
27
+ queryPages(input: QueryMetadataInput, options?: QueryPagesOptions<JsonObject, QueryMetadataRecord>): AsyncGenerator<ConnectOutcome<QueryMetadataPage, CollectionQueryProblemCode>>;
19
28
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
29
+ queryAll(input: QueryMetadataInput, options?: QueryAllOptions<JsonObject, QueryMetadataRecord>): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
20
30
  queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
21
31
  listViews(options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewList, CollectionReadProblemCode>>;
22
32
  executeView(input: ExecuteViewInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;
@@ -1,9 +1,13 @@
1
1
  import { abortableDelay } from "./async.js";
2
+ import { invalidatesDescription, normalizeChangesPage } from "./change-events.js";
3
+ import { CollectionDescriptionCache } from "./description-cache.js";
2
4
  import { coordinatedQueryPages } from "./query-pagination.js";
5
+ import { readMany as readManyRecords } from "./read-many.js";
6
+ import { wireDataContractIdentity, wireQueryInput, wireQueryRecord, wireQueryResult } from "./query-wire.js";
3
7
  import { coordinatedSavedViewPages } from "./saved-view-pagination.js";
4
8
  import { MdbaseConnectError, connectError, connectProblem, operationProblem } from "./errors.js";
5
- import { COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_DESCRIPTION_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, COLLECTION_QUERY_PROBLEM_CODES, COLLECTION_READ_PROBLEM_CODES, COLLECTION_TYPE_PROBLEM_CODES, ALL_CONNECT_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
6
- import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason } from "./request-budget.js";
9
+ import { COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, COLLECTION_QUERY_PROBLEM_CODES, COLLECTION_READ_PROBLEM_CODES, COLLECTION_TYPE_PROBLEM_CODES, ALL_CONNECT_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
10
+ import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason, requestOptionsWithinBudget, withRequestBudget } from "./request-budget.js";
7
11
  import { watchRetryPolicy } from "./watch-policy.js";
8
12
  /**
9
13
  * Typed collection operations independent of OAuth, HTTP, or storage.
@@ -14,44 +18,69 @@ import { watchRetryPolicy } from "./watch-policy.js";
14
18
  export class MdbaseCollectionClient {
15
19
  transport;
16
20
  requestTimeoutMs;
17
- constructor(transport, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
21
+ supportsAuthorityFeature;
22
+ descriptions;
23
+ lastSchemaCursor = -1;
24
+ /** Local cache generation, not an authority revision or cross-connection identity. */
25
+ get schemaGeneration() { return this.descriptions.schemaGeneration; }
26
+ constructor(transport, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, supportsAuthorityFeature) {
18
27
  this.transport = transport;
19
28
  this.requestTimeoutMs = requestTimeoutMs;
29
+ this.supportsAuthorityFeature = supportsAuthorityFeature;
30
+ this.descriptions = new CollectionDescriptionCache(transport, requestTimeoutMs);
20
31
  }
21
32
  operation(operation, input, options) {
22
33
  return captureConnectOutcome(() => this.transport.operation(operation, input, options), ALL_CONNECT_PROBLEM_CODES);
23
34
  }
24
- async describe(options) {
25
- const outcome = await this.rawOperation("describe", {}, COLLECTION_DESCRIPTION_PROBLEM_CODES, options);
26
- return mapOutcome(outcome, wireCollectionDescription);
35
+ describe(options) {
36
+ return this.descriptions.describe(options);
27
37
  }
28
38
  async changes(input = {}, options) {
29
39
  const outcome = await this.rawOperation("changes", input, COLLECTION_CHANGES_PROBLEM_CODES, options);
30
- return mapOutcome(outcome, wireChangesPage);
40
+ const mapped = mapOutcome(outcome, normalizeChangesPage);
41
+ if (mapped.ok) {
42
+ const invalidations = mapped.value.events.filter(invalidatesDescription);
43
+ if (mapped.value.reset || invalidations.some((event) => event.cursor > this.lastSchemaCursor)) {
44
+ this.descriptions.invalidate();
45
+ this.lastSchemaCursor = mapped.value.cursor;
46
+ }
47
+ }
48
+ return mapped;
31
49
  }
32
50
  async read(input, options) {
33
51
  const outcome = await this.envelopeOperation("read", wireReadInput(input), COLLECTION_READ_PROBLEM_CODES, options);
34
52
  return mapOutcome(outcome, wireRecordDocument);
35
53
  }
54
+ readMany(paths, options = {}) {
55
+ return readManyRecords((input, requestOptions) => this.queryAll(input, requestOptions), (selected, includeBody, requestOptions) => this.envelopeOperation("read", { paths: selected, include_body: includeBody, include_document: false }, COLLECTION_READ_PROBLEM_CODES, requestOptions), this.supportsAuthorityFeature, paths, options, this.requestTimeoutMs);
56
+ }
36
57
  async query(input = {}, options) {
37
- const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, options);
38
- return mapOutcome(outcome, ({ results, meta }) => ({
39
- results: results.map(wireQueryRecord),
40
- ...(meta ? {
41
- meta: {
42
- ...(typeof meta.total_count === "number" ? { totalCount: meta.total_count } : {}),
43
- ...(meta.total_count_outcome === undefined
44
- ? {}
45
- : { totalCountOutcome: meta.total_count_outcome }),
46
- hasMore: meta.has_more,
47
- ...(meta.cursor ? { cursor: meta.cursor } : {}),
48
- ...(meta.snapshot ? { snapshot: meta.snapshot } : {})
49
- }
50
- } : {})
51
- }));
58
+ if (input.output !== undefined && input.output !== "metadata") {
59
+ return connectFailure(connectProblem("invalid_request", "Unknown query output mode."));
60
+ }
61
+ if (input.output !== "metadata") {
62
+ const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, options);
63
+ return mapOutcome(outcome, value => wireQueryResult(value, undefined));
64
+ }
65
+ const captured = await captureConnectOutcome(() => withRequestBudget(options, this.requestTimeoutMs, async (budget) => {
66
+ const requestOptions = requestOptionsWithinBudget({ ...options, timeoutMs: null }, budget);
67
+ if (input.includeBody)
68
+ throw connectError("invalid_request", "Metadata queries cannot include a body.");
69
+ const support = await this.supportsAuthorityFeature?.("query-metadata-v1", requestOptions);
70
+ if (support && !support.ok)
71
+ throw new MdbaseConnectError(support.problem);
72
+ if (!support?.value)
73
+ throw connectError("unsupported_operation", "This authority does not support metadata queries. Use an ordinary query on legacy authorities.");
74
+ const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, requestOptions);
75
+ return mapOutcome(outcome, value => wireQueryResult(value, input.output));
76
+ }), COLLECTION_QUERY_PROBLEM_CODES);
77
+ return captured.ok ? captured.value : captured;
52
78
  }
53
79
  async *queryPages(input = {}, options = {}) {
54
- yield* coordinatedQueryPages((pageInput, pageOptions) => this.query(pageInput, pageOptions), (cursor) => this.releaseQueryCursor(cursor), input, options);
80
+ yield* coordinatedQueryPages((pageInput, pageOptions) => this.query(pageInput, pageOptions), (cursor) => this.releaseQueryCursor(cursor, input.output), input,
81
+ // The overload ties callback row types to input.output; the wire parser
82
+ // rejects mode mismatch before pagination can deliver a page.
83
+ options);
55
84
  }
56
85
  async queryAll(input = {}, options = {}) {
57
86
  const budget = createRequestBudget(options, this.requestTimeoutMs);
@@ -59,14 +88,18 @@ export class MdbaseCollectionClient {
59
88
  let finalPage;
60
89
  const diagnostics = [];
61
90
  try {
62
- for await (const outcome of this.queryPages(input, {
91
+ const pageOptions = {
63
92
  firstPageSize: options.firstPageSize,
64
93
  pageSize: options.pageSize,
94
+ maxResults: options.maxResults,
65
95
  signal: budget.signal,
66
96
  pageTimeoutMs: null,
67
- coordination: options.coordination,
68
- onProgress: options.onProgress
69
- })) {
97
+ coordination: options.coordination
98
+ };
99
+ const pages = input.output === "metadata"
100
+ ? this.queryPages(input, { ...pageOptions, onProgress: options.onProgress })
101
+ : this.queryPages(input, { ...pageOptions, onProgress: options.onProgress });
102
+ for await (const outcome of pages) {
70
103
  if (!outcome.ok)
71
104
  return outcome;
72
105
  const page = outcome.value;
@@ -76,14 +109,19 @@ export class MdbaseCollectionClient {
76
109
  }
77
110
  if (budget.signal.aborted)
78
111
  throw requestAbortReason(budget.signal);
79
- return connectSuccess({
80
- results,
81
- meta: {
82
- totalCount: finalPage?.meta?.totalCount ?? results.length,
83
- hasMore: finalPage ? !finalPage.complete : false,
84
- ...(finalPage?.snapshot ? { snapshot: finalPage.snapshot } : {})
85
- }
86
- }, diagnostics);
112
+ const meta = finalPage ? {
113
+ ...(finalPage.meta?.totalCount === undefined
114
+ ? (!finalPage.meta?.hasMore ? { totalCount: results.length } : {})
115
+ : { totalCount: finalPage.meta.totalCount }),
116
+ ...(finalPage.meta?.hasMore && finalPage.meta.totalCountOutcome ? { totalCountOutcome: finalPage.meta.totalCountOutcome } : {}),
117
+ hasMore: finalPage.meta?.hasMore ?? false,
118
+ ...(finalPage.snapshot ? { snapshot: finalPage.snapshot } : {})
119
+ } : undefined;
120
+ // Every page is parsed in the requested mode, never converted from full
121
+ // rows to revision-required metadata (or from partial rows to documents).
122
+ return input.output === "metadata"
123
+ ? connectSuccess({ output: "metadata", results: results, ...(meta ? { meta } : {}) }, diagnostics)
124
+ : connectSuccess({ results: results, ...(meta ? { meta } : {}) }, diagnostics);
87
125
  }
88
126
  catch (error) {
89
127
  if (error instanceof MdbaseConnectError) {
@@ -283,9 +321,19 @@ export class MdbaseCollectionClient {
283
321
  rawOperation(operation, input, allowedCodes, options) {
284
322
  return captureConnectOutcome(() => this.transport.operation(operation, input, options), allowedCodes);
285
323
  }
286
- async releaseQueryCursor(cursor) {
324
+ async releaseQueryCursor(cursor, output) {
287
325
  try {
288
- await this.envelopeOperation("query", { release_cursor: cursor }, COLLECTION_QUERY_PROBLEM_CODES, { timeoutMs: 2_000, coordination: { coalesce: false } });
326
+ await withRequestBudget({ timeoutMs: 2_000 }, null, async (budget) => {
327
+ const options = requestOptionsWithinBudget({ signal: budget.signal, timeoutMs: null, coordination: { coalesce: false } }, budget);
328
+ if (output) {
329
+ // A supplied/stale cursor can reach cleanup even when the page was
330
+ // rejected before dispatch. Never send metadata release to an old route.
331
+ const support = await this.supportsAuthorityFeature?.("query-metadata-v1", options);
332
+ if (!support?.ok || !support.value)
333
+ return;
334
+ }
335
+ await this.envelopeOperation("query", { release_cursor: cursor, ...(output ? { output } : {}) }, COLLECTION_QUERY_PROBLEM_CODES, options);
336
+ });
289
337
  }
290
338
  catch {
291
339
  // Lease expiry is bounded by the authority; cleanup never masks query results.
@@ -299,11 +347,23 @@ export class MdbaseCollectionClient {
299
347
  if (!envelope.valid) {
300
348
  return connectFailure(operationProblem(envelope));
301
349
  }
350
+ if (["create_type", "update_type", "apply_type_pack", "apply_collection_setup", "create_view_source", "update_view_source", "delete_view_source"].includes(operation)) {
351
+ this.descriptions.invalidate();
352
+ }
302
353
  return connectSuccess(envelope.result, envelope.diagnostics);
303
354
  }
304
355
  }
305
356
  function mapOutcome(outcome, map) {
306
- return outcome.ok ? connectSuccess(map(outcome.value), outcome.diagnostics) : outcome;
357
+ if (!outcome.ok)
358
+ return outcome;
359
+ try {
360
+ return connectSuccess(map(outcome.value), outcome.diagnostics);
361
+ }
362
+ catch (error) {
363
+ if (error instanceof MdbaseConnectError)
364
+ return connectFailure(error.problem);
365
+ throw error;
366
+ }
307
367
  }
308
368
  function wireReadInput(input) {
309
369
  return {
@@ -312,47 +372,6 @@ function wireReadInput(input) {
312
372
  ...(input.includeDocument === undefined ? {} : { include_document: input.includeDocument })
313
373
  };
314
374
  }
315
- function wireQueryInput(input) {
316
- return {
317
- ...(input.types ? { types: input.types } : {}),
318
- ...(input.timezone ? { timezone: input.timezone } : {}),
319
- ...(input.context ? { context: input.context } : {}),
320
- ...(input.projections ? {
321
- projections: Object.fromEntries(Object.entries(input.projections).map(([name, projection]) => [
322
- name,
323
- { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
324
- ]))
325
- } : {}),
326
- ...(input.where ? { where: input.where } : {}),
327
- ...(input.select ? {
328
- select: input.select.map((selection) => typeof selection === "string"
329
- ? selection
330
- : {
331
- name: selection.name,
332
- expr: selection.expression,
333
- ...(selection.label ? { label: selection.label } : {}),
334
- ...(selection.description ? { description: selection.description } : {})
335
- })
336
- } : {}),
337
- ...(input.orderBy ? { order_by: input.orderBy } : {}),
338
- ...(input.groupBy ? { group_by: input.groupBy } : {}),
339
- ...(input.summaryFunctions ? {
340
- summary_functions: Object.fromEntries(Object.entries(input.summaryFunctions).map(([name, projection]) => [
341
- name,
342
- { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
343
- ]))
344
- } : {}),
345
- ...(input.summaries ? { summaries: input.summaries } : {}),
346
- ...(input.limit === undefined ? {} : { limit: input.limit }),
347
- ...(input.offset === undefined ? {} : { offset: input.offset }),
348
- ...(input.pagination ? { pagination: input.pagination } : {}),
349
- ...(input.cursor ? { cursor: input.cursor } : {}),
350
- ...(input.snapshot ? { snapshot: input.snapshot } : {}),
351
- ...(input.includeBody === undefined ? {} : { include_body: input.includeBody }),
352
- ...(input.frontmatterMode ? { frontmatter_mode: input.frontmatterMode } : {}),
353
- ...(input.contract ? { contract: input.contract } : {})
354
- };
355
- }
356
375
  function wireCreateInput(input) {
357
376
  return {
358
377
  ...(input.path ? { path: input.path } : {}),
@@ -500,23 +519,6 @@ function wireContractSetupChoice(choice) {
500
519
  ...(choice.binding ? { binding: choice.binding } : {})
501
520
  };
502
521
  }
503
- function wireDataContractIdentity(value) {
504
- return {
505
- id: value.id,
506
- version: value.version,
507
- digest: value.digest,
508
- type: value.type,
509
- implementationDigest: value.implementation_digest
510
- };
511
- }
512
- function wireQueryRecord(value) {
513
- const { effective_frontmatter, contract, ...record } = value;
514
- return {
515
- ...record,
516
- ...(effective_frontmatter === undefined ? {} : { effectiveFrontmatter: effective_frontmatter }),
517
- ...(contract ? { contract: wireDataContractIdentity(contract) } : {})
518
- };
519
- }
520
522
  function wireRecordDocument(value) {
521
523
  const { effective_frontmatter, contract, ...record } = value;
522
524
  return {
@@ -635,47 +637,3 @@ function wireCollectionSetupReceipt(value) {
635
637
  cleanupDeferred: value.cleanup_deferred
636
638
  };
637
639
  }
638
- function wireChange(value) {
639
- return {
640
- cursor: value.cursor,
641
- type: value.type,
642
- occurredAt: value.occurred_at,
643
- payload: value.payload
644
- };
645
- }
646
- function wireChangesPage(value) {
647
- return {
648
- events: value.events.map(wireChange),
649
- cursor: value.cursor,
650
- hasMore: value.has_more,
651
- reset: value.reset
652
- };
653
- }
654
- function wireCollectionDescription(value) {
655
- return {
656
- protocolVersion: value.protocol_version,
657
- collectionId: value.collection_id,
658
- displayName: value.display_name,
659
- specVersion: value.spec_version,
660
- operations: value.operations,
661
- changeCursor: value.change_cursor,
662
- types: value.types,
663
- contracts: value.contracts.map((contract) => ({
664
- contractType: contract.contract_type,
665
- id: contract.id,
666
- version: contract.version,
667
- digest: contract.digest,
668
- schema: contract.schema,
669
- ...(contract.binding_schema ? { bindingSchema: contract.binding_schema } : {}),
670
- implementations: contract.implementations.map((implementation) => ({
671
- typeName: implementation.type_name,
672
- typeVersion: implementation.type_version,
673
- ...(implementation.type_path ? { typePath: implementation.type_path } : {}),
674
- digest: implementation.digest,
675
- fields: implementation.fields,
676
- ...(implementation.binding ? { binding: implementation.binding } : {})
677
- }))
678
- })),
679
- ...(value.configuration ? { configuration: value.configuration } : {})
680
- };
681
- }
@@ -1,6 +1,7 @@
1
1
  import { MdbasePeopleClient } from "./people-client.js";
2
2
  import type { CollectionOperation, CollectionTypeDocument, FileCapability, GrantScope, JsonObject } from "@mdbase-dev/connect-protocol";
3
3
  import { type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "./application-contract.js";
4
+ import { MdbaseCollectionClient } from "./collection-client.js";
4
5
  import { type ConnectionTransportInternals } from "./connection-transport.js";
5
6
  import type { DirectAccessStatus, MdbaseAuthorizationCapabilities, MdbaseConnectionInfo, MdbaseConnectionRoute, MdbaseSyncConnection } from "./connection-types.js";
6
7
  import type { GrantKeyStore } from "./crypto.js";
@@ -8,7 +9,7 @@ import { MdbaseFileClient } from "./files.js";
8
9
  import { MdbaseRecords } from "./records.js";
9
10
  import { type Application } from "./internal-types.js";
10
11
  import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.js";
11
- import type { ChangesInput, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, ApplyCollectionSetupInput, ApplyTypePackInput, AssessCollectionSetupInput, AssessTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryAllOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
12
+ import type { ChangesInput, CollectionChangesPage, DescribeOptions, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, ApplyCollectionSetupInput, ApplyTypePackInput, AssessCollectionSetupInput, AssessTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryAllOptions, QueryInput, QueryMetadataInput, QueryMetadataRecord, QueryMetadataResult, QueryMetadataPage, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
12
13
  import { type AuthorizationProblemCode, type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome, type DirectAccessProblemCode, type NotificationProblemCode, type RegistrationProblemCode } from "./outcomes.js";
13
14
  import type { MdbaseDeviceAuthorization } from "./connection-types.js";
14
15
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
@@ -75,10 +76,12 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
75
76
  private readonly collectionClient;
76
77
  private readonly notifications;
77
78
  private readonly transport;
79
+ private readonly authorityFeatures;
78
80
  readonly files: MdbaseFileClient;
79
81
  /** Editable record sessions shared by every view of this connection. */
80
82
  readonly records: MdbaseRecords<Frontmatter>;
81
83
  readonly people: MdbasePeopleClient;
84
+ readonly readMany: MdbaseCollectionClient<Frontmatter>["readMany"];
82
85
  private readonly connectionListeners;
83
86
  constructor(internals: MdbaseConnectionInternals<Frontmatter>, collectionId: string);
84
87
  get displayName(): string;
@@ -87,6 +90,9 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
87
90
  get fileCapability(): FileCapability | null;
88
91
  get directAccess(): DirectAccessStatus;
89
92
  get route(): MdbaseConnectionRoute;
93
+ /** Implementation support for this connection lifetime; not an authorization check. */
94
+ supportsAuthorityFeature(id: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<boolean>>;
95
+ get authorityCapabilities(): readonly string[];
90
96
  register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
91
97
  info(): MdbaseConnectionInfo | null;
92
98
  authorizationCapabilities(requiredOperations?: CollectionOperation[]): MdbaseAuthorizationCapabilities;
@@ -123,11 +129,15 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
123
129
  unregisterNativeNotifications(options?: ConnectRequestOptions): Promise<ConnectOutcome<void, NotificationProblemCode>>;
124
130
  unregisterNotifications(serviceWorker?: ServiceWorkerRegistration, options?: ConnectRequestOptions): Promise<ConnectOutcome<void, NotificationProblemCode>>;
125
131
  forget(): void;
126
- describe(options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
132
+ get schemaGeneration(): number;
133
+ describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
127
134
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
128
135
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
136
+ query(input: QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
129
137
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
138
+ queryPages(input: QueryMetadataInput, options?: QueryPagesOptions<JsonObject, QueryMetadataRecord>): AsyncGenerator<ConnectOutcome<QueryMetadataPage, CollectionQueryProblemCode>>;
130
139
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
140
+ queryAll(input: QueryMetadataInput, options?: QueryAllOptions<JsonObject, QueryMetadataRecord>): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
131
141
  queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
132
142
  listViews(options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewList, CollectionReadProblemCode>>;
133
143
  executeView(input: ExecuteViewInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;