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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +86 -6
  2. package/dist/application-session.js +45 -27
  3. package/dist/browser/integrity.json +1 -1
  4. package/dist/browser/mdbase-connect.min.js +4 -4
  5. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  6. package/dist/change-events.d.ts +6 -0
  7. package/dist/change-events.js +102 -0
  8. package/dist/collection-client.d.ts +7 -2
  9. package/dist/collection-client.js +35 -54
  10. package/dist/connection.d.ts +5 -2
  11. package/dist/connection.js +4 -3
  12. package/dist/description-cache.d.ts +14 -0
  13. package/dist/description-cache.js +73 -0
  14. package/dist/file-crypto.js +10 -6
  15. package/dist/file-descriptor.d.ts +12 -0
  16. package/dist/file-descriptor.js +12 -0
  17. package/dist/file-sha256.js +3 -3
  18. package/dist/files.d.ts +2 -11
  19. package/dist/files.js +36 -30
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.js +2 -0
  22. package/dist/operation-types.d.ts +127 -6
  23. package/dist/query-pagination-internals.d.ts +9 -0
  24. package/dist/query-pagination-internals.js +52 -0
  25. package/dist/query-pagination.js +28 -26
  26. package/dist/query-predicates.d.ts +11 -0
  27. package/dist/query-predicates.js +15 -0
  28. package/dist/read-many.d.ts +6 -0
  29. package/dist/read-many.js +70 -0
  30. package/dist/record-session.d.ts +3 -2
  31. package/dist/record-session.js +11 -2
  32. package/dist/records.d.ts +15 -2
  33. package/dist/records.js +109 -7
  34. package/dist/saved-view-pagination.js +20 -20
  35. package/dist/startup-timing.d.ts +2 -0
  36. package/dist/startup-timing.js +20 -0
  37. package/dist/structured-readiness.d.ts +3 -1
  38. package/dist/structured-readiness.js +21 -1
  39. package/package.json +2 -2
@@ -1 +1 @@
1
- sha384-Nv0Du948fR/kSC/QXiny8mY1SZ2akUFBT7QmsPb3irgnUGNN0LgsWDubjxozn5cU
1
+ sha384-oeF346B54GbocYns1w4tBu6zmyDtayo5iL/ujiP4qtf4pk1NsES2mMQj/0lIYmBb
@@ -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,102 @@
1
+ import { CHANGE_EVENT_KINDS } from "@mdbase-dev/connect-protocol";
2
+ import { clientFileDescriptor } 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 clientFileDescriptorIsValid(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
+ }
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, 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, 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,11 +10,16 @@ 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
+ private readonly descriptions;
14
+ private lastSchemaCursor;
15
+ /** Local cache generation, not an authority revision or cross-connection identity. */
16
+ get schemaGeneration(): number;
13
17
  constructor(transport: MdbaseCollectionTransport, requestTimeoutMs?: number | null);
14
18
  operation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<ConnectOutcome<Result>>;
15
- describe(options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
19
+ describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
16
20
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
17
21
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
22
+ readMany(paths: readonly string[], options?: ReadManyOptions): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
18
23
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
19
24
  queryPages(input?: QueryInput, options?: QueryPagesOptions<Frontmatter>): AsyncGenerator<ConnectOutcome<QueryPage<Frontmatter>, CollectionQueryProblemCode>>;
20
25
  queryAll(input?: QueryInput, options?: QueryAllOptions<Frontmatter>): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
@@ -1,8 +1,11 @@
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 { queryReadMany } from "./read-many.js";
3
6
  import { coordinatedSavedViewPages } from "./saved-view-pagination.js";
4
7
  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";
8
+ 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";
6
9
  import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason } from "./request-budget.js";
7
10
  import { watchRetryPolicy } from "./watch-policy.js";
8
11
  /**
@@ -14,25 +17,40 @@ import { watchRetryPolicy } from "./watch-policy.js";
14
17
  export class MdbaseCollectionClient {
15
18
  transport;
16
19
  requestTimeoutMs;
20
+ descriptions;
21
+ lastSchemaCursor = -1;
22
+ /** Local cache generation, not an authority revision or cross-connection identity. */
23
+ get schemaGeneration() { return this.descriptions.schemaGeneration; }
17
24
  constructor(transport, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
18
25
  this.transport = transport;
19
26
  this.requestTimeoutMs = requestTimeoutMs;
27
+ this.descriptions = new CollectionDescriptionCache(transport, requestTimeoutMs);
20
28
  }
21
29
  operation(operation, input, options) {
22
30
  return captureConnectOutcome(() => this.transport.operation(operation, input, options), ALL_CONNECT_PROBLEM_CODES);
23
31
  }
24
- async describe(options) {
25
- const outcome = await this.rawOperation("describe", {}, COLLECTION_DESCRIPTION_PROBLEM_CODES, options);
26
- return mapOutcome(outcome, wireCollectionDescription);
32
+ describe(options) {
33
+ return this.descriptions.describe(options);
27
34
  }
28
35
  async changes(input = {}, options) {
29
36
  const outcome = await this.rawOperation("changes", input, COLLECTION_CHANGES_PROBLEM_CODES, options);
30
- return mapOutcome(outcome, wireChangesPage);
37
+ const mapped = mapOutcome(outcome, normalizeChangesPage);
38
+ if (mapped.ok) {
39
+ const invalidations = mapped.value.events.filter(invalidatesDescription);
40
+ if (mapped.value.reset || invalidations.some((event) => event.cursor > this.lastSchemaCursor)) {
41
+ this.descriptions.invalidate();
42
+ this.lastSchemaCursor = mapped.value.cursor;
43
+ }
44
+ }
45
+ return mapped;
31
46
  }
32
47
  async read(input, options) {
33
48
  const outcome = await this.envelopeOperation("read", wireReadInput(input), COLLECTION_READ_PROBLEM_CODES, options);
34
49
  return mapOutcome(outcome, wireRecordDocument);
35
50
  }
51
+ readMany(paths, options = {}) {
52
+ return queryReadMany((input, requestOptions) => this.queryAll(input, requestOptions), paths, options, this.requestTimeoutMs);
53
+ }
36
54
  async query(input = {}, options) {
37
55
  const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, options);
38
56
  return mapOutcome(outcome, ({ results, meta }) => ({
@@ -62,6 +80,7 @@ export class MdbaseCollectionClient {
62
80
  for await (const outcome of this.queryPages(input, {
63
81
  firstPageSize: options.firstPageSize,
64
82
  pageSize: options.pageSize,
83
+ maxResults: options.maxResults,
65
84
  signal: budget.signal,
66
85
  pageTimeoutMs: null,
67
86
  coordination: options.coordination,
@@ -78,11 +97,14 @@ export class MdbaseCollectionClient {
78
97
  throw requestAbortReason(budget.signal);
79
98
  return connectSuccess({
80
99
  results,
81
- meta: {
82
- totalCount: finalPage?.meta?.totalCount ?? results.length,
83
- hasMore: finalPage ? !finalPage.complete : false,
84
- ...(finalPage?.snapshot ? { snapshot: finalPage.snapshot } : {})
85
- }
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
+ } } : {})
86
108
  }, diagnostics);
87
109
  }
88
110
  catch (error) {
@@ -299,6 +321,9 @@ export class MdbaseCollectionClient {
299
321
  if (!envelope.valid) {
300
322
  return connectFailure(operationProblem(envelope));
301
323
  }
324
+ if (["create_type", "update_type", "apply_type_pack", "apply_collection_setup", "create_view_source", "update_view_source", "delete_view_source"].includes(operation)) {
325
+ this.descriptions.invalidate();
326
+ }
302
327
  return connectSuccess(envelope.result, envelope.diagnostics);
303
328
  }
304
329
  }
@@ -635,47 +660,3 @@ function wireCollectionSetupReceipt(value) {
635
660
  cleanupDeferred: value.cleanup_deferred
636
661
  };
637
662
  }
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, 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";
@@ -79,6 +80,7 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
79
80
  /** Editable record sessions shared by every view of this connection. */
80
81
  readonly records: MdbaseRecords<Frontmatter>;
81
82
  readonly people: MdbasePeopleClient;
83
+ readonly readMany: MdbaseCollectionClient<Frontmatter>["readMany"];
82
84
  private readonly connectionListeners;
83
85
  constructor(internals: MdbaseConnectionInternals<Frontmatter>, collectionId: string);
84
86
  get displayName(): string;
@@ -123,7 +125,8 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
123
125
  unregisterNativeNotifications(options?: ConnectRequestOptions): Promise<ConnectOutcome<void, NotificationProblemCode>>;
124
126
  unregisterNotifications(serviceWorker?: ServiceWorkerRegistration, options?: ConnectRequestOptions): Promise<ConnectOutcome<void, NotificationProblemCode>>;
125
127
  forget(): void;
126
- describe(options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
128
+ get schemaGeneration(): number;
129
+ describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
127
130
  changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
128
131
  read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
129
132
  query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
@@ -22,6 +22,7 @@ export class MdbaseConnection {
22
22
  /** Editable record sessions shared by every view of this connection. */
23
23
  records;
24
24
  people;
25
+ readMany;
25
26
  connectionListeners = new Set();
26
27
  constructor(internals, collectionId) {
27
28
  this.internals = internals;
@@ -55,6 +56,7 @@ export class MdbaseConnection {
55
56
  this.collectionClient = new MdbaseCollectionClient(new CollectionRequestCoordinator({
56
57
  operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
57
58
  }, internals.timeouts.requestMs), internals.timeouts.requestMs);
59
+ this.readMany = this.collectionClient.readMany.bind(this.collectionClient);
58
60
  this.notifications = new ConnectionNotifications({
59
61
  serverUrl: internals.serverUrl,
60
62
  storage: internals.storage,
@@ -254,9 +256,8 @@ export class MdbaseConnection {
254
256
  this.internals.removeToken(this.collectionId, token?.keyHandle, "not_authorized", true);
255
257
  this.transport.notifyStorageChanged();
256
258
  }
257
- describe(options) {
258
- return this.collectionClient.describe(options);
259
- }
259
+ get schemaGeneration() { return this.collectionClient.schemaGeneration; }
260
+ describe(options) { return this.collectionClient.describe(options); }
260
261
  changes(input = {}, options) {
261
262
  return this.collectionClient.changes(input, options);
262
263
  }
@@ -0,0 +1,14 @@
1
+ import type { CollectionDescription, DescribeOptions, MdbaseCollectionTransport } from "./operation-types.js";
2
+ import { type CollectionDescriptionProblemCode, type ConnectOutcome } from "./outcomes.js";
3
+ /** One collection's description lifecycle; no background observer or persisted schema state. */
4
+ export declare class CollectionDescriptionCache {
5
+ private readonly transport;
6
+ private readonly requestTimeoutMs;
7
+ private generation;
8
+ private description?;
9
+ private describing?;
10
+ constructor(transport: MdbaseCollectionTransport, requestTimeoutMs: number | null);
11
+ get schemaGeneration(): number;
12
+ invalidate(): void;
13
+ describe(options?: DescribeOptions): Promise<ConnectOutcome<CollectionDescription, CollectionDescriptionProblemCode>>;
14
+ }
@@ -0,0 +1,73 @@
1
+ import { COLLECTION_DESCRIPTION_PROBLEM_CODES, captureConnectOutcome, connectSuccess } from "./outcomes.js";
2
+ import { withRequestBudget } from "./request-budget.js";
3
+ /** One collection's description lifecycle; no background observer or persisted schema state. */
4
+ export class CollectionDescriptionCache {
5
+ transport;
6
+ requestTimeoutMs;
7
+ generation = 0;
8
+ description;
9
+ describing;
10
+ constructor(transport, requestTimeoutMs) {
11
+ this.transport = transport;
12
+ this.requestTimeoutMs = requestTimeoutMs;
13
+ }
14
+ get schemaGeneration() { return this.generation; }
15
+ invalidate() {
16
+ this.generation += 1;
17
+ this.description = undefined;
18
+ this.describing = undefined;
19
+ }
20
+ describe(options = {}) {
21
+ return captureConnectOutcome(() => withRequestBudget(options, this.requestTimeoutMs, async () => {
22
+ if (!options.fresh && this.description && Date.now() < this.description.expiresAt) {
23
+ return connectSuccess(this.description.value);
24
+ }
25
+ if (!this.describing) {
26
+ this.description = undefined;
27
+ const generation = this.generation;
28
+ // The shared load has its own budget. Cancelling one waiter cannot cancel another.
29
+ const request = captureConnectOutcome(() => withRequestBudget({}, this.requestTimeoutMs, (budget) => this.transport.operation("describe", {}, { signal: budget.signal, timeoutMs: this.requestTimeoutMs })), COLLECTION_DESCRIPTION_PROBLEM_CODES).then((outcome) => {
30
+ if (!outcome.ok)
31
+ return outcome;
32
+ const value = wireCollectionDescription(outcome.value);
33
+ if (this.generation === generation)
34
+ this.description = { value, expiresAt: Date.now() + 60_000 };
35
+ return connectSuccess(value, outcome.diagnostics);
36
+ }).finally(() => {
37
+ if (this.describing === request)
38
+ this.describing = undefined;
39
+ });
40
+ this.describing = request;
41
+ }
42
+ return this.describing;
43
+ }), COLLECTION_DESCRIPTION_PROBLEM_CODES).then((outcome) => outcome.ok ? outcome.value : outcome);
44
+ }
45
+ }
46
+ function wireCollectionDescription(value) {
47
+ return {
48
+ protocolVersion: value.protocol_version,
49
+ collectionId: value.collection_id,
50
+ displayName: value.display_name,
51
+ specVersion: value.spec_version,
52
+ operations: value.operations,
53
+ changeCursor: value.change_cursor,
54
+ types: value.types,
55
+ contracts: value.contracts.map((contract) => ({
56
+ contractType: contract.contract_type,
57
+ id: contract.id,
58
+ version: contract.version,
59
+ digest: contract.digest,
60
+ schema: contract.schema,
61
+ ...(contract.binding_schema ? { bindingSchema: contract.binding_schema } : {}),
62
+ implementations: contract.implementations.map((implementation) => ({
63
+ typeName: implementation.type_name,
64
+ typeVersion: implementation.type_version,
65
+ ...(implementation.type_path ? { typePath: implementation.type_path } : {}),
66
+ digest: implementation.digest,
67
+ fields: implementation.fields,
68
+ ...(implementation.binding ? { binding: implementation.binding } : {})
69
+ }))
70
+ })),
71
+ ...(value.configuration ? { configuration: value.configuration } : {})
72
+ };
73
+ }
@@ -42,9 +42,9 @@ export class GrantFileTransferCipher {
42
42
  const payload = await crypto.subtle.encrypt({
43
43
  name: "AES-GCM",
44
44
  iv: chunkNonce(header.chunk_index),
45
- additionalData: arrayBuffer(authenticatedData),
45
+ additionalData: webCryptoBytes(authenticatedData),
46
46
  tagLength: 128
47
- }, this.key, arrayBuffer(plaintext));
47
+ }, this.key, webCryptoBytes(plaintext));
48
48
  return encodeFileFrame({ kind, header, payload: new Uint8Array(payload) });
49
49
  }
50
50
  async decryptChunk(encoded) {
@@ -55,9 +55,9 @@ export class GrantFileTransferCipher {
55
55
  const plaintext = await crypto.subtle.decrypt({
56
56
  name: "AES-GCM",
57
57
  iv: chunkNonce(frame.header.chunk_index),
58
- additionalData: arrayBuffer(authenticatedData),
58
+ additionalData: webCryptoBytes(authenticatedData),
59
59
  tagLength: 128
60
- }, this.key, arrayBuffer(frame.payload));
60
+ }, this.key, webCryptoBytes(frame.payload));
61
61
  return new Uint8Array(plaintext);
62
62
  }
63
63
  catch {
@@ -127,6 +127,10 @@ function chunkNonce(index) {
127
127
  new DataView(nonce.buffer).setBigUint64(4, BigInt(index), false);
128
128
  return nonce;
129
129
  }
130
- function arrayBuffer(bytes) {
131
- return Uint8Array.from(bytes).buffer;
130
+ function webCryptoBytes(bytes) {
131
+ // WebCrypto snapshots BufferSource synchronously, including the view bounds.
132
+ // SharedArrayBuffer is not a valid BufferSource and still needs an owned copy.
133
+ return bytes.buffer instanceof ArrayBuffer
134
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
135
+ : Uint8Array.from(bytes);
132
136
  }
@@ -0,0 +1,12 @@
1
+ import type { CollectionFileDescriptor as WireFile } from "@mdbase-dev/connect-protocol";
2
+ export interface CollectionFileDescriptor {
3
+ fileId: string;
4
+ path: string;
5
+ revision: string;
6
+ contentDigest: `sha256:${string}`;
7
+ size: number;
8
+ mediaType?: string;
9
+ mediaClass: import("@mdbase-dev/connect-protocol").FileMediaClass;
10
+ modifiedAt: string;
11
+ }
12
+ export declare function clientFileDescriptor(file: WireFile): CollectionFileDescriptor;
@@ -0,0 +1,12 @@
1
+ export function clientFileDescriptor(file) {
2
+ return {
3
+ fileId: file.file_id,
4
+ path: file.path,
5
+ revision: file.revision,
6
+ contentDigest: file.content_digest,
7
+ size: file.size,
8
+ ...(file.media_type ? { mediaType: file.media_type } : {}),
9
+ mediaClass: file.media_class,
10
+ modifiedAt: file.modified_at
11
+ };
12
+ }
@@ -53,7 +53,7 @@ export class IncrementalSha256 {
53
53
  }
54
54
  }
55
55
  while (offset + 64 <= input.byteLength) {
56
- this.compress(input.subarray(offset, offset + 64));
56
+ this.compress(input, offset);
57
57
  offset += 64;
58
58
  }
59
59
  if (offset < input.byteLength) {
@@ -82,10 +82,10 @@ export class IncrementalSha256 {
82
82
  .map((word) => word.toString(16).padStart(8, "0"))
83
83
  .join("");
84
84
  }
85
- compress(block) {
85
+ compress(block, blockOffset = 0) {
86
86
  const words = this.words;
87
87
  for (let index = 0; index < 16; index += 1) {
88
- const offset = index * 4;
88
+ const offset = blockOffset + index * 4;
89
89
  words[index] = ((block[offset] << 24)
90
90
  | (block[offset + 1] << 16)
91
91
  | (block[offset + 2] << 8)
package/dist/files.d.ts CHANGED
@@ -1,18 +1,10 @@
1
+ import { type CollectionFileDescriptor } from "./file-descriptor.js";
2
+ export type { CollectionFileDescriptor } from "./file-descriptor.js";
1
3
  import type { FileCapability, FileTransferSession } from "@mdbase-dev/connect-protocol";
2
4
  import { type FileSource } from "./file-transfer-internals.js";
3
5
  import type { ConnectRequestOptions } from "./operation-types.js";
4
6
  import { type ResolvedConnectTimeouts } from "./request-budget.js";
5
7
  export type MdbaseFileSource = FileSource;
6
- export interface CollectionFileDescriptor {
7
- fileId: string;
8
- path: string;
9
- revision: string;
10
- contentDigest: `sha256:${string}`;
11
- size: number;
12
- mediaType?: string;
13
- mediaClass: import("@mdbase-dev/connect-protocol").FileMediaClass;
14
- modifiedAt: string;
15
- }
16
8
  export interface MdbaseFileDeleteReceipt {
17
9
  protocolVersion: 1;
18
10
  type: "file_deleted";
@@ -112,4 +104,3 @@ export declare class MdbaseFileClient {
112
104
  private requireAction;
113
105
  private abort;
114
106
  }
115
- export {};
package/dist/files.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { clientFileDescriptor } from "./file-descriptor.js";
1
2
  import { FILE_PROTOCOL_VERSION } from "@mdbase-dev/connect-protocol";
2
3
  import { abortableDelay } from "./async.js";
3
4
  import { MdbaseConnectError, connectError } from "./errors.js";
@@ -220,23 +221,9 @@ export class MdbaseFileClient {
220
221
  }
221
222
  }
222
223
  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 ?? "" });
224
+ const bytes = await this.downloadBytes(file, options);
225
+ // downloadBytes owns a full, non-shared ArrayBuffer; Blob snapshots it.
226
+ return new Blob([bytes.buffer], { type: file.mediaType ?? "" });
240
227
  }
241
228
  /** Download and verify a file with network backpressure and bounded buffering. */
242
229
  async downloadStream(file, options = {}) {
@@ -416,7 +403,38 @@ export class MdbaseFileClient {
416
403
  });
417
404
  }
418
405
  async downloadBytes(file, options = {}) {
419
- return new Uint8Array(await (await this.download(file, options)).arrayBuffer());
406
+ if (file.size > MAX_BUFFERED_DOWNLOAD_BYTES) {
407
+ throw connectError("invalid_request", `download() buffers at most ${MAX_BUFFERED_DOWNLOAD_BYTES} bytes; use downloadStream() for larger files.`);
408
+ }
409
+ const stream = await this.downloadStream(file, options);
410
+ const bytes = new Uint8Array(file.size);
411
+ const reader = stream.getReader();
412
+ let offset = 0;
413
+ try {
414
+ while (true) {
415
+ const result = await reader.read();
416
+ // Reading through EOF is essential: hosted streams verify their digest
417
+ // only after the final range ends, not when the last bytes arrive.
418
+ if (result.done)
419
+ break;
420
+ if (result.value.byteLength > bytes.byteLength - offset) {
421
+ throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
422
+ }
423
+ bytes.set(result.value, offset);
424
+ offset += result.value.byteLength;
425
+ }
426
+ if (offset !== bytes.byteLength) {
427
+ throw connectError("invalid_operation_response", "Downloaded file bytes failed integrity verification.");
428
+ }
429
+ return bytes;
430
+ }
431
+ catch (error) {
432
+ await reader.cancel().catch(() => undefined);
433
+ throw error;
434
+ }
435
+ finally {
436
+ reader.releaseLock();
437
+ }
420
438
  }
421
439
  async move(file, path, options = {}) {
422
440
  return withCooperativeRequestBudget(options, this.timeouts.requestMs, (budget) => this.moveWithinBudget(file, path, { ...options, signal: budget.signal, timeoutMs: null }));
@@ -585,15 +603,3 @@ export class MdbaseFileClient {
585
603
  await this.request("DELETE", `transfers/${encodeURIComponent(transferId)}`).catch(() => undefined);
586
604
  }
587
605
  }
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
- }