@mdbase-dev/connect 0.1.0-beta.123 → 0.1.0-beta.125

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.
@@ -1 +1 @@
1
- sha384-oeF346B54GbocYns1w4tBu6zmyDtayo5iL/ujiP4qtf4pk1NsES2mMQj/0lIYmBb
1
+ sha384-JJDoYra3MuCFJxyktOaw/6TQwPZCx8ZC2YcJhJxOZqXF8xU67X+tyXiEm5KKZwcf
@@ -1,5 +1,5 @@
1
1
  import { CHANGE_EVENT_KINDS } from "@mdbase-dev/connect-protocol";
2
- import { clientFileDescriptor } from "./file-descriptor.js";
2
+ import { clientFileDescriptor, validFileDescriptor } from "./file-descriptor.js";
3
3
  const RECORD_FIELDS = Object.entries({
4
4
  path: string, from: string, to: string, revision: string, previous_revision: string,
5
5
  types: strings, previous_types: strings, changed_fields: strings, body_changed: boolean,
@@ -43,7 +43,7 @@ export function normalizeCollectionChange(raw) {
43
43
  }
44
44
  }
45
45
  if (kind === "file.put") {
46
- return clientFileDescriptorIsValid(p.file)
46
+ return validFileDescriptor(p.file)
47
47
  ? { ...base, kind, file: clientFileDescriptor(p.file) }
48
48
  : unknown("invalid_payload");
49
49
  }
@@ -93,10 +93,3 @@ function object(value) { return value !== null && typeof value === "object" && !
93
93
  function validFields(value, fields) {
94
94
  return fields.every(([key, valid]) => value[key] == null || valid(value[key]));
95
95
  }
96
- function clientFileDescriptorIsValid(value) {
97
- return object(value) && string(value.file_id) && string(value.path) && string(value.revision)
98
- && string(value.content_digest) && value.content_digest.startsWith("sha256:")
99
- && typeof value.size === "number" && Number.isSafeInteger(value.size) && value.size >= 0
100
- && ["image", "audio", "video", "pdf", "other"].includes(value.media_class)
101
- && string(value.modified_at) && (value.media_type == null || string(value.media_type));
102
- }
@@ -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, 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, 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";
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,18 +10,23 @@ import type { ChangesInput, DescribeOptions, CollectionChange, CollectionChanges
10
10
  export declare class MdbaseCollectionClient<Frontmatter extends JsonObject = JsonObject> {
11
11
  private readonly transport;
12
12
  private readonly requestTimeoutMs;
13
+ private readonly supportsAuthorityFeature?;
13
14
  private readonly descriptions;
14
15
  private lastSchemaCursor;
15
16
  /** Local cache generation, not an authority revision or cross-connection identity. */
16
17
  get schemaGeneration(): number;
17
- constructor(transport: MdbaseCollectionTransport, requestTimeoutMs?: number | null);
18
+ constructor(transport: MdbaseCollectionTransport, requestTimeoutMs?: number | null, supportsAuthorityFeature?: ((id: string, options?: ConnectRequestOptions) => Promise<ConnectOutcome<boolean>>) | undefined);
18
19
  operation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<ConnectOutcome<Result>>;
19
20
  describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
20
21
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
21
22
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
22
23
  readMany(paths: readonly string[], options?: ReadManyOptions): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
24
+ query(input: QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
23
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>>;
24
28
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
29
+ queryAll(input: QueryMetadataInput, options?: QueryAllOptions<JsonObject, QueryMetadataRecord>): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
25
30
  queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
26
31
  listViews(options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewList, CollectionReadProblemCode>>;
27
32
  executeView(input: ExecuteViewInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;
@@ -2,11 +2,12 @@ import { abortableDelay } from "./async.js";
2
2
  import { invalidatesDescription, normalizeChangesPage } from "./change-events.js";
3
3
  import { CollectionDescriptionCache } from "./description-cache.js";
4
4
  import { coordinatedQueryPages } from "./query-pagination.js";
5
- import { queryReadMany } from "./read-many.js";
5
+ import { readMany as readManyRecords } from "./read-many.js";
6
+ import { wireDataContractIdentity, wireQueryInput, wireQueryRecord, wireQueryResult } from "./query-wire.js";
6
7
  import { coordinatedSavedViewPages } from "./saved-view-pagination.js";
7
8
  import { MdbaseConnectError, connectError, connectProblem, operationProblem } from "./errors.js";
8
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";
9
- import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason } from "./request-budget.js";
10
+ import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason, requestOptionsWithinBudget, withRequestBudget } from "./request-budget.js";
10
11
  import { watchRetryPolicy } from "./watch-policy.js";
11
12
  /**
12
13
  * Typed collection operations independent of OAuth, HTTP, or storage.
@@ -17,13 +18,15 @@ import { watchRetryPolicy } from "./watch-policy.js";
17
18
  export class MdbaseCollectionClient {
18
19
  transport;
19
20
  requestTimeoutMs;
21
+ supportsAuthorityFeature;
20
22
  descriptions;
21
23
  lastSchemaCursor = -1;
22
24
  /** Local cache generation, not an authority revision or cross-connection identity. */
23
25
  get schemaGeneration() { return this.descriptions.schemaGeneration; }
24
- constructor(transport, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
26
+ constructor(transport, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, supportsAuthorityFeature) {
25
27
  this.transport = transport;
26
28
  this.requestTimeoutMs = requestTimeoutMs;
29
+ this.supportsAuthorityFeature = supportsAuthorityFeature;
27
30
  this.descriptions = new CollectionDescriptionCache(transport, requestTimeoutMs);
28
31
  }
29
32
  operation(operation, input, options) {
@@ -49,27 +52,35 @@ export class MdbaseCollectionClient {
49
52
  return mapOutcome(outcome, wireRecordDocument);
50
53
  }
51
54
  readMany(paths, options = {}) {
52
- return queryReadMany((input, requestOptions) => this.queryAll(input, requestOptions), paths, options, this.requestTimeoutMs);
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);
53
56
  }
54
57
  async query(input = {}, options) {
55
- const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, options);
56
- return mapOutcome(outcome, ({ results, meta }) => ({
57
- results: results.map(wireQueryRecord),
58
- ...(meta ? {
59
- meta: {
60
- ...(typeof meta.total_count === "number" ? { totalCount: meta.total_count } : {}),
61
- ...(meta.total_count_outcome === undefined
62
- ? {}
63
- : { totalCountOutcome: meta.total_count_outcome }),
64
- hasMore: meta.has_more,
65
- ...(meta.cursor ? { cursor: meta.cursor } : {}),
66
- ...(meta.snapshot ? { snapshot: meta.snapshot } : {})
67
- }
68
- } : {})
69
- }));
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;
70
78
  }
71
79
  async *queryPages(input = {}, options = {}) {
72
- 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);
73
84
  }
74
85
  async queryAll(input = {}, options = {}) {
75
86
  const budget = createRequestBudget(options, this.requestTimeoutMs);
@@ -77,15 +88,18 @@ export class MdbaseCollectionClient {
77
88
  let finalPage;
78
89
  const diagnostics = [];
79
90
  try {
80
- for await (const outcome of this.queryPages(input, {
91
+ const pageOptions = {
81
92
  firstPageSize: options.firstPageSize,
82
93
  pageSize: options.pageSize,
83
94
  maxResults: options.maxResults,
84
95
  signal: budget.signal,
85
96
  pageTimeoutMs: null,
86
- coordination: options.coordination,
87
- onProgress: options.onProgress
88
- })) {
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) {
89
103
  if (!outcome.ok)
90
104
  return outcome;
91
105
  const page = outcome.value;
@@ -95,17 +109,19 @@ export class MdbaseCollectionClient {
95
109
  }
96
110
  if (budget.signal.aborted)
97
111
  throw requestAbortReason(budget.signal);
98
- return connectSuccess({
99
- results,
100
- ...(finalPage ? { meta: {
101
- ...(finalPage.meta?.totalCount === undefined
102
- ? (!finalPage.meta?.hasMore ? { totalCount: results.length } : {})
103
- : { totalCount: finalPage.meta.totalCount }),
104
- ...(finalPage.meta?.hasMore && finalPage.meta.totalCountOutcome ? { totalCountOutcome: finalPage.meta.totalCountOutcome } : {}),
105
- hasMore: finalPage.meta?.hasMore ?? false,
106
- ...(finalPage.snapshot ? { snapshot: finalPage.snapshot } : {})
107
- } } : {})
108
- }, 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);
109
125
  }
110
126
  catch (error) {
111
127
  if (error instanceof MdbaseConnectError) {
@@ -305,9 +321,19 @@ export class MdbaseCollectionClient {
305
321
  rawOperation(operation, input, allowedCodes, options) {
306
322
  return captureConnectOutcome(() => this.transport.operation(operation, input, options), allowedCodes);
307
323
  }
308
- async releaseQueryCursor(cursor) {
324
+ async releaseQueryCursor(cursor, output) {
309
325
  try {
310
- 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
+ });
311
337
  }
312
338
  catch {
313
339
  // Lease expiry is bounded by the authority; cleanup never masks query results.
@@ -328,7 +354,16 @@ export class MdbaseCollectionClient {
328
354
  }
329
355
  }
330
356
  function mapOutcome(outcome, map) {
331
- 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
+ }
332
367
  }
333
368
  function wireReadInput(input) {
334
369
  return {
@@ -337,47 +372,6 @@ function wireReadInput(input) {
337
372
  ...(input.includeDocument === undefined ? {} : { include_document: input.includeDocument })
338
373
  };
339
374
  }
340
- function wireQueryInput(input) {
341
- return {
342
- ...(input.types ? { types: input.types } : {}),
343
- ...(input.timezone ? { timezone: input.timezone } : {}),
344
- ...(input.context ? { context: input.context } : {}),
345
- ...(input.projections ? {
346
- projections: Object.fromEntries(Object.entries(input.projections).map(([name, projection]) => [
347
- name,
348
- { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
349
- ]))
350
- } : {}),
351
- ...(input.where ? { where: input.where } : {}),
352
- ...(input.select ? {
353
- select: input.select.map((selection) => typeof selection === "string"
354
- ? selection
355
- : {
356
- name: selection.name,
357
- expr: selection.expression,
358
- ...(selection.label ? { label: selection.label } : {}),
359
- ...(selection.description ? { description: selection.description } : {})
360
- })
361
- } : {}),
362
- ...(input.orderBy ? { order_by: input.orderBy } : {}),
363
- ...(input.groupBy ? { group_by: input.groupBy } : {}),
364
- ...(input.summaryFunctions ? {
365
- summary_functions: Object.fromEntries(Object.entries(input.summaryFunctions).map(([name, projection]) => [
366
- name,
367
- { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
368
- ]))
369
- } : {}),
370
- ...(input.summaries ? { summaries: input.summaries } : {}),
371
- ...(input.limit === undefined ? {} : { limit: input.limit }),
372
- ...(input.offset === undefined ? {} : { offset: input.offset }),
373
- ...(input.pagination ? { pagination: input.pagination } : {}),
374
- ...(input.cursor ? { cursor: input.cursor } : {}),
375
- ...(input.snapshot ? { snapshot: input.snapshot } : {}),
376
- ...(input.includeBody === undefined ? {} : { include_body: input.includeBody }),
377
- ...(input.frontmatterMode ? { frontmatter_mode: input.frontmatterMode } : {}),
378
- ...(input.contract ? { contract: input.contract } : {})
379
- };
380
- }
381
375
  function wireCreateInput(input) {
382
376
  return {
383
377
  ...(input.path ? { path: input.path } : {}),
@@ -525,23 +519,6 @@ function wireContractSetupChoice(choice) {
525
519
  ...(choice.binding ? { binding: choice.binding } : {})
526
520
  };
527
521
  }
528
- function wireDataContractIdentity(value) {
529
- return {
530
- id: value.id,
531
- version: value.version,
532
- digest: value.digest,
533
- type: value.type,
534
- implementationDigest: value.implementation_digest
535
- };
536
- }
537
- function wireQueryRecord(value) {
538
- const { effective_frontmatter, contract, ...record } = value;
539
- return {
540
- ...record,
541
- ...(effective_frontmatter === undefined ? {} : { effectiveFrontmatter: effective_frontmatter }),
542
- ...(contract ? { contract: wireDataContractIdentity(contract) } : {})
543
- };
544
- }
545
522
  function wireRecordDocument(value) {
546
523
  const { effective_frontmatter, contract, ...record } = value;
547
524
  return {
@@ -580,6 +557,7 @@ function wireTypePackResource(value) {
580
557
  ...(value.current_digest ? { currentDigest: value.current_digest } : {}),
581
558
  ...(value.installed_digest ? { installedDigest: value.installed_digest } : {}),
582
559
  ...(value.adopted_from_digest ? { adoptedFromDigest: value.adopted_from_digest } : {}),
560
+ ...(value.upgrade_baseline ? { upgradeBaseline: value.upgrade_baseline } : {}),
583
561
  ...(value.reason ? { reason: value.reason } : {})
584
562
  };
585
563
  }
@@ -595,7 +573,8 @@ function wireTypePackReceipt(value) {
595
573
  kind: resource.kind,
596
574
  mode: resource.mode,
597
575
  digest: resource.digest,
598
- ...(resource.adopted_from_digest ? { adoptedFromDigest: resource.adopted_from_digest } : {})
576
+ ...(resource.adopted_from_digest ? { adoptedFromDigest: resource.adopted_from_digest } : {}),
577
+ ...(resource.origin_digest ? { originDigest: resource.origin_digest } : {})
599
578
  }))
600
579
  };
601
580
  }
@@ -9,7 +9,7 @@ import { MdbaseFileClient } from "./files.js";
9
9
  import { MdbaseRecords } from "./records.js";
10
10
  import { type Application } from "./internal-types.js";
11
11
  import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.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, 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";
13
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";
14
14
  import type { MdbaseDeviceAuthorization } from "./connection-types.js";
15
15
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
@@ -76,6 +76,7 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
76
76
  private readonly collectionClient;
77
77
  private readonly notifications;
78
78
  private readonly transport;
79
+ private readonly authorityFeatures;
79
80
  readonly files: MdbaseFileClient;
80
81
  /** Editable record sessions shared by every view of this connection. */
81
82
  readonly records: MdbaseRecords<Frontmatter>;
@@ -89,6 +90,9 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
89
90
  get fileCapability(): FileCapability | null;
90
91
  get directAccess(): DirectAccessStatus;
91
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[];
92
96
  register(options?: ConnectRequestOptions): Promise<ConnectOutcome<Application, RegistrationProblemCode>>;
93
97
  info(): MdbaseConnectionInfo | null;
94
98
  authorizationCapabilities(requiredOperations?: CollectionOperation[]): MdbaseAuthorizationCapabilities;
@@ -129,8 +133,11 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
129
133
  describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
130
134
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
131
135
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
136
+ query(input: QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
132
137
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
138
+ queryPages(input: QueryMetadataInput, options?: QueryPagesOptions<JsonObject, QueryMetadataRecord>): AsyncGenerator<ConnectOutcome<QueryMetadataPage, CollectionQueryProblemCode>>;
133
139
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
140
+ queryAll(input: QueryMetadataInput, options?: QueryAllOptions<JsonObject, QueryMetadataRecord>): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
134
141
  queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
135
142
  listViews(options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewList, CollectionReadProblemCode>>;
136
143
  executeView(input: ExecuteViewInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<SavedViewExecution<Frontmatter>, CollectionReadProblemCode>>;
@@ -2,9 +2,13 @@ import { MdbasePeopleClient, requestPeople } from "./people-client.js";
2
2
  import { capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
3
3
  import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "./application-contract.js";
4
4
  import { MdbaseCollectionClient } from "./collection-client.js";
5
+ import { AuthorityFeatures, authorityCapabilities } from "./authority-features.js";
6
+ import { filePage } from "./file-metadata.js";
7
+ import { wireCollectionDescription } from "./query-wire.js";
8
+ import { CollectionWatchSubscription } from "./watch-policy.js";
5
9
  import { ConnectionNotifications } from "./connection-notifications.js";
6
10
  import { ConnectionTransport } from "./connection-transport.js";
7
- import { MdbaseConnectError, connectProblem, operationProblem } from "./errors.js";
11
+ import { MdbaseConnectError, connectError, connectProblem, operationProblem } from "./errors.js";
8
12
  import { MdbaseFileClient } from "./files.js";
9
13
  import { MdbaseRecords } from "./records.js";
10
14
  import { DEFAULT_OPERATIONS } from "./internal-types.js";
@@ -18,6 +22,7 @@ export class MdbaseConnection {
18
22
  collectionClient;
19
23
  notifications;
20
24
  transport;
25
+ authorityFeatures;
21
26
  files;
22
27
  /** Editable record sessions shared by every view of this connection. */
23
28
  records;
@@ -36,8 +41,34 @@ export class MdbaseConnection {
36
41
  collectionId,
37
42
  internals,
38
43
  timeouts: internals.timeouts,
39
- onChange: () => this.emitConnection()
44
+ onChange: () => {
45
+ this.authorityFeatures.refreshLifetime();
46
+ this.emitConnection();
47
+ }
40
48
  });
49
+ this.authorityFeatures = new AuthorityFeatures({
50
+ lifetime: () => {
51
+ const token = this.transport.currentToken();
52
+ return JSON.stringify([token?.savedAt, token?.grantId, token?.keyHandle,
53
+ token?.authority?.operationsUrl, token?.authority?.filesUrl, token?.authority?.replicaId,
54
+ token?.authority?.proofPublicKey, token?.clientId, token?.applicationOrigin,
55
+ token?.encryption, token?.scope, token?.operations, token?.fileCapability, this.transport.route]);
56
+ },
57
+ operations: () => this.operations,
58
+ fileCapability: () => this.fileCapability,
59
+ // Catalog TTL/in-flight loads are not evidence for a new authority route.
60
+ // B1 owns this authenticated baseline's sharing, deadline and lifetime.
61
+ describe: async (options) => {
62
+ const value = wireCollectionDescription(await this.transport.performOperation("describe", {}, options));
63
+ if (value.collectionId.toLowerCase() !== this.collectionId.toLowerCase())
64
+ throw connectError("invalid_operation_response", "The authority described a different collection.");
65
+ return connectSuccess(value);
66
+ },
67
+ filesPage: async (folder, signal) => {
68
+ const page = await filePage((method, path, input, signal) => this.transport.files.control(method, path, input, signal), new URLSearchParams({ protocol_version: "1", limit: "1", ...(folder ? { folder } : {}) }), signal);
69
+ return authorityCapabilities(page.authority_capabilities);
70
+ }
71
+ }, internals.timeouts.requestMs);
41
72
  this.people = new MdbasePeopleClient({
42
73
  request: (resource, options) => requestPeople(resource, options ?? {}, {
43
74
  serverUrl: internals.serverUrl, collectionId, requestMs: internals.timeouts.requestMs,
@@ -51,11 +82,11 @@ export class MdbaseConnection {
51
82
  downloadChunk: (session, chunkIndex, signal) => this.transport.files.downloadChunk(session, chunkIndex, signal)
52
83
  }, {
53
84
  downloadPart: (session, partIndex, expectedLength, signal) => this.transport.files.downloadHostedPart(session, partIndex, expectedLength, signal)
54
- }, internals.timeouts);
85
+ }, internals.timeouts, (id, options) => this.supportsAuthorityFeature(id, options));
55
86
  this.records = new MdbaseRecords(this, (path) => this.transport.pendingUpdate(path));
56
87
  this.collectionClient = new MdbaseCollectionClient(new CollectionRequestCoordinator({
57
88
  operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
58
- }, internals.timeouts.requestMs), internals.timeouts.requestMs);
89
+ }, internals.timeouts.requestMs), internals.timeouts.requestMs, (id, options) => this.supportsAuthorityFeature(id, options));
59
90
  this.readMany = this.collectionClient.readMany.bind(this.collectionClient);
60
91
  this.notifications = new ConnectionNotifications({
61
92
  serverUrl: internals.serverUrl,
@@ -85,6 +116,11 @@ export class MdbaseConnection {
85
116
  get route() {
86
117
  return this.transport.route;
87
118
  }
119
+ /** Implementation support for this connection lifetime; not an authorization check. */
120
+ supportsAuthorityFeature(id, options) {
121
+ return this.authorityFeatures.supports(id, options);
122
+ }
123
+ get authorityCapabilities() { return this.authorityFeatures.capabilities; }
88
124
  register(options) {
89
125
  return captureConnectOutcome(() => this.internals.register(options), REGISTRATION_PROBLEM_CODES);
90
126
  }
@@ -168,6 +204,7 @@ export class MdbaseConnection {
168
204
  return () => this.connectionListeners.delete(listener);
169
205
  }
170
206
  notifyStorageChanged() {
207
+ this.authorityFeatures.invalidate();
171
208
  this.transport.notifyStorageChanged();
172
209
  }
173
210
  checkDirectAccess(options) {
@@ -257,7 +294,13 @@ export class MdbaseConnection {
257
294
  this.transport.notifyStorageChanged();
258
295
  }
259
296
  get schemaGeneration() { return this.collectionClient.schemaGeneration; }
260
- describe(options) { return this.collectionClient.describe(options); }
297
+ async describe(options) {
298
+ const outcome = await this.collectionClient.describe(options);
299
+ if (outcome.ok && outcome.value.collectionId.toLowerCase() !== this.collectionId.toLowerCase()) {
300
+ return connectFailure(connectProblem("invalid_operation_response", "The authority described a different collection."));
301
+ }
302
+ return outcome;
303
+ }
261
304
  changes(input = {}, options) {
262
305
  return this.collectionClient.changes(input, options);
263
306
  }
@@ -268,10 +311,14 @@ export class MdbaseConnection {
268
311
  return this.collectionClient.query(input, options);
269
312
  }
270
313
  queryPages(input = {}, options = {}) {
271
- return this.collectionClient.queryPages(input, options);
314
+ return input.output === "metadata"
315
+ ? this.collectionClient.queryPages(input, options)
316
+ : this.collectionClient.queryPages(input, options);
272
317
  }
273
318
  queryAll(input = {}, options = {}) {
274
- return this.collectionClient.queryAll(input, options);
319
+ return input.output === "metadata"
320
+ ? this.collectionClient.queryAll(input, options)
321
+ : this.collectionClient.queryAll(input, options);
275
322
  }
276
323
  listViews(options) {
277
324
  return this.collectionClient.listViews(options);
@@ -493,7 +540,7 @@ export class MdbaseConnection {
493
540
  if (initial.value.reset) {
494
541
  throw new MdbaseConnectError(connectProblem("change_cursor_reset", "The collection change cursor expired. Refresh collection state before subscribing again."));
495
542
  }
496
- return new CollectionWatchSubscription(this.collectionClient, initial.value.cursor, input, input.cursor === undefined ? [] : initial.value.events, this.internals.timeouts.watchStartMs);
543
+ return new CollectionWatchSubscription(options => this.collectionClient.watch(options), initial.value.cursor, input, input.cursor === undefined ? [] : initial.value.events, this.internals.timeouts.watchStartMs);
497
544
  }), COLLECTION_CHANGES_PROBLEM_CODES);
498
545
  }
499
546
  emitConnection() {
@@ -502,114 +549,6 @@ export class MdbaseConnection {
502
549
  listener(connection);
503
550
  }
504
551
  }
505
- class CollectionWatchSubscription {
506
- client;
507
- input;
508
- watchStartTimeoutMs;
509
- changes = new Set();
510
- statuses = new Set();
511
- problems = new Set();
512
- controller = new AbortController();
513
- removeLifetimeAbort;
514
- currentStatus;
515
- currentProblem = null;
516
- pendingChanges;
517
- constructor(client, cursor, input, pendingChanges, watchStartTimeoutMs) {
518
- this.client = client;
519
- this.input = input;
520
- this.watchStartTimeoutMs = watchStartTimeoutMs;
521
- this.pendingChanges = [...pendingChanges];
522
- this.currentStatus = { state: "connected", cursor, recovered: false };
523
- const lifetimeSignal = input.lifetimeSignal;
524
- if (lifetimeSignal?.aborted)
525
- this.close();
526
- else if (lifetimeSignal) {
527
- const close = () => this.close();
528
- lifetimeSignal.addEventListener("abort", close, { once: true });
529
- this.removeLifetimeAbort = () => lifetimeSignal.removeEventListener("abort", close);
530
- }
531
- if (!this.controller.signal.aborted)
532
- void this.run(cursor);
533
- }
534
- get status() {
535
- return this.currentStatus;
536
- }
537
- get problem() {
538
- return this.currentProblem;
539
- }
540
- subscribe(listener, onStatus, onProblem) {
541
- this.changes.add(listener);
542
- if (onStatus) {
543
- this.statuses.add(onStatus);
544
- onStatus(this.currentStatus);
545
- }
546
- if (onProblem) {
547
- this.problems.add(onProblem);
548
- if (this.currentProblem)
549
- onProblem(this.currentProblem);
550
- }
551
- for (const change of this.pendingChanges)
552
- listener(change);
553
- this.pendingChanges = [];
554
- return () => {
555
- this.changes.delete(listener);
556
- if (onStatus)
557
- this.statuses.delete(onStatus);
558
- if (onProblem)
559
- this.problems.delete(onProblem);
560
- };
561
- }
562
- close() {
563
- if (this.controller.signal.aborted)
564
- return;
565
- this.controller.abort();
566
- this.removeLifetimeAbort?.();
567
- this.removeLifetimeAbort = undefined;
568
- const cursor = "cursor" in this.currentStatus ? this.currentStatus.cursor : undefined;
569
- this.publishStatus({ state: "closed", ...(cursor === undefined ? {} : { cursor }) });
570
- }
571
- async run(cursor) {
572
- let firstStatus = true;
573
- const iterator = this.client.watch({
574
- cursor,
575
- pollIntervalMs: this.input.pollIntervalMs,
576
- retry: this.input.retry,
577
- signal: this.controller.signal,
578
- timeoutMs: this.watchStartTimeoutMs,
579
- onStatus: (status) => {
580
- if (firstStatus && status.state === "connecting") {
581
- firstStatus = false;
582
- return;
583
- }
584
- firstStatus = false;
585
- this.publishStatus(status);
586
- }
587
- });
588
- try {
589
- for await (const outcome of iterator) {
590
- if (this.controller.signal.aborted)
591
- return;
592
- if (!outcome.ok) {
593
- this.currentProblem = outcome.problem;
594
- for (const listener of this.problems)
595
- listener(outcome.problem);
596
- return;
597
- }
598
- for (const listener of this.changes)
599
- listener(outcome.value);
600
- }
601
- }
602
- finally {
603
- if (!this.controller.signal.aborted)
604
- this.close();
605
- }
606
- }
607
- publishStatus(status) {
608
- this.currentStatus = status;
609
- for (const listener of this.statuses)
610
- listener(status);
611
- }
612
- }
613
552
  function syncRequestOptions(options, defaultTimeoutMs) {
614
553
  return {
615
554
  ...options,