@mdbase-dev/connect 0.1.0-beta.64 → 0.1.0-beta.66

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-lle3e5SEN8S45RwEaaerEa1VKEBzMw7JZGQcuO1VI734WP7P95QHHR5DbXASXaaL
1
+ sha384-Sac9rFWvetnHV52bEp32Rc1ljfJn2zHX+iQ7rHGKCRRAsiaeaofsdMp2IskpKr4N
@@ -60,5 +60,6 @@ export declare class MdbaseCollectionClient<Frontmatter extends JsonObject = Jso
60
60
  }, options?: ConnectRequestOptions): Promise<ConnectOutcome<MdbaseTimerReconciliation, CollectionMutationProblemCode>>;
61
61
  watch(options?: WatchOptions): AsyncGenerator<ConnectOutcome<CollectionChange, CollectionChangesProblemCode>>;
62
62
  private rawOperation;
63
+ private releaseQueryCursor;
63
64
  private envelopeOperation;
64
65
  }
@@ -1,4 +1,5 @@
1
1
  import { abortableDelay } from "./async.js";
2
+ import { coordinatedQueryPages } from "./query-pagination.js";
2
3
  import { MdbaseConnectError, connectError, connectProblem, operationProblem } from "./errors.js";
3
4
  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";
4
5
  import { createRequestBudget, DEFAULT_REQUEST_TIMEOUT_MS, requestAbortReason } from "./request-budget.js";
@@ -39,56 +40,14 @@ export class MdbaseCollectionClient {
39
40
  meta: {
40
41
  totalCount: meta.total_count,
41
42
  hasMore: meta.has_more,
43
+ ...(meta.cursor ? { cursor: meta.cursor } : {}),
42
44
  ...(meta.snapshot ? { snapshot: meta.snapshot } : {})
43
45
  }
44
46
  } : {})
45
47
  }));
46
48
  }
47
49
  async *queryPages(input = {}, options = {}) {
48
- const { offset: requestedOffset, limit: requestedLimit, snapshot: requestedSnapshot, ...criteria } = input;
49
- let offset = nonNegativeInteger(requestedOffset, 0);
50
- const firstPageSize = positiveInteger(options.firstPageSize ?? requestedLimit, 200);
51
- const pageSize = positiveInteger(options.pageSize ?? requestedLimit, 1_000);
52
- let snapshot = requestedSnapshot;
53
- let loaded = 0;
54
- let pageNumber = 0;
55
- while (!options.signal?.aborted) {
56
- const queried = await this.query({
57
- ...criteria,
58
- offset,
59
- limit: pageNumber === 0 ? firstPageSize : pageSize,
60
- ...(snapshot ? { snapshot } : {})
61
- }, { signal: options.signal, timeoutMs: options.pageTimeoutMs });
62
- if (!queried.ok) {
63
- yield queried;
64
- return;
65
- }
66
- const result = queried.value;
67
- const returnedSnapshot = result.meta?.snapshot;
68
- if (snapshot && returnedSnapshot && snapshot !== returnedSnapshot) {
69
- yield connectFailure(connectProblem("query_snapshot_changed", "The collection query snapshot changed while paging. Refresh the query before continuing."));
70
- return;
71
- }
72
- if (!snapshot && returnedSnapshot)
73
- snapshot = returnedSnapshot;
74
- loaded += result.results.length;
75
- const complete = !result.meta?.hasMore || result.results.length === 0;
76
- const page = {
77
- results: result.results,
78
- ...(result.meta ? { meta: result.meta } : {}),
79
- page: pageNumber,
80
- offset,
81
- loaded,
82
- complete,
83
- ...(snapshot ? { snapshot } : {})
84
- };
85
- options.onProgress?.(page);
86
- yield connectSuccess(page, queried.diagnostics);
87
- if (complete)
88
- return;
89
- offset += result.results.length;
90
- pageNumber += 1;
91
- }
50
+ yield* coordinatedQueryPages((pageInput, pageOptions) => this.query(pageInput, pageOptions), (cursor) => this.releaseQueryCursor(cursor), input, options);
92
51
  }
93
52
  async queryAll(input = {}, options = {}) {
94
53
  const budget = createRequestBudget(options, this.requestTimeoutMs);
@@ -101,6 +60,7 @@ export class MdbaseCollectionClient {
101
60
  pageSize: options.pageSize,
102
61
  signal: budget.signal,
103
62
  pageTimeoutMs: null,
63
+ coordination: options.coordination,
104
64
  onProgress: options.onProgress
105
65
  })) {
106
66
  if (!outcome.ok)
@@ -115,7 +75,6 @@ export class MdbaseCollectionClient {
115
75
  return connectSuccess({
116
76
  results,
117
77
  meta: {
118
- ...(finalPage?.meta ?? {}),
119
78
  totalCount: finalPage?.meta?.totalCount ?? results.length,
120
79
  hasMore: finalPage ? !finalPage.complete : false,
121
80
  ...(finalPage?.snapshot ? { snapshot: finalPage.snapshot } : {})
@@ -317,6 +276,14 @@ export class MdbaseCollectionClient {
317
276
  rawOperation(operation, input, allowedCodes, options) {
318
277
  return captureConnectOutcome(() => this.transport.operation(operation, input, options), allowedCodes);
319
278
  }
279
+ async releaseQueryCursor(cursor) {
280
+ try {
281
+ await this.envelopeOperation("query", { release_cursor: cursor }, COLLECTION_QUERY_PROBLEM_CODES, { timeoutMs: 2_000, coordination: { coalesce: false } });
282
+ }
283
+ catch {
284
+ // Lease expiry is bounded by the authority; cleanup never masks query results.
285
+ }
286
+ }
320
287
  async envelopeOperation(operation, input, allowedCodes, options) {
321
288
  const transported = await captureConnectOutcome(() => this.transport.operation(operation, input, options), allowedCodes);
322
289
  if (!transported.ok)
@@ -371,6 +338,8 @@ function wireQueryInput(input) {
371
338
  ...(input.summaries ? { summaries: input.summaries } : {}),
372
339
  ...(input.limit === undefined ? {} : { limit: input.limit }),
373
340
  ...(input.offset === undefined ? {} : { offset: input.offset }),
341
+ ...(input.pagination ? { pagination: input.pagination } : {}),
342
+ ...(input.cursor ? { cursor: input.cursor } : {}),
374
343
  ...(input.snapshot ? { snapshot: input.snapshot } : {}),
375
344
  ...(input.includeBody === undefined ? {} : { include_body: input.includeBody }),
376
345
  ...(input.frontmatterMode ? { frontmatter_mode: input.frontmatterMode } : {}),
@@ -707,9 +676,3 @@ function wireCollectionDescription(value) {
707
676
  ...(value.configuration ? { configuration: value.configuration } : {})
708
677
  };
709
678
  }
710
- function nonNegativeInteger(value, fallback) {
711
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
712
- }
713
- function positiveInteger(value, fallback) {
714
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
715
- }
@@ -7,6 +7,7 @@ import { DEFAULT_OPERATIONS } from "./internal-types.js";
7
7
  import { assertDeletePreview, assertRenamePreview, deleteEstimate, isCancellation, renameEstimate, throwIfCancelled, uniqueOperations } from "./operation-helpers.js";
8
8
  import { AUTHORIZATION_PROBLEM_CODES, COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, DIRECT_ACCESS_PROBLEM_CODES, NOTIFICATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
9
9
  import { withCooperativeRequestBudget, withRequestBudget } from "./request-budget.js";
10
+ import { CollectionRequestCoordinator } from "./request-coordinator.js";
10
11
  export class MdbaseConnection {
11
12
  internals;
12
13
  collectionId;
@@ -35,9 +36,9 @@ export class MdbaseConnection {
35
36
  }, {
36
37
  downloadPart: (session, partIndex, expectedLength, signal) => this.transport.files.downloadHostedPart(session, partIndex, expectedLength, signal)
37
38
  }, internals.timeouts);
38
- this.collectionClient = new MdbaseCollectionClient({
39
+ this.collectionClient = new MdbaseCollectionClient(new CollectionRequestCoordinator({
39
40
  operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
40
- }, internals.timeouts.requestMs);
41
+ }, internals.timeouts.requestMs), internals.timeouts.requestMs);
41
42
  this.notifications = new ConnectionNotifications({
42
43
  serverUrl: internals.serverUrl,
43
44
  storage: internals.storage,
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceip
15
15
  export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
16
16
  export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
17
17
  export type { ConnectSuccess, ConnectFailure, ConnectOutcome, CommonOperationProblemCode, RegistrationProblemCode, AuthorizationProblemCode, NotificationProblemCode, DirectAccessProblemCode, SessionProblemCode, CollectionSetupProblemCode, CollectionDescriptionProblemCode, CollectionReadProblemCode, CollectionQueryProblemCode, CollectionMutationProblemCode, CollectionChangesProblemCode, CollectionTypeProblemCode } from "./outcomes.js";
18
- 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, 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, 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";
18
+ 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, 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";
19
19
  export { externalStore } from "./external-store.js";
20
20
  export type { MdbaseExternalStore } from "./external-store.js";
21
21
  export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
@@ -158,7 +158,11 @@ export interface QueryInput {
158
158
  summaries?: QuerySummary[];
159
159
  limit?: number;
160
160
  offset?: number;
161
- /** Opaque token returned by the first metadata page for consistent, fast pagination. */
161
+ /** Request generation-pinned cursor pagination from a supporting authority. */
162
+ pagination?: "cursor";
163
+ /** Opaque, single-use token returned for the next generation-pinned page. */
164
+ cursor?: string;
165
+ /** Legacy stable-query token used by older authorities. */
162
166
  snapshot?: string;
163
167
  includeBody?: boolean;
164
168
  frontmatterMode?: "effective" | "persisted" | "both";
@@ -170,6 +174,7 @@ export interface QueryResult<Record extends JsonObject = JsonObject> {
170
174
  meta?: {
171
175
  totalCount: number;
172
176
  hasMore: boolean;
177
+ cursor?: string;
173
178
  snapshot?: string;
174
179
  };
175
180
  }
@@ -179,6 +184,7 @@ export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
179
184
  signal?: AbortSignal;
180
185
  /** Independent budget for each page requested by this caller-driven iterator. */
181
186
  pageTimeoutMs?: number | null;
187
+ coordination?: RequestCoordinationOptions;
182
188
  onProgress?: (page: QueryPage<Record>) => void;
183
189
  }
184
190
  export interface QueryAllOptions<Record extends JsonObject = JsonObject> extends ConnectRequestOptions {
@@ -193,12 +199,27 @@ export interface QueryPage<Record extends JsonObject = JsonObject> {
193
199
  offset: number;
194
200
  loaded: number;
195
201
  complete: boolean;
202
+ /** Opaque token for the next page while this iterator remains open. */
203
+ cursor?: string;
196
204
  snapshot?: string;
197
205
  }
198
206
  export interface ConnectRequestOptions {
199
207
  signal?: AbortSignal;
200
208
  /** Relative request budget. `null` deliberately disables the SDK default. */
201
209
  timeoutMs?: number | null;
210
+ /** Optional coordination for replaceable reads on this selected connection. */
211
+ coordination?: RequestCoordinationOptions;
212
+ }
213
+ export interface RequestCoordinationOptions {
214
+ /**
215
+ * Stable UI/request family. Required when `latestWins` is enabled; a newer
216
+ * request in the same family cancels the older read or query.
217
+ */
218
+ family?: string;
219
+ /** Explicitly cancel older work in this family. Mutations never allow this. */
220
+ latestWins?: boolean;
221
+ /** Disable automatic coalescing of identical signal-free reads. */
222
+ coalesce?: boolean;
202
223
  }
203
224
  export interface MutationEstimate {
204
225
  /** Records whose links may be affected, excluding the record being mutated. */
@@ -29,14 +29,14 @@ export declare const SESSION_PROBLEM_CODES: readonly ["discovery_failed", "inval
29
29
  export type CollectionSetupProblemCode = "collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported";
30
30
  export type CollectionDescriptionProblemCode = CommonOperationProblemCode | CollectionSetupProblemCode;
31
31
  export type CollectionReadProblemCode = CollectionDescriptionProblemCode | "invalid_path" | "operation_invalid";
32
- export type CollectionQueryProblemCode = CollectionReadProblemCode | "query_snapshot_changed" | "sandbox_unsupported";
32
+ export type CollectionQueryProblemCode = CollectionReadProblemCode | "cursor_capacity_exhausted" | "generation_expired" | "invalid_read_cursor" | "query_snapshot_changed" | "sandbox_unsupported";
33
33
  export type CollectionMutationProblemCode = CollectionReadProblemCode | "concurrent_modification" | "invalid_preflight" | "pending_mutation_unresolved";
34
34
  export type CollectionChangesProblemCode = CollectionDescriptionProblemCode | "change_cursor_reset";
35
35
  export type CollectionTypeProblemCode = CollectionMutationProblemCode | "type_pack_provision_failed";
36
36
  export declare const COLLECTION_SETUP_PROBLEM_CODES: readonly ["collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported"];
37
37
  export declare const COLLECTION_DESCRIPTION_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported"];
38
38
  export declare const COLLECTION_READ_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid"];
39
- export declare const COLLECTION_QUERY_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "query_snapshot_changed", "sandbox_unsupported"];
39
+ export declare const COLLECTION_QUERY_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "cursor_capacity_exhausted", "generation_expired", "invalid_read_cursor", "query_snapshot_changed", "sandbox_unsupported"];
40
40
  export declare const COLLECTION_MUTATION_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved"];
41
41
  export declare const COLLECTION_CHANGES_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "change_cursor_reset"];
42
42
  export declare const COLLECTION_TYPE_PROBLEM_CODES: readonly ["access_denied", "access_paused", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved", "type_pack_provision_failed"];
package/dist/outcomes.js CHANGED
@@ -127,6 +127,9 @@ export const COLLECTION_READ_PROBLEM_CODES = [
127
127
  ];
128
128
  export const COLLECTION_QUERY_PROBLEM_CODES = [
129
129
  ...COLLECTION_READ_PROBLEM_CODES,
130
+ "cursor_capacity_exhausted",
131
+ "generation_expired",
132
+ "invalid_read_cursor",
130
133
  "query_snapshot_changed",
131
134
  "sandbox_unsupported"
132
135
  ];
@@ -0,0 +1,6 @@
1
+ import type { JsonObject } from "@mdbase-dev/connect-protocol";
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>>;
6
+ export {};
@@ -0,0 +1,98 @@
1
+ import { connectProblem } from "./errors.js";
2
+ import { connectFailure, connectSuccess } from "./outcomes.js";
3
+ export async function* coordinatedQueryPages(query, releaseQueryCursor, input = {}, options = {}) {
4
+ const { offset: requestedOffset, limit: requestedLimit, cursor: requestedCursor, pagination: requestedPagination, snapshot: requestedSnapshot, ...criteria } = input;
5
+ let offset = nonNegativeInteger(requestedOffset, 0);
6
+ const firstPageSize = positiveInteger(options.firstPageSize ?? requestedLimit, 200);
7
+ const pageSize = positiveInteger(options.pageSize ?? requestedLimit, 1_000);
8
+ let cursor = requestedCursor;
9
+ let cursorMode = requestedCursor !== undefined;
10
+ let cursorToRelease = requestedCursor;
11
+ let snapshot = requestedSnapshot;
12
+ let loaded = 0;
13
+ let pageNumber = 0;
14
+ try {
15
+ while (!options.signal?.aborted) {
16
+ const pageCursor = cursor;
17
+ const queried = await query({
18
+ ...criteria,
19
+ limit: pageNumber === 0 ? firstPageSize : pageSize,
20
+ ...(cursorMode
21
+ ? (pageCursor ? { cursor: pageCursor } : { pagination: "cursor" })
22
+ : {
23
+ offset,
24
+ ...(snapshot ? { snapshot } : {}),
25
+ ...(!snapshot && requestedPagination === "cursor"
26
+ ? { pagination: "cursor" }
27
+ : {})
28
+ }),
29
+ ...(!cursorMode && !snapshot && requestedPagination === undefined
30
+ ? { pagination: "cursor" }
31
+ : {})
32
+ }, {
33
+ signal: options.signal,
34
+ timeoutMs: options.pageTimeoutMs,
35
+ coordination: { ...options.coordination, coalesce: false }
36
+ });
37
+ if (!queried.ok) {
38
+ yield queried;
39
+ return;
40
+ }
41
+ const result = queried.value;
42
+ const returnedCursor = result.meta?.cursor;
43
+ if (returnedCursor) {
44
+ cursorMode = true;
45
+ cursor = returnedCursor;
46
+ cursorToRelease = returnedCursor;
47
+ }
48
+ else if (cursorMode) {
49
+ cursor = undefined;
50
+ cursorToRelease = pageCursor;
51
+ }
52
+ if (cursorMode && result.meta?.hasMore && !returnedCursor) {
53
+ yield connectFailure(connectProblem("invalid_operation_response", "The collection authority omitted the cursor required for the next query page."));
54
+ return;
55
+ }
56
+ const returnedSnapshot = result.meta?.snapshot;
57
+ if (!cursorMode && snapshot && returnedSnapshot && snapshot !== returnedSnapshot) {
58
+ yield connectFailure(connectProblem("query_snapshot_changed", "The collection query snapshot changed while paging. Refresh the query before continuing."));
59
+ return;
60
+ }
61
+ if (!cursorMode && !snapshot && returnedSnapshot)
62
+ snapshot = returnedSnapshot;
63
+ loaded += result.results.length;
64
+ const complete = !result.meta?.hasMore || result.results.length === 0;
65
+ const page = {
66
+ results: result.results,
67
+ ...(result.meta ? { meta: result.meta } : {}),
68
+ page: pageNumber,
69
+ offset,
70
+ loaded,
71
+ complete,
72
+ ...(returnedCursor ? { cursor: returnedCursor } : {}),
73
+ ...(!cursorMode && snapshot ? { snapshot } : {})
74
+ };
75
+ options.onProgress?.(page);
76
+ yield connectSuccess(page, queried.diagnostics);
77
+ if (complete)
78
+ return;
79
+ offset += result.results.length;
80
+ pageNumber += 1;
81
+ }
82
+ }
83
+ finally {
84
+ if (cursorMode && cursorToRelease) {
85
+ await releaseQueryCursor(cursorToRelease);
86
+ }
87
+ }
88
+ }
89
+ function nonNegativeInteger(value, fallback) {
90
+ return typeof value === "number" && Number.isFinite(value)
91
+ ? Math.max(0, Math.floor(value))
92
+ : fallback;
93
+ }
94
+ function positiveInteger(value, fallback) {
95
+ return typeof value === "number" && Number.isFinite(value) && value > 0
96
+ ? Math.floor(value)
97
+ : fallback;
98
+ }
@@ -0,0 +1,29 @@
1
+ import { type CollectionOperation } from "@mdbase-dev/connect-protocol";
2
+ import type { ConnectRequestOptions, MdbaseCollectionTransport } from "./operation-types.js";
3
+ interface CoordinatorLimits {
4
+ foregroundCapacity?: number;
5
+ foregroundQueueCapacity?: number;
6
+ mutationQueueCapacity?: number;
7
+ }
8
+ /** One bounded request owner for a selected connection. */
9
+ export declare class CollectionRequestCoordinator implements MdbaseCollectionTransport {
10
+ private readonly transport;
11
+ private readonly requestTimeoutMs;
12
+ private readonly foregroundCapacity;
13
+ private readonly foregroundQueueCapacity;
14
+ private readonly mutationQueueCapacity;
15
+ private readonly foregroundQueue;
16
+ private readonly mutationQueue;
17
+ private readonly identicalReads;
18
+ private readonly latestFamilies;
19
+ private activeForeground;
20
+ private mutationActive;
21
+ constructor(transport: MdbaseCollectionTransport, requestTimeoutMs: number | null, limits?: CoordinatorLimits);
22
+ operation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<Result>;
23
+ private schedule;
24
+ private replaceLatest;
25
+ private releaseLatest;
26
+ private pump;
27
+ private start;
28
+ }
29
+ export {};
@@ -0,0 +1,212 @@
1
+ import { isMutatingOperation } from "@mdbase-dev/connect-protocol";
2
+ import { connectError } from "./errors.js";
3
+ import { requestAbortReason, requestOptionsWithinBudget, withCooperativeRequestBudget } from "./request-budget.js";
4
+ const DEFAULT_FOREGROUND_CAPACITY = 4;
5
+ const DEFAULT_QUEUE_CAPACITY = 32;
6
+ /** One bounded request owner for a selected connection. */
7
+ export class CollectionRequestCoordinator {
8
+ transport;
9
+ requestTimeoutMs;
10
+ foregroundCapacity;
11
+ foregroundQueueCapacity;
12
+ mutationQueueCapacity;
13
+ foregroundQueue = [];
14
+ mutationQueue = [];
15
+ identicalReads = new Map();
16
+ latestFamilies = new Map();
17
+ activeForeground = 0;
18
+ mutationActive = false;
19
+ constructor(transport, requestTimeoutMs, limits = {}) {
20
+ this.transport = transport;
21
+ this.requestTimeoutMs = requestTimeoutMs;
22
+ this.foregroundCapacity = positiveCapacity(limits.foregroundCapacity, DEFAULT_FOREGROUND_CAPACITY);
23
+ this.foregroundQueueCapacity = positiveCapacity(limits.foregroundQueueCapacity, DEFAULT_QUEUE_CAPACITY);
24
+ this.mutationQueueCapacity = positiveCapacity(limits.mutationQueueCapacity, DEFAULT_QUEUE_CAPACITY);
25
+ }
26
+ operation(operation, input, options = {}) {
27
+ const read = !isMutatingOperation(operation, input);
28
+ const coordination = validatedCoordination(options.coordination, read);
29
+ const coalesceKey = read
30
+ && coordination.coalesce !== false
31
+ && !coordination.latestWins
32
+ && options.signal === undefined
33
+ ? stableRequestKey(operation, input, options.timeoutMs)
34
+ : undefined;
35
+ if (coalesceKey) {
36
+ const existing = this.identicalReads.get(coalesceKey);
37
+ if (existing)
38
+ return existing;
39
+ }
40
+ const request = withCooperativeRequestBudget(options, this.requestTimeoutMs, (budget) => this.schedule(operation, input, options, coordination, read, budget));
41
+ if (coalesceKey) {
42
+ this.identicalReads.set(coalesceKey, request);
43
+ void request.finally(() => {
44
+ if (this.identicalReads.get(coalesceKey) === request) {
45
+ this.identicalReads.delete(coalesceKey);
46
+ }
47
+ }).catch(() => undefined);
48
+ }
49
+ return request;
50
+ }
51
+ schedule(operation, input, options, coordination, read, budget) {
52
+ const replacement = coordination.latestWins
53
+ ? this.replaceLatest(coordination.family)
54
+ : undefined;
55
+ const combined = combineSignals(budget.signal, replacement?.signal);
56
+ const { coordination: _coordination, ...transportOptions } = options;
57
+ const withinBudget = requestOptionsWithinBudget({ ...transportOptions, timeoutMs: null }, budget);
58
+ const run = () => this.transport.operation(operation, input, {
59
+ ...withinBudget,
60
+ signal: combined.signal
61
+ });
62
+ const queue = read ? this.foregroundQueue : this.mutationQueue;
63
+ const capacity = read ? this.foregroundQueueCapacity : this.mutationQueueCapacity;
64
+ if (queue.length >= capacity) {
65
+ combined.dispose();
66
+ this.releaseLatest(coordination.family, replacement);
67
+ return Promise.reject(connectError("connector_busy", "The selected connection request queue is full.", { operationOutcome: "not_sent", status: 503 }));
68
+ }
69
+ return new Promise((resolve, reject) => {
70
+ const queued = {
71
+ budget,
72
+ run,
73
+ resolve: (result) => resolve(result),
74
+ reject,
75
+ started: false,
76
+ settled: false
77
+ };
78
+ const abortQueued = () => {
79
+ if (queued.started || queued.settled)
80
+ return;
81
+ queued.settled = true;
82
+ queued.reject(requestAbortReason(combined.signal));
83
+ combined.dispose();
84
+ this.releaseLatest(coordination.family, replacement);
85
+ };
86
+ combined.signal.addEventListener("abort", abortQueued, { once: true });
87
+ const originalRun = queued.run;
88
+ queued.run = async () => {
89
+ combined.signal.removeEventListener("abort", abortQueued);
90
+ try {
91
+ if (combined.signal.aborted)
92
+ throw requestAbortReason(combined.signal);
93
+ return await originalRun();
94
+ }
95
+ finally {
96
+ combined.dispose();
97
+ this.releaseLatest(coordination.family, replacement);
98
+ }
99
+ };
100
+ queue.push(queued);
101
+ this.pump();
102
+ });
103
+ }
104
+ replaceLatest(family) {
105
+ const replacement = new AbortController();
106
+ this.latestFamilies.get(family)?.abort(connectError("operation_cancelled", "A newer request replaced this read before it changed the collection.", { operationOutcome: "not_sent" }));
107
+ this.latestFamilies.set(family, replacement);
108
+ return replacement;
109
+ }
110
+ releaseLatest(family, controller) {
111
+ if (family && controller && this.latestFamilies.get(family) === controller) {
112
+ this.latestFamilies.delete(family);
113
+ }
114
+ }
115
+ pump() {
116
+ while (this.activeForeground < this.foregroundCapacity) {
117
+ const request = nextLive(this.foregroundQueue);
118
+ if (!request)
119
+ break;
120
+ this.activeForeground += 1;
121
+ this.start(request, () => {
122
+ this.activeForeground -= 1;
123
+ this.pump();
124
+ });
125
+ }
126
+ if (!this.mutationActive) {
127
+ const request = nextLive(this.mutationQueue);
128
+ if (request) {
129
+ this.mutationActive = true;
130
+ this.start(request, () => {
131
+ this.mutationActive = false;
132
+ this.pump();
133
+ });
134
+ }
135
+ }
136
+ }
137
+ start(request, release) {
138
+ request.started = true;
139
+ void request.run().then((result) => {
140
+ if (!request.settled) {
141
+ request.settled = true;
142
+ request.resolve(result);
143
+ }
144
+ }, (error) => {
145
+ if (!request.settled) {
146
+ request.settled = true;
147
+ request.reject(error);
148
+ }
149
+ }).finally(release);
150
+ }
151
+ }
152
+ function nextLive(queue) {
153
+ while (queue.length > 0) {
154
+ const request = queue.shift();
155
+ if (!request.settled)
156
+ return request;
157
+ }
158
+ return undefined;
159
+ }
160
+ function validatedCoordination(coordination, read) {
161
+ const resolved = coordination ?? {};
162
+ if (resolved.latestWins && !read) {
163
+ throw new TypeError("latestWins coordination is available only for read operations.");
164
+ }
165
+ if (resolved.latestWins && !resolved.family?.trim()) {
166
+ throw new TypeError("latestWins coordination requires a non-empty family.");
167
+ }
168
+ return resolved;
169
+ }
170
+ function stableRequestKey(operation, input, timeoutMs) {
171
+ return `${operation}:${timeoutMs === undefined ? "default" : String(timeoutMs)}:${stableJson(input)}`;
172
+ }
173
+ function stableJson(value) {
174
+ if (value === null || typeof value !== "object") {
175
+ return JSON.stringify(value) ?? String(value);
176
+ }
177
+ if (Array.isArray(value))
178
+ return `[${value.map(stableJson).join(",")}]`;
179
+ return `{${Object.entries(value)
180
+ .sort(([left], [right]) => left.localeCompare(right))
181
+ .map(([key, nested]) => `${JSON.stringify(key)}:${stableJson(nested)}`)
182
+ .join(",")}}`;
183
+ }
184
+ function combineSignals(primary, secondary) {
185
+ if (!secondary)
186
+ return { signal: primary, dispose: () => undefined };
187
+ const controller = new AbortController();
188
+ const abortPrimary = () => controller.abort(primary.reason);
189
+ const abortSecondary = () => controller.abort(secondary.reason);
190
+ if (primary.aborted)
191
+ abortPrimary();
192
+ else
193
+ primary.addEventListener("abort", abortPrimary, { once: true });
194
+ if (secondary.aborted)
195
+ abortSecondary();
196
+ else
197
+ secondary.addEventListener("abort", abortSecondary, { once: true });
198
+ return {
199
+ signal: controller.signal,
200
+ dispose: () => {
201
+ primary.removeEventListener("abort", abortPrimary);
202
+ secondary.removeEventListener("abort", abortSecondary);
203
+ }
204
+ };
205
+ }
206
+ function positiveCapacity(value, fallback) {
207
+ const resolved = value ?? fallback;
208
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
209
+ throw new TypeError("Request coordinator capacities must be positive safe integers.");
210
+ }
211
+ return resolved;
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mdbase-dev/connect",
3
- "version": "0.1.0-beta.64",
3
+ "version": "0.1.0-beta.66",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "dependencies": {
36
- "@mdbase-dev/connect-protocol": "0.1.0-beta.64"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.66"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",