@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
package/dist/files.js CHANGED
@@ -1,10 +1,12 @@
1
+ import { clientFileDescriptor } from "./file-descriptor.js";
1
2
  import { FILE_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
- import { abortableDelay } from "./async.js";
3
+ import { listFiles, statFile } from "./file-metadata.js";
4
+ import { connectSuccess } from "./outcomes.js";
3
5
  import { MdbaseConnectError, connectError } from "./errors.js";
4
6
  import { IncrementalSha256 } from "./file-sha256.js";
5
7
  import { BinaryPartReader } from "./file-stream-source.js";
6
- import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency, validPageSize } from "./file-transfer-internals.js";
7
- import { createRequestBudget, resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
8
+ import { MAX_BUFFERED_DOWNLOAD_BYTES, MAX_OBJECT_ATTEMPTS, SHA256_DIGEST, UUID, browserObjectHeaders, chunkLength, hashBlob, mapConcurrent, normalizeFileError, requirePreparedPart, requireTransferSession, requireTransferStatus, retryChunk, sourceBlob, throwIfAborted, uploadBodyBytes, uploadBodyLength, validConcurrency } from "./file-transfer-internals.js";
9
+ import { resolveConnectTimeouts, withCooperativeRequestBudget } from "./request-budget.js";
8
10
  /** Ergonomic facade over resumable, authority-specific file delivery. */
9
11
  export class MdbaseFileClient {
10
12
  capability;
@@ -12,54 +14,21 @@ export class MdbaseFileClient {
12
14
  framed;
13
15
  hosted;
14
16
  timeouts;
15
- constructor(capability, request, framed, hosted, timeouts = resolveConnectTimeouts()) {
17
+ supportsAuthorityFeature;
18
+ constructor(capability, request, framed, hosted, timeouts = resolveConnectTimeouts(), supportsAuthorityFeature = async () => connectSuccess(false)) {
16
19
  this.capability = capability;
17
20
  this.request = request;
18
21
  this.framed = framed;
19
22
  this.hosted = hosted;
20
23
  this.timeouts = timeouts;
24
+ this.supportsAuthorityFeature = supportsAuthorityFeature;
21
25
  }
22
- async *list(options = {}) {
23
- const budget = createRequestBudget(options, this.timeouts.fileIndexMs);
24
- const signal = budget.signal;
25
- try {
26
- this.requireAction("list");
27
- let after;
28
- do {
29
- throwIfAborted(signal);
30
- const query = new URLSearchParams({
31
- protocol_version: String(FILE_PROTOCOL_VERSION),
32
- ...(options.folder ? { folder: options.folder } : {}),
33
- ...(after ? { after } : {}),
34
- ...(options.pageSize ? { limit: String(validPageSize(options.pageSize)) } : {})
35
- });
36
- let page;
37
- while (true) {
38
- try {
39
- page = await this.request("GET", `?${query.toString()}`, undefined, signal);
40
- break;
41
- }
42
- catch (error) {
43
- const normalized = normalizeFileError(error);
44
- if (normalized.code !== "file_index_warming")
45
- throw normalized;
46
- options.onIndexing?.();
47
- await abortableDelay(500, signal);
48
- }
49
- }
50
- if (page.protocol_version !== 1 || page.type !== "files_page" || !Array.isArray(page.files)) {
51
- throw connectError("invalid_operation_response", "The authority returned an invalid file page.");
52
- }
53
- for (const file of page.files) {
54
- throwIfAborted(signal);
55
- yield clientFileDescriptor(file);
56
- }
57
- after = page.next;
58
- } while (after);
59
- }
60
- finally {
61
- budget.dispose();
62
- }
26
+ list(options = {}) {
27
+ return listFiles(this.capability, this.request, options, this.timeouts.fileIndexMs);
28
+ }
29
+ /** Current metadata lookup; requires file list approval, not file byte access. */
30
+ stat(target, options = {}) {
31
+ return statFile(this.capability, this.request, this.supportsAuthorityFeature, options => this.list(options), target, options, this.timeouts.fileIndexMs);
63
32
  }
64
33
  async upload(path, source, options = {}) {
65
34
  return withCooperativeRequestBudget(options, this.timeouts.uploadMs, (budget) => this.uploadWithinBudget(path, source, { ...options, signal: budget.signal, timeoutMs: null }));
@@ -220,23 +189,9 @@ export class MdbaseFileClient {
220
189
  }
221
190
  }
222
191
  async download(file, options = {}) {
223
- if (file.size > MAX_BUFFERED_DOWNLOAD_BYTES) {
224
- throw connectError("invalid_request", `download() buffers at most ${MAX_BUFFERED_DOWNLOAD_BYTES} bytes; use downloadStream() for larger files.`);
225
- }
226
- const stream = await this.downloadStream(file, options);
227
- const chunks = [];
228
- const reader = stream.getReader();
229
- while (true) {
230
- const result = await reader.read();
231
- if (result.done)
232
- break;
233
- chunks.push(result.value);
234
- }
235
- return new Blob(chunks.map((chunk) => {
236
- const copy = new Uint8Array(chunk.byteLength);
237
- copy.set(chunk);
238
- return copy.buffer;
239
- }), { type: file.mediaType ?? "" });
192
+ const bytes = await this.downloadBytes(file, options);
193
+ // downloadBytes owns a full, non-shared ArrayBuffer; Blob snapshots it.
194
+ return new Blob([bytes.buffer], { type: file.mediaType ?? "" });
240
195
  }
241
196
  /** Download and verify a file with network backpressure and bounded buffering. */
242
197
  async downloadStream(file, options = {}) {
@@ -416,7 +371,38 @@ export class MdbaseFileClient {
416
371
  });
417
372
  }
418
373
  async downloadBytes(file, options = {}) {
419
- return new Uint8Array(await (await this.download(file, options)).arrayBuffer());
374
+ if (file.size > MAX_BUFFERED_DOWNLOAD_BYTES) {
375
+ throw connectError("invalid_request", `download() buffers at most ${MAX_BUFFERED_DOWNLOAD_BYTES} bytes; use downloadStream() for larger files.`);
376
+ }
377
+ const stream = await this.downloadStream(file, options);
378
+ const bytes = new Uint8Array(file.size);
379
+ const reader = stream.getReader();
380
+ let offset = 0;
381
+ try {
382
+ while (true) {
383
+ const result = await reader.read();
384
+ // Reading through EOF is essential: hosted streams verify their digest
385
+ // only after the final range ends, not when the last bytes arrive.
386
+ if (result.done)
387
+ break;
388
+ if (result.value.byteLength > bytes.byteLength - offset) {
389
+ throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
390
+ }
391
+ bytes.set(result.value, offset);
392
+ offset += result.value.byteLength;
393
+ }
394
+ if (offset !== bytes.byteLength) {
395
+ throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
396
+ }
397
+ return bytes;
398
+ }
399
+ catch (error) {
400
+ await reader.cancel().catch(() => undefined);
401
+ throw error;
402
+ }
403
+ finally {
404
+ reader.releaseLock();
405
+ }
420
406
  }
421
407
  async move(file, path, options = {}) {
422
408
  return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.moveWithinBudget(file, path, { ...options, signal: budget.signal, timeoutMs: null }));
@@ -585,15 +571,3 @@ export class MdbaseFileClient {
585
571
  await this.request("DELETE", `transfers/${encodeURIComponent(transferId)}`).catch(() => undefined);
586
572
  }
587
573
  }
588
- function clientFileDescriptor(file) {
589
- return {
590
- fileId: file.file_id,
591
- path: file.path,
592
- revision: file.revision,
593
- contentDigest: file.content_digest,
594
- size: file.size,
595
- ...(file.media_type ? { mediaType: file.media_type } : {}),
596
- mediaClass: file.media_class,
597
- modifiedAt: file.modified_at
598
- };
599
- }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { normalizeCollectionChange } from "./change-events.js";
1
2
  export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
2
3
  export type { MdbaseApplicationSessionOptions, MdbaseApplicationVerificationStore, MdbaseDefinitionUpdate, MdbaseCollectionSetupUpdate, MdbaseApplicationSessionSnapshot } from "./application-session.js";
3
4
  export type { MdbaseConnectOptions, MdbaseConnectTimeouts, MdbaseFrontmatter } from "./connect-options.js";
@@ -18,11 +19,13 @@ export type { MdbaseRecordResolution, MdbaseRecordSessionOptions, MdbaseRecordSe
18
19
  export { MdbasePeopleClient, PERSON_CONTRACT, sameIdentity } from "./people-client.js";
19
20
  export type { CurrentAccount, InvalidPersonRecord, PeopleDirectory, PeopleDirectoryOptions, PersonRecord, PersonResolution } from "./people-client.js";
20
21
  export type { AccountIdentity, AccountProfile, CollectionMemberProfile, ApplicationPeopleRequirement, PeoplePermission } from "@mdbase-dev/connect-protocol";
21
- export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
22
+ export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileStatTarget, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
22
23
  export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
23
24
  export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
24
25
  export type { ConnectSuccess, ConnectFailure, ConnectOutcome, CommonOperationProblemCode, RegistrationProblemCode, AuthorizationProblemCode, NotificationProblemCode, DirectAccessProblemCode, SessionProblemCode, CollectionSetupProblemCode, CollectionDescriptionProblemCode, CollectionReadProblemCode, CollectionQueryProblemCode, CollectionMutationProblemCode, CollectionChangesProblemCode, CollectionTypeProblemCode } from "./outcomes.js";
25
- export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
26
+ export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, ReadManyOptions, ReadManyRecord, ReadManyEntry, ReadManyBatchError, ReadManyResult, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryMetadataInput, QueryMetadataRecord, QueryMetadataResult, QueryMetadataPage, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, RecordChangeMetadata, DescribeOptions, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
27
+ export { linksTo } from "./query-predicates.js";
28
+ export type { LinksToOptions } from "./query-predicates.js";
26
29
  export { externalStore } from "./external-store.js";
27
30
  export type { MdbaseExternalStore } from "./external-store.js";
28
31
  export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { normalizeCollectionChange } from "./change-events.js";
1
2
  export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
2
3
  export { MdbaseConnect } from "./mdbase-connect.js";
3
4
  export { MdbaseConnection } from "./connection.js";
@@ -8,6 +9,7 @@ export { MdbaseRecords } from "./records.js";
8
9
  export { MdbaseRecordSession } from "./record-session.js";
9
10
  export { MdbasePeopleClient, PERSON_CONTRACT, sameIdentity } from "./people-client.js";
10
11
  export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
12
+ export { linksTo } from "./query-predicates.js";
11
13
  export { externalStore } from "./external-store.js";
12
14
  export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
13
15
  export { MDBASE_RECORD_CREATED_CONTRACT, MDBASE_TIMER_FIRED_CONTRACT, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
@@ -1,4 +1,4 @@
1
- import { decodeFileFrame, FILE_PROTOCOL_VERSION, FILE_TRANSFER_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
1
+ import { decodeFileFrame, isMutatingOperation, FILE_PROTOCOL_VERSION, FILE_TRANSFER_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
2
  import { decryptRelayResponse, encryptRelayRequest } from "./crypto.js";
3
3
  import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
4
4
  import { GrantFileTransferCipher } from "./file-crypto.js";
@@ -51,7 +51,7 @@ export class LocalFileTransport {
51
51
  if (response)
52
52
  this.options.onDirectUnavailable();
53
53
  }
54
- if (triedDirect && method === "GET") {
54
+ if (triedDirect && !isMutatingOperation("file_control", controlInput)) {
55
55
  token = token.expiresAt > Date.now() + 30_000
56
56
  ? token
57
57
  : await this.options.refreshAuthorization(signal);
@@ -81,7 +81,7 @@ export class LocalFileTransport {
81
81
  return this.controlAttempt(token, method, path, input, signal, freshReadRetried, connectorBusyRetries + 1, leases);
82
82
  }
83
83
  if (error.code === "fresh_request_required"
84
- && method === "GET"
84
+ && !isMutatingOperation("file_control", localFileControlInput(method, path, input))
85
85
  && !freshReadRetried) {
86
86
  return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
87
87
  }
@@ -94,7 +94,7 @@ export class LocalFileTransport {
94
94
  const decrypted = await decryptRelayResponse(this.options.keyStore, token.keyHandle, relayBinding(token), encryptedRequest, encryptedResponse);
95
95
  if (!decrypted.ok
96
96
  && decrypted.problem.code === "fresh_request_required"
97
- && method === "GET"
97
+ && !isMutatingOperation("file_control", localFileControlInput(method, path, input))
98
98
  && !freshReadRetried) {
99
99
  return this.controlAttempt(token, method, path, input, signal, true, connectorBusyRetries, leases);
100
100
  }
@@ -407,19 +407,21 @@ function localFileControlInput(method, path, input) {
407
407
  return transferControl("abort_file_transfer", segments[1]);
408
408
  }
409
409
  if (method === "POST" && isRecord(input)) {
410
- const expectedType = path === "uploads"
411
- ? "open_file_upload"
412
- : path === "downloads"
413
- ? "open_file_download"
414
- : segments.length === 3 && segments[0] === "uploads" && segments[2] === "commit"
415
- ? "commit_file_upload"
416
- : segments.length === 2 && segments[1] === "move"
417
- && input.file_id === decodeURIComponent(segments[0])
418
- ? "move_file"
419
- : segments.length === 2 && segments[1] === "delete"
410
+ const expectedType = path === "stat"
411
+ ? "stat_file"
412
+ : path === "uploads"
413
+ ? "open_file_upload"
414
+ : path === "downloads"
415
+ ? "open_file_download"
416
+ : segments.length === 3 && segments[0] === "uploads" && segments[2] === "commit"
417
+ ? "commit_file_upload"
418
+ : segments.length === 2 && segments[1] === "move"
420
419
  && input.file_id === decodeURIComponent(segments[0])
421
- ? "delete_file"
422
- : null;
420
+ ? "move_file"
421
+ : segments.length === 2 && segments[1] === "delete"
422
+ && input.file_id === decodeURIComponent(segments[0])
423
+ ? "delete_file"
424
+ : null;
423
425
  if (expectedType && input.type === expectedType)
424
426
  return { ...input };
425
427
  }
@@ -1,4 +1,4 @@
1
- import type { CollectionOperation, ConnectProblem, ContractRequirement, JsonObject, MutationOperationIdentifier, TypePackProvision } from "@mdbase-dev/connect-protocol";
1
+ import type { CollectionChange as WireCollectionChange, CollectionChangesPage as WireCollectionChangesPage, CollectionOperation, ConnectProblem, ContractRequirement, JsonObject, MutationOperationIdentifier, TypePackProvision } from "@mdbase-dev/connect-protocol";
2
2
  export interface MdbaseDesiredTimer {
3
3
  /** Stable within the timer namespace. */
4
4
  id: string;
@@ -53,6 +53,8 @@ export interface DataContractViewIdentity {
53
53
  }
54
54
  export interface QueryRecord<Frontmatter extends JsonObject = JsonObject> {
55
55
  path: string;
56
+ /** Exact-source token when the authority supports query-record-revisions-v1. */
57
+ revision?: string;
56
58
  frontmatter?: Frontmatter;
57
59
  effectiveFrontmatter?: Frontmatter;
58
60
  body?: string;
@@ -64,6 +66,41 @@ export interface QueryRecord<Frontmatter extends JsonObject = JsonObject> {
64
66
  values?: JsonObject;
65
67
  contract?: DataContractViewIdentity;
66
68
  }
69
+ /** Query-shaped projection; qualified document batches supply exact-source revisions. */
70
+ export type ReadManyRecord<Frontmatter extends JsonObject = JsonObject> = QueryRecord<Frontmatter>;
71
+ export interface ReadManyOptions extends ConnectRequestOptions {
72
+ includeBody?: boolean;
73
+ frontmatterMode?: QueryInput["frontmatterMode"];
74
+ /** Restrict matching records to these raw collection types, not a contract view. */
75
+ types?: string[];
76
+ /** Unique paths per batch, default 100; range 1..1,000. Document authorities cap at 100. */
77
+ batchSize?: number;
78
+ /** Independent batches in flight, default 4; range 1..4. Cursor pages stay serial. */
79
+ concurrency?: number;
80
+ }
81
+ export type ReadManyEntry<Frontmatter extends JsonObject = JsonObject> = {
82
+ status: "found";
83
+ path: string;
84
+ record: ReadManyRecord<Frontmatter>;
85
+ } | {
86
+ status: "missing";
87
+ path: string;
88
+ } | {
89
+ status: "error";
90
+ path: string;
91
+ batch: number;
92
+ };
93
+ export interface ReadManyBatchError {
94
+ batch: number;
95
+ paths: string[];
96
+ failure: import("./outcomes.js").ConnectFailure<import("./outcomes.js").CollectionQueryProblemCode>;
97
+ }
98
+ export interface ReadManyResult<Frontmatter extends JsonObject = JsonObject> {
99
+ /** One entry per input path, in input order, including duplicates. */
100
+ results: ReadManyEntry<Frontmatter>[];
101
+ /** Batch failures cover the entire batch; semantic item failures cover only their paths. */
102
+ errors: ReadManyBatchError[];
103
+ }
67
104
  export interface RecordDocument<Frontmatter extends JsonObject = JsonObject> {
68
105
  path: string;
69
106
  revision: string;
@@ -107,6 +144,8 @@ export interface CollectionContractDescriptor {
107
144
  implementations: CollectionContractImplementationDescriptor[];
108
145
  }
109
146
  export interface CollectionDescription {
147
+ /** Immutable authority implementation features; never a permission claim. */
148
+ authorityCapabilities?: readonly string[];
110
149
  protocolVersion: 1;
111
150
  collectionId: string;
112
151
  displayName: string;
@@ -139,6 +178,7 @@ export interface QuerySummary {
139
178
  }
140
179
  /** Application-facing form of the canonical mdbase v0.3 query schema. */
141
180
  export interface QueryInput {
181
+ output?: never;
142
182
  /**
143
183
  * Queries using a semantic contract view accept only `types`, `timezone`,
144
184
  * pagination, `frontmatterMode`, and `contract`; filter normalized fields in
@@ -172,8 +212,26 @@ export interface QueryInput {
172
212
  /** Select one exact semantic contract/provider view for this query. */
173
213
  contract?: DataContractSelector;
174
214
  }
175
- export interface QueryResult<Record extends JsonObject = JsonObject> {
176
- results: Array<QueryRecord<Record>>;
215
+ export type QueryMetadataInput = Omit<QueryInput, "output" | "includeBody"> & {
216
+ output: "metadata";
217
+ includeBody?: false;
218
+ };
219
+ /** Partial metadata is not a record document; revision and selected values are required. */
220
+ export interface QueryMetadataRecord {
221
+ path: string;
222
+ types: string[];
223
+ revision: string;
224
+ values: JsonObject;
225
+ contract?: DataContractViewIdentity;
226
+ }
227
+ export type QueryMetadataResult = QueryResult<JsonObject, QueryMetadataRecord> & {
228
+ output: "metadata";
229
+ };
230
+ export type QueryMetadataPage = QueryPage<JsonObject, QueryMetadataRecord> & {
231
+ output: "metadata";
232
+ };
233
+ export interface QueryResult<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
234
+ results: Row[];
177
235
  meta?: {
178
236
  /** Exact total when it is available within the page summary budget. */
179
237
  totalCount?: number;
@@ -188,26 +246,30 @@ export interface QueryResult<Record extends JsonObject = JsonObject> {
188
246
  snapshot?: string;
189
247
  };
190
248
  }
191
- export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
192
- /** Initial page size. Cursor authorities pin this size for the query lifetime. */
249
+ export interface QueryPagesOptions<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
250
+ /** Initial page size. Cursor continuations use this pinned size. */
193
251
  firstPageSize?: number;
194
- /** Also sets the initial size unless firstPageSize is explicit; authorities may cap it. */
252
+ /** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
195
253
  pageSize?: number;
254
+ /** Total rows to deliver, not a page size. Zero makes no data requests. */
255
+ maxResults?: number;
196
256
  signal?: AbortSignal;
197
257
  /** Independent budget for each page requested by this caller-driven iterator. */
198
258
  pageTimeoutMs?: number | null;
199
259
  coordination?: RequestCoordinationOptions;
200
- onProgress?: (page: QueryPage<Record>) => void;
260
+ onProgress?: (page: QueryPage<Record, Row>) => void;
201
261
  }
202
- export interface QueryAllOptions<Record extends JsonObject = JsonObject> extends ConnectRequestOptions {
203
- /** Initial page size. Cursor authorities pin this size for the query lifetime. */
262
+ export interface QueryAllOptions<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> extends ConnectRequestOptions {
263
+ /** Initial page size. Cursor continuations use this pinned size. */
204
264
  firstPageSize?: number;
205
- /** Also sets the initial size unless firstPageSize is explicit; authorities may cap it. */
265
+ /** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
206
266
  pageSize?: number;
207
- onProgress?: (page: QueryPage<Record>) => void;
267
+ /** Total rows to return, independent of page size. */
268
+ maxResults?: number;
269
+ onProgress?: (page: QueryPage<Record, Row>) => void;
208
270
  }
209
- export interface QueryPage<Record extends JsonObject = JsonObject> {
210
- results: QueryResult<Record>["results"];
271
+ export interface QueryPage<Record extends JsonObject = JsonObject, Row = QueryRecord<Record>> {
272
+ results: QueryResult<Record, Row>["results"];
211
273
  meta?: QueryResult<Record>["meta"];
212
274
  page: number;
213
275
  offset: number;
@@ -404,8 +466,12 @@ export interface ExecuteViewInput {
404
466
  render?: boolean;
405
467
  }
406
468
  export interface SavedViewPagesOptions<Frontmatter extends JsonObject = JsonObject> {
469
+ /** Initial page size, pinned for cursor continuations. */
407
470
  firstPageSize?: number;
471
+ /** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
408
472
  pageSize?: number;
473
+ /** Total rows to deliver, not a page size. Zero makes no data requests. */
474
+ maxResults?: number;
409
475
  signal?: AbortSignal;
410
476
  /** Independent budget for each page requested by this caller-driven iterator. */
411
477
  pageTimeoutMs?: number | null;
@@ -685,12 +751,87 @@ export interface MdbaseWatchSubscription {
685
751
  subscribe(listener: (change: CollectionChange) => void, onStatus?: (status: WatchStatus) => void, onProblem?: (problem: ConnectProblem) => void): () => void;
686
752
  close(): void;
687
753
  }
688
- export interface CollectionChange {
754
+ export interface DescribeOptions extends ConnectRequestOptions {
755
+ /** Bypass the settled description cache (concurrent requests still share a load). */
756
+ fresh?: boolean;
757
+ }
758
+ interface ChangeBase {
689
759
  cursor: number;
760
+ /** Original authority event ID. Prefer kind for application logic. */
690
761
  type: string;
691
762
  occurredAt: string;
692
763
  payload: JsonObject;
764
+ /** Unmodified wire event, including authority-specific metadata. */
765
+ raw: WireCollectionChange;
766
+ }
767
+ export interface RecordChangeMetadata {
768
+ revision?: string;
769
+ previousRevision?: string;
770
+ types?: string[];
771
+ previousTypes?: string[];
772
+ /** Authority field identifiers: local JSON Pointers, hosted top-level keys. Preserved, not guessed. */
773
+ changedFields?: string[];
774
+ bodyChanged?: boolean;
775
+ /** Hosted authorities may supply frontmatter; local authorities need not. */
776
+ before?: JsonObject;
777
+ after?: JsonObject;
778
+ }
779
+ interface ResourceChangeBase extends ChangeBase {
780
+ path?: string;
781
+ name?: string;
782
+ revision?: string;
783
+ previousRevision?: string;
693
784
  }
785
+ export type CollectionChange = (ChangeBase & RecordChangeMetadata & {
786
+ kind: "record.created";
787
+ path: string;
788
+ }) | (ChangeBase & RecordChangeMetadata & {
789
+ kind: "record.updated";
790
+ path: string;
791
+ }) | (ChangeBase & RecordChangeMetadata & {
792
+ kind: "record.deleted";
793
+ path: string;
794
+ }) | (ChangeBase & RecordChangeMetadata & {
795
+ kind: "record.renamed";
796
+ from: string;
797
+ to: string;
798
+ }) | (ResourceChangeBase & {
799
+ kind: "schema.changed";
800
+ }) | (ResourceChangeBase & {
801
+ kind: "config.changed";
802
+ }) | (ResourceChangeBase & {
803
+ kind: "contract.changed";
804
+ }) | (ResourceChangeBase & {
805
+ kind: "view.changed";
806
+ }) | (ChangeBase & {
807
+ kind: "file.changed";
808
+ path: string;
809
+ revision?: string;
810
+ previousRevision?: string;
811
+ }) | (ChangeBase & {
812
+ kind: "file.put";
813
+ file: import("./file-descriptor.js").CollectionFileDescriptor;
814
+ }) | (ChangeBase & {
815
+ kind: "file.removed";
816
+ fileId: string;
817
+ previousPath?: string;
818
+ revision?: string;
819
+ }) | (ChangeBase & {
820
+ kind: "gap";
821
+ reason?: string;
822
+ }) | (ChangeBase & {
823
+ kind: "unknown";
824
+ reason: "unrecognized_type" | "invalid_payload";
825
+ })
826
+ /** Synthesized from a reset page, not a new authority event ID. Watch still terminates with change_cursor_reset. */
827
+ | {
828
+ kind: "reset";
829
+ cursor: number;
830
+ type: "reset";
831
+ occurredAt: null;
832
+ payload: JsonObject;
833
+ raw: WireCollectionChangesPage;
834
+ };
694
835
  export interface CollectionChangesPage {
695
836
  events: CollectionChange[];
696
837
  cursor: number;
@@ -0,0 +1,9 @@
1
+ /** Owns the latest single-use cursor, including while an iterator is paused at yield. */
2
+ export declare function queryCursorLease(release: (cursor: string) => Promise<void>, signal?: AbortSignal, initialCursor?: string): {
3
+ startRequest(): void;
4
+ finishRequest(nextCursor?: string): void;
5
+ dispose(): void;
6
+ };
7
+ export declare function resultCap(value?: number): number;
8
+ export declare function nonNegativeInteger(value: unknown, fallback: number): number;
9
+ export declare function positiveInteger(value: unknown, fallback: number): number;
@@ -0,0 +1,52 @@
1
+ /** Owns the latest single-use cursor, including while an iterator is paused at yield. */
2
+ export function queryCursorLease(release, signal, initialCursor) {
3
+ let cursor = initialCursor;
4
+ let inFlight = false;
5
+ const close = () => {
6
+ if (!cursor)
7
+ return;
8
+ const owned = cursor;
9
+ cursor = undefined;
10
+ // Collection cleanup has an independent bounded budget, never the data signal.
11
+ void release(owned).catch(() => undefined);
12
+ };
13
+ const abort = () => {
14
+ // A consumed token can rotate while the request settles. Release the returned
15
+ // token instead; a paused iterator has no request and closes immediately.
16
+ if (!inFlight)
17
+ close();
18
+ };
19
+ signal?.addEventListener("abort", abort, { once: true });
20
+ return {
21
+ startRequest() { inFlight = true; },
22
+ finishRequest(nextCursor) {
23
+ if (nextCursor)
24
+ cursor = nextCursor;
25
+ inFlight = false;
26
+ if (signal?.aborted)
27
+ close();
28
+ },
29
+ dispose() {
30
+ signal?.removeEventListener("abort", abort);
31
+ close();
32
+ }
33
+ };
34
+ }
35
+ export function resultCap(value) {
36
+ if (value === undefined)
37
+ return Infinity;
38
+ if (!Number.isSafeInteger(value) || value < 0) {
39
+ throw new TypeError("maxResults must be a non-negative safe integer.");
40
+ }
41
+ return value;
42
+ }
43
+ export function nonNegativeInteger(value, fallback) {
44
+ return typeof value === "number" && Number.isFinite(value)
45
+ ? Math.max(0, Math.floor(value))
46
+ : fallback;
47
+ }
48
+ export function positiveInteger(value, fallback) {
49
+ return typeof value === "number" && Number.isFinite(value) && value >= 1
50
+ ? Math.floor(value)
51
+ : fallback;
52
+ }
@@ -1,6 +1,10 @@
1
1
  import type { JsonObject } from "@mdbase-dev/connect-protocol";
2
2
  import { type CollectionQueryProblemCode, type ConnectOutcome } from "./outcomes.js";
3
- import type { ConnectRequestOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult } from "./operation-types.js";
4
- type QueryOperation<Frontmatter extends JsonObject> = (input: QueryInput, options?: ConnectRequestOptions) => Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
5
- export declare function coordinatedQueryPages<Frontmatter extends JsonObject>(query: QueryOperation<Frontmatter>, releaseQueryCursor: (cursor: string) => Promise<void>, input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
3
+ import type { ConnectRequestOptions, QueryInput, QueryMetadataInput, QueryRecord, QueryPage, QueryPagesOptions, QueryResult } from "./operation-types.js";
4
+ type QueryOperation<Frontmatter extends JsonObject, Row> = (input: QueryInput | QueryMetadataInput, options?: ConnectRequestOptions) => Promise<ConnectOutcome<QueryResult<Frontmatter, Row> & {
5
+ output?: "metadata";
6
+ }, CollectionQueryProblemCode>>;
7
+ export declare function coordinatedQueryPages<Frontmatter extends JsonObject, Row = QueryRecord<Frontmatter>>(query: QueryOperation<Frontmatter, Row>, releaseQueryCursor: (cursor: string) => Promise<void>, input?: QueryInput | QueryMetadataInput, options?: QueryPagesOptions<Frontmatter, Row>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter, Row> & {
8
+ output?: "metadata";
9
+ }, CollectionQueryProblemCode>>;
6
10
  export {};