@mdbase-dev/connect 0.1.0-beta.120 → 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.
- package/README.md +86 -6
- package/dist/application-session.js +45 -27
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +4 -4
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/change-events.d.ts +6 -0
- package/dist/change-events.js +102 -0
- package/dist/collection-client.d.ts +7 -2
- package/dist/collection-client.js +35 -54
- package/dist/connection.d.ts +5 -2
- package/dist/connection.js +4 -3
- package/dist/description-cache.d.ts +14 -0
- package/dist/description-cache.js +73 -0
- package/dist/file-crypto.js +10 -6
- package/dist/file-descriptor.d.ts +12 -0
- package/dist/file-descriptor.js +12 -0
- package/dist/file-sha256.js +3 -3
- package/dist/files.d.ts +2 -11
- package/dist/files.js +36 -30
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/operation-types.d.ts +127 -6
- package/dist/query-pagination-internals.d.ts +9 -0
- package/dist/query-pagination-internals.js +52 -0
- package/dist/query-pagination.js +28 -26
- package/dist/query-predicates.d.ts +11 -0
- package/dist/query-predicates.js +15 -0
- package/dist/read-many.d.ts +6 -0
- package/dist/read-many.js +70 -0
- package/dist/record-session.d.ts +8 -2
- package/dist/record-session.js +27 -5
- package/dist/records.d.ts +15 -2
- package/dist/records.js +109 -7
- package/dist/saved-view-pagination.js +20 -20
- package/dist/startup-timing.d.ts +2 -0
- package/dist/startup-timing.js +20 -0
- package/dist/structured-readiness.d.ts +3 -1
- package/dist/structured-readiness.js +21 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { normalizeCollectionChange } from "./change-events.js";
|
|
1
2
|
export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
|
|
2
3
|
export type { MdbaseApplicationSessionOptions, MdbaseApplicationVerificationStore, MdbaseDefinitionUpdate, MdbaseCollectionSetupUpdate, MdbaseApplicationSessionSnapshot } from "./application-session.js";
|
|
3
4
|
export type { MdbaseConnectOptions, MdbaseConnectTimeouts, MdbaseFrontmatter } from "./connect-options.js";
|
|
@@ -22,7 +23,9 @@ export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceip
|
|
|
22
23
|
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
23
24
|
export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
|
|
24
25
|
export type { ConnectSuccess, ConnectFailure, ConnectOutcome, CommonOperationProblemCode, RegistrationProblemCode, AuthorizationProblemCode, NotificationProblemCode, DirectAccessProblemCode, SessionProblemCode, CollectionSetupProblemCode, CollectionDescriptionProblemCode, CollectionReadProblemCode, CollectionQueryProblemCode, CollectionMutationProblemCode, CollectionChangesProblemCode, CollectionTypeProblemCode } from "./outcomes.js";
|
|
25
|
-
export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
|
|
26
|
+
export type { MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ReadInput, ReadManyOptions, ReadManyRecord, ReadManyEntry, ReadManyBatchError, ReadManyResult, DataContractSelector, CollectionFileMetadata, DataContractViewIdentity, QueryRecord, RecordDocument, QueryInput, QueryProjection, QuerySelectionExpression, QueryOrder, QuerySummary, QueryResult, QueryPagesOptions, QueryAllOptions, QueryPage, ConnectRequestOptions, MutationEstimate, MutationProgressState, MutationProgress, MutationProgressOptions, RequestCoordinationOptions, RenameProgressOptions, DeleteProgressOptions, PendingMutationSummary, PendingMutation, CreateInput, UpdateInput, DeleteInput, DeleteResult, DeletePreflightResult, RenameInput, RenameResult, RenamePreflightResult, ReadTypeInput, CreateTypeInput, UpdateTypeInput, ReadViewSourceInput, CreateViewSourceInput, UpdateViewSourceInput, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, SavedViewPresentation, SavedViewSource, SavedViewProperty, SavedNamedView, SavedViewDocument, SavedViewList, SavedViewSourceDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, ContractSetupChoice, TypePackResourceDiff, TypePackReceipt, TypePackAssessment, TypePackApplyResult, ConfigurationContributionValue, ConfigurationRequirement, ConfigurationProvision, ApplicationCollectionSetupRequirements, ApplicationCollectionSetupProvisions, ConfigurationSetupConflict, ConfigurationSetupAssessment, CollectionSetupAssessment, CollectionSetupReceipt, CollectionSetupApplyResult, AssessTypePackInput, ApplyTypePackInput, AssessCollectionSetupInput, ApplyCollectionSetupInput, ChangesInput, WatchOptions, WatchRetryOptions, WatchStatus, WatchInput, MdbaseWatchSubscription, CollectionChange, RecordChangeMetadata, DescribeOptions, CollectionChangesPage, CollectionDescription, CollectionContractDescriptor, CollectionContractImplementationDescriptor, CollectionTypeDescriptor } from "./operation-types.js";
|
|
27
|
+
export { linksTo } from "./query-predicates.js";
|
|
28
|
+
export type { LinksToOptions } from "./query-predicates.js";
|
|
26
29
|
export { externalStore } from "./external-store.js";
|
|
27
30
|
export type { MdbaseExternalStore } from "./external-store.js";
|
|
28
31
|
export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { normalizeCollectionChange } from "./change-events.js";
|
|
1
2
|
export { MdbaseApplicationSession, MdbaseMemoryVerificationStore } from "./application-session.js";
|
|
2
3
|
export { MdbaseConnect } from "./mdbase-connect.js";
|
|
3
4
|
export { MdbaseConnection } from "./connection.js";
|
|
@@ -8,6 +9,7 @@ export { MdbaseRecords } from "./records.js";
|
|
|
8
9
|
export { MdbaseRecordSession } from "./record-session.js";
|
|
9
10
|
export { MdbasePeopleClient, PERSON_CONTRACT, sameIdentity } from "./people-client.js";
|
|
10
11
|
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
12
|
+
export { linksTo } from "./query-predicates.js";
|
|
11
13
|
export { externalStore } from "./external-store.js";
|
|
12
14
|
export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
|
|
13
15
|
export { MDBASE_RECORD_CREATED_CONTRACT, MDBASE_TIMER_FIRED_CONTRACT, operationsForApplicationCapabilities } from "@mdbase-dev/connect-protocol";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CollectionOperation, ConnectProblem, ContractRequirement, JsonObject, MutationOperationIdentifier, TypePackProvision } from "@mdbase-dev/connect-protocol";
|
|
1
|
+
import type { CollectionChange as WireCollectionChange, CollectionChangesPage as WireCollectionChangesPage, CollectionOperation, ConnectProblem, ContractRequirement, JsonObject, MutationOperationIdentifier, TypePackProvision } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
export interface MdbaseDesiredTimer {
|
|
3
3
|
/** Stable within the timer namespace. */
|
|
4
4
|
id: string;
|
|
@@ -64,6 +64,44 @@ export interface QueryRecord<Frontmatter extends JsonObject = JsonObject> {
|
|
|
64
64
|
values?: JsonObject;
|
|
65
65
|
contract?: DataContractViewIdentity;
|
|
66
66
|
}
|
|
67
|
+
/** Query-shaped batched record today; a future authority may supply a revision. */
|
|
68
|
+
export interface ReadManyRecord<Frontmatter extends JsonObject = JsonObject> extends QueryRecord<Frontmatter> {
|
|
69
|
+
/** Absent on current query authorities. Never synthesized from timestamps or content. */
|
|
70
|
+
revision?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface ReadManyOptions extends ConnectRequestOptions {
|
|
73
|
+
includeBody?: boolean;
|
|
74
|
+
frontmatterMode?: QueryInput["frontmatterMode"];
|
|
75
|
+
/** Restrict matching records to these raw collection types, not a contract view. */
|
|
76
|
+
types?: string[];
|
|
77
|
+
/** Unique paths per independent query, default 100; range 1..1,000. */
|
|
78
|
+
batchSize?: number;
|
|
79
|
+
/** Independent batches in flight, default 4; range 1..4. Cursor pages stay serial. */
|
|
80
|
+
concurrency?: number;
|
|
81
|
+
}
|
|
82
|
+
export type ReadManyEntry<Frontmatter extends JsonObject = JsonObject> = {
|
|
83
|
+
status: "found";
|
|
84
|
+
path: string;
|
|
85
|
+
record: ReadManyRecord<Frontmatter>;
|
|
86
|
+
} | {
|
|
87
|
+
status: "missing";
|
|
88
|
+
path: string;
|
|
89
|
+
} | {
|
|
90
|
+
status: "error";
|
|
91
|
+
path: string;
|
|
92
|
+
batch: number;
|
|
93
|
+
};
|
|
94
|
+
export interface ReadManyBatchError {
|
|
95
|
+
batch: number;
|
|
96
|
+
paths: string[];
|
|
97
|
+
failure: import("./outcomes.js").ConnectFailure<import("./outcomes.js").CollectionQueryProblemCode>;
|
|
98
|
+
}
|
|
99
|
+
export interface ReadManyResult<Frontmatter extends JsonObject = JsonObject> {
|
|
100
|
+
/** One entry per input path, in input order, including duplicates. */
|
|
101
|
+
results: ReadManyEntry<Frontmatter>[];
|
|
102
|
+
/** A failed batch is not proof of missing records; its entries are errors. */
|
|
103
|
+
errors: ReadManyBatchError[];
|
|
104
|
+
}
|
|
67
105
|
export interface RecordDocument<Frontmatter extends JsonObject = JsonObject> {
|
|
68
106
|
path: string;
|
|
69
107
|
revision: string;
|
|
@@ -189,10 +227,12 @@ export interface QueryResult<Record extends JsonObject = JsonObject> {
|
|
|
189
227
|
};
|
|
190
228
|
}
|
|
191
229
|
export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
|
|
192
|
-
/** Initial page size. Cursor
|
|
230
|
+
/** Initial page size. Cursor continuations use this pinned size. */
|
|
193
231
|
firstPageSize?: number;
|
|
194
|
-
/**
|
|
232
|
+
/** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
|
|
195
233
|
pageSize?: number;
|
|
234
|
+
/** Total rows to deliver, not a page size. Zero makes no data requests. */
|
|
235
|
+
maxResults?: number;
|
|
196
236
|
signal?: AbortSignal;
|
|
197
237
|
/** Independent budget for each page requested by this caller-driven iterator. */
|
|
198
238
|
pageTimeoutMs?: number | null;
|
|
@@ -200,10 +240,12 @@ export interface QueryPagesOptions<Record extends JsonObject = JsonObject> {
|
|
|
200
240
|
onProgress?: (page: QueryPage<Record>) => void;
|
|
201
241
|
}
|
|
202
242
|
export interface QueryAllOptions<Record extends JsonObject = JsonObject> extends ConnectRequestOptions {
|
|
203
|
-
/** Initial page size. Cursor
|
|
243
|
+
/** Initial page size. Cursor continuations use this pinned size. */
|
|
204
244
|
firstPageSize?: number;
|
|
205
|
-
/**
|
|
245
|
+
/** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
|
|
206
246
|
pageSize?: number;
|
|
247
|
+
/** Total rows to return, independent of page size. */
|
|
248
|
+
maxResults?: number;
|
|
207
249
|
onProgress?: (page: QueryPage<Record>) => void;
|
|
208
250
|
}
|
|
209
251
|
export interface QueryPage<Record extends JsonObject = JsonObject> {
|
|
@@ -404,8 +446,12 @@ export interface ExecuteViewInput {
|
|
|
404
446
|
render?: boolean;
|
|
405
447
|
}
|
|
406
448
|
export interface SavedViewPagesOptions<Frontmatter extends JsonObject = JsonObject> {
|
|
449
|
+
/** Initial page size, pinned for cursor continuations. */
|
|
407
450
|
firstPageSize?: number;
|
|
451
|
+
/** Initial cursor size (default 200), unless firstPageSize is set. Offset continuations use this size (default 1,000). Authorities may cap it. */
|
|
408
452
|
pageSize?: number;
|
|
453
|
+
/** Total rows to deliver, not a page size. Zero makes no data requests. */
|
|
454
|
+
maxResults?: number;
|
|
409
455
|
signal?: AbortSignal;
|
|
410
456
|
/** Independent budget for each page requested by this caller-driven iterator. */
|
|
411
457
|
pageTimeoutMs?: number | null;
|
|
@@ -685,12 +731,87 @@ export interface MdbaseWatchSubscription {
|
|
|
685
731
|
subscribe(listener: (change: CollectionChange) => void, onStatus?: (status: WatchStatus) => void, onProblem?: (problem: ConnectProblem) => void): () => void;
|
|
686
732
|
close(): void;
|
|
687
733
|
}
|
|
688
|
-
export interface
|
|
734
|
+
export interface DescribeOptions extends ConnectRequestOptions {
|
|
735
|
+
/** Bypass the settled description cache (concurrent requests still share a load). */
|
|
736
|
+
fresh?: boolean;
|
|
737
|
+
}
|
|
738
|
+
interface ChangeBase {
|
|
689
739
|
cursor: number;
|
|
740
|
+
/** Original authority event ID. Prefer kind for application logic. */
|
|
690
741
|
type: string;
|
|
691
742
|
occurredAt: string;
|
|
692
743
|
payload: JsonObject;
|
|
744
|
+
/** Unmodified wire event, including authority-specific metadata. */
|
|
745
|
+
raw: WireCollectionChange;
|
|
746
|
+
}
|
|
747
|
+
export interface RecordChangeMetadata {
|
|
748
|
+
revision?: string;
|
|
749
|
+
previousRevision?: string;
|
|
750
|
+
types?: string[];
|
|
751
|
+
previousTypes?: string[];
|
|
752
|
+
/** Authority field identifiers: local JSON Pointers, hosted top-level keys. Preserved, not guessed. */
|
|
753
|
+
changedFields?: string[];
|
|
754
|
+
bodyChanged?: boolean;
|
|
755
|
+
/** Hosted authorities may supply frontmatter; local authorities need not. */
|
|
756
|
+
before?: JsonObject;
|
|
757
|
+
after?: JsonObject;
|
|
758
|
+
}
|
|
759
|
+
interface ResourceChangeBase extends ChangeBase {
|
|
760
|
+
path?: string;
|
|
761
|
+
name?: string;
|
|
762
|
+
revision?: string;
|
|
763
|
+
previousRevision?: string;
|
|
693
764
|
}
|
|
765
|
+
export type CollectionChange = (ChangeBase & RecordChangeMetadata & {
|
|
766
|
+
kind: "record.created";
|
|
767
|
+
path: string;
|
|
768
|
+
}) | (ChangeBase & RecordChangeMetadata & {
|
|
769
|
+
kind: "record.updated";
|
|
770
|
+
path: string;
|
|
771
|
+
}) | (ChangeBase & RecordChangeMetadata & {
|
|
772
|
+
kind: "record.deleted";
|
|
773
|
+
path: string;
|
|
774
|
+
}) | (ChangeBase & RecordChangeMetadata & {
|
|
775
|
+
kind: "record.renamed";
|
|
776
|
+
from: string;
|
|
777
|
+
to: string;
|
|
778
|
+
}) | (ResourceChangeBase & {
|
|
779
|
+
kind: "schema.changed";
|
|
780
|
+
}) | (ResourceChangeBase & {
|
|
781
|
+
kind: "config.changed";
|
|
782
|
+
}) | (ResourceChangeBase & {
|
|
783
|
+
kind: "contract.changed";
|
|
784
|
+
}) | (ResourceChangeBase & {
|
|
785
|
+
kind: "view.changed";
|
|
786
|
+
}) | (ChangeBase & {
|
|
787
|
+
kind: "file.changed";
|
|
788
|
+
path: string;
|
|
789
|
+
revision?: string;
|
|
790
|
+
previousRevision?: string;
|
|
791
|
+
}) | (ChangeBase & {
|
|
792
|
+
kind: "file.put";
|
|
793
|
+
file: import("./file-descriptor.js").CollectionFileDescriptor;
|
|
794
|
+
}) | (ChangeBase & {
|
|
795
|
+
kind: "file.removed";
|
|
796
|
+
fileId: string;
|
|
797
|
+
previousPath?: string;
|
|
798
|
+
revision?: string;
|
|
799
|
+
}) | (ChangeBase & {
|
|
800
|
+
kind: "gap";
|
|
801
|
+
reason?: string;
|
|
802
|
+
}) | (ChangeBase & {
|
|
803
|
+
kind: "unknown";
|
|
804
|
+
reason: "unrecognized_type" | "invalid_payload";
|
|
805
|
+
})
|
|
806
|
+
/** Synthesized from a reset page, not a new authority event ID. Watch still terminates with change_cursor_reset. */
|
|
807
|
+
| {
|
|
808
|
+
kind: "reset";
|
|
809
|
+
cursor: number;
|
|
810
|
+
type: "reset";
|
|
811
|
+
occurredAt: null;
|
|
812
|
+
payload: JsonObject;
|
|
813
|
+
raw: WireCollectionChangesPage;
|
|
814
|
+
};
|
|
694
815
|
export interface CollectionChangesPage {
|
|
695
816
|
events: CollectionChange[];
|
|
696
817
|
cursor: number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Owns the latest single-use cursor, including while an iterator is paused at yield. */
|
|
2
|
+
export declare function queryCursorLease(release: (cursor: string) => Promise<void>, signal?: AbortSignal, initialCursor?: string): {
|
|
3
|
+
startRequest(): void;
|
|
4
|
+
finishRequest(nextCursor?: string): void;
|
|
5
|
+
dispose(): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function resultCap(value?: number): number;
|
|
8
|
+
export declare function nonNegativeInteger(value: unknown, fallback: number): number;
|
|
9
|
+
export declare function positiveInteger(value: unknown, fallback: number): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Owns the latest single-use cursor, including while an iterator is paused at yield. */
|
|
2
|
+
export function queryCursorLease(release, signal, initialCursor) {
|
|
3
|
+
let cursor = initialCursor;
|
|
4
|
+
let inFlight = false;
|
|
5
|
+
const close = () => {
|
|
6
|
+
if (!cursor)
|
|
7
|
+
return;
|
|
8
|
+
const owned = cursor;
|
|
9
|
+
cursor = undefined;
|
|
10
|
+
// Collection cleanup has an independent bounded budget, never the data signal.
|
|
11
|
+
void release(owned).catch(() => undefined);
|
|
12
|
+
};
|
|
13
|
+
const abort = () => {
|
|
14
|
+
// A consumed token can rotate while the request settles. Release the returned
|
|
15
|
+
// token instead; a paused iterator has no request and closes immediately.
|
|
16
|
+
if (!inFlight)
|
|
17
|
+
close();
|
|
18
|
+
};
|
|
19
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
20
|
+
return {
|
|
21
|
+
startRequest() { inFlight = true; },
|
|
22
|
+
finishRequest(nextCursor) {
|
|
23
|
+
if (nextCursor)
|
|
24
|
+
cursor = nextCursor;
|
|
25
|
+
inFlight = false;
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
close();
|
|
28
|
+
},
|
|
29
|
+
dispose() {
|
|
30
|
+
signal?.removeEventListener("abort", abort);
|
|
31
|
+
close();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function resultCap(value) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return Infinity;
|
|
38
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
39
|
+
throw new TypeError("maxResults must be a non-negative safe integer.");
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export function nonNegativeInteger(value, fallback) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
45
|
+
? Math.max(0, Math.floor(value))
|
|
46
|
+
: fallback;
|
|
47
|
+
}
|
|
48
|
+
export function positiveInteger(value, fallback) {
|
|
49
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
50
|
+
? Math.floor(value)
|
|
51
|
+
: fallback;
|
|
52
|
+
}
|
package/dist/query-pagination.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connectProblem } from "./errors.js";
|
|
2
2
|
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
3
|
+
import { nonNegativeInteger, positiveInteger, queryCursorLease, resultCap } from "./query-pagination-internals.js";
|
|
3
4
|
export async function* coordinatedQueryPages(query, releaseQueryCursor, input = {}, options = {}) {
|
|
4
5
|
const { offset: requestedOffset, limit: requestedLimit, cursor: requestedCursor, pagination: requestedPagination, snapshot: requestedSnapshot, ...criteria } = input;
|
|
5
6
|
let offset = nonNegativeInteger(requestedOffset, 0);
|
|
@@ -7,12 +8,14 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
7
8
|
const pageSize = positiveInteger(options.pageSize ?? requestedLimit, 1_000);
|
|
8
9
|
let cursor = requestedCursor;
|
|
9
10
|
let cursorMode = requestedCursor !== undefined;
|
|
10
|
-
|
|
11
|
+
const maxResults = resultCap(options.maxResults);
|
|
12
|
+
const lease = queryCursorLease(releaseQueryCursor, options.signal, requestedCursor);
|
|
11
13
|
let snapshot = requestedSnapshot;
|
|
12
14
|
let loaded = 0;
|
|
13
15
|
let pageNumber = 0;
|
|
14
16
|
try {
|
|
15
|
-
while (!options.signal?.aborted) {
|
|
17
|
+
while (!options.signal?.aborted && loaded < maxResults) {
|
|
18
|
+
lease.startRequest();
|
|
16
19
|
const pageCursor = cursor;
|
|
17
20
|
const pageRequestOptions = {
|
|
18
21
|
signal: options.signal,
|
|
@@ -25,8 +28,11 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
25
28
|
&& requestedSnapshot === undefined;
|
|
26
29
|
let queried = await query({
|
|
27
30
|
...criteria,
|
|
28
|
-
//
|
|
29
|
-
|
|
31
|
+
// Initial size is portable across authorities; continuations use the
|
|
32
|
+
// pinned size. Do not infer variable-size support from operation errors.
|
|
33
|
+
...(!cursorMode
|
|
34
|
+
? { limit: Math.min(pageNumber === 0 ? firstPageSize : pageSize, maxResults - loaded) }
|
|
35
|
+
: {}),
|
|
30
36
|
...(cursorMode
|
|
31
37
|
? (pageCursor ? { cursor: pageCursor } : { pagination: "cursor" })
|
|
32
38
|
: {
|
|
@@ -43,17 +49,23 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
43
49
|
// Cursor pagination is a read-only capability probe for authorities
|
|
44
50
|
// predating generation cursors. Retry the first page without that
|
|
45
51
|
// optional field only when the authority rejected the operation
|
|
46
|
-
// schema; explicit cursor requests remain strict.
|
|
52
|
+
// schema; explicit cursor requests remain strict. This existing fallback
|
|
53
|
+
// serves pre-cursor desktop authorities; remove when those are unsupported.
|
|
47
54
|
if (automaticCursorProbe
|
|
55
|
+
&& !options.signal?.aborted
|
|
48
56
|
&& !queried.ok
|
|
49
57
|
&& queried.problem.code === "operation_invalid") {
|
|
50
58
|
queried = await query({
|
|
51
59
|
...criteria,
|
|
52
|
-
limit: firstPageSize,
|
|
60
|
+
limit: Math.min(firstPageSize, maxResults - loaded),
|
|
53
61
|
offset
|
|
54
62
|
}, pageRequestOptions);
|
|
55
63
|
}
|
|
64
|
+
lease.finishRequest(queried.ok ? queried.value.meta?.cursor : undefined);
|
|
65
|
+
if (options.signal?.aborted)
|
|
66
|
+
return;
|
|
56
67
|
if (!queried.ok) {
|
|
68
|
+
lease.dispose();
|
|
57
69
|
yield queried;
|
|
58
70
|
return;
|
|
59
71
|
}
|
|
@@ -62,27 +74,29 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
62
74
|
if (returnedCursor) {
|
|
63
75
|
cursorMode = true;
|
|
64
76
|
cursor = returnedCursor;
|
|
65
|
-
cursorToRelease = returnedCursor;
|
|
66
77
|
}
|
|
67
78
|
else if (cursorMode) {
|
|
68
79
|
cursor = undefined;
|
|
69
|
-
cursorToRelease = pageCursor;
|
|
70
80
|
}
|
|
71
81
|
if (cursorMode && result.meta?.hasMore && !returnedCursor) {
|
|
82
|
+
lease.dispose();
|
|
72
83
|
yield connectFailure(connectProblem("invalid_operation_response", "The collection authority omitted the cursor required for the next query page."));
|
|
73
84
|
return;
|
|
74
85
|
}
|
|
75
86
|
const returnedSnapshot = result.meta?.snapshot;
|
|
76
87
|
if (!cursorMode && snapshot && returnedSnapshot && snapshot !== returnedSnapshot) {
|
|
88
|
+
lease.dispose();
|
|
77
89
|
yield connectFailure(connectProblem("query_snapshot_changed", "The collection query snapshot changed while paging. Refresh the query before continuing."));
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
80
92
|
if (!cursorMode && !snapshot && returnedSnapshot)
|
|
81
93
|
snapshot = returnedSnapshot;
|
|
82
|
-
|
|
83
|
-
const
|
|
94
|
+
const remaining = maxResults - loaded;
|
|
95
|
+
const results = result.results.length > remaining ? result.results.slice(0, remaining) : result.results;
|
|
96
|
+
loaded += results.length;
|
|
97
|
+
const complete = loaded >= maxResults || !result.meta?.hasMore || result.results.length === 0;
|
|
84
98
|
const page = {
|
|
85
|
-
results
|
|
99
|
+
results,
|
|
86
100
|
...(result.meta ? { meta: result.meta } : {}),
|
|
87
101
|
page: pageNumber,
|
|
88
102
|
offset,
|
|
@@ -91,6 +105,8 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
91
105
|
...(returnedCursor ? { cursor: returnedCursor } : {}),
|
|
92
106
|
...(!cursorMode && snapshot ? { snapshot } : {})
|
|
93
107
|
};
|
|
108
|
+
if (complete)
|
|
109
|
+
lease.dispose();
|
|
94
110
|
options.onProgress?.(page);
|
|
95
111
|
yield connectSuccess(page, queried.diagnostics);
|
|
96
112
|
if (complete)
|
|
@@ -100,20 +116,6 @@ export async function* coordinatedQueryPages(query, releaseQueryCursor, input =
|
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
finally {
|
|
103
|
-
|
|
104
|
-
// Cleanup has its own bounded request. It must not delay delivery of
|
|
105
|
-
// completed pages or consume the caller's data deadline.
|
|
106
|
-
void releaseQueryCursor(cursorToRelease).catch(() => undefined);
|
|
107
|
-
}
|
|
119
|
+
lease.dispose();
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
|
-
function nonNegativeInteger(value, fallback) {
|
|
111
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
112
|
-
? Math.max(0, Math.floor(value))
|
|
113
|
-
: fallback;
|
|
114
|
-
}
|
|
115
|
-
function positiveInteger(value, fallback) {
|
|
116
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
117
|
-
? Math.floor(value)
|
|
118
|
-
: fallback;
|
|
119
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface LinksToOptions {
|
|
2
|
+
/** The field is a link list instead of a scalar link. Uses CEL exists(). */
|
|
3
|
+
multiple?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A CEL predicate comparing an authority-resolved link with an exact collection
|
|
7
|
+
* path. `field` is a literal top-level effective-frontmatter key, not an expression.
|
|
8
|
+
* Missing/null fields and unresolved links do not match. Resolution (including
|
|
9
|
+
* configured IDs, relative paths and duplicate names) belongs to the authority.
|
|
10
|
+
*/
|
|
11
|
+
export declare function linksTo(field: string, path: string, options?: LinksToOptions): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CEL predicate comparing an authority-resolved link with an exact collection
|
|
3
|
+
* path. `field` is a literal top-level effective-frontmatter key, not an expression.
|
|
4
|
+
* Missing/null fields and unresolved links do not match. Resolution (including
|
|
5
|
+
* configured IDs, relative paths and duplicate names) belongs to the authority.
|
|
6
|
+
*/
|
|
7
|
+
export function linksTo(field, path, options = {}) {
|
|
8
|
+
const key = JSON.stringify(field);
|
|
9
|
+
const value = `record[${key}]`;
|
|
10
|
+
const target = JSON.stringify(path);
|
|
11
|
+
const matches = (link) => `${link} != null && ${link}.asFile() != null && ${link}.asFile().file.path == ${target}`;
|
|
12
|
+
return `${key} in record && ${options.multiple
|
|
13
|
+
? `${value} != null && ${value}.exists(link, ${matches("link")})`
|
|
14
|
+
: matches(value)}`;
|
|
15
|
+
}
|
|
@@ -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 { QueryAllOptions, QueryInput, QueryResult, ReadManyOptions, ReadManyResult } from "./operation-types.js";
|
|
4
|
+
type QueryAll<Frontmatter extends JsonObject> = (input: QueryInput, options: QueryAllOptions<Frontmatter>) => Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
5
|
+
export declare function queryReadMany<Frontmatter extends JsonObject>(queryAll: QueryAll<Frontmatter>, paths: readonly string[], options: ReadManyOptions, defaultTimeoutMs: number | null): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { MdbaseConnectError } from "./errors.js";
|
|
2
|
+
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
3
|
+
import { createRequestBudget, requestAbortReason } from "./request-budget.js";
|
|
4
|
+
export async function queryReadMany(queryAll, paths, options, defaultTimeoutMs) {
|
|
5
|
+
const batchSize = boundedInteger(options.batchSize ?? 100, 1_000, "batchSize");
|
|
6
|
+
const concurrency = boundedInteger(options.concurrency ?? 4, 4, "concurrency");
|
|
7
|
+
if (options.coordination?.latestWins) {
|
|
8
|
+
throw new TypeError("readMany batches cannot use latestWins coordination.");
|
|
9
|
+
}
|
|
10
|
+
const inputs = [...paths];
|
|
11
|
+
const unique = [...new Set(inputs)];
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
const errors = [];
|
|
14
|
+
const budget = createRequestBudget(options, defaultTimeoutMs);
|
|
15
|
+
let nextBatch = 0;
|
|
16
|
+
const batchCount = Math.ceil(unique.length / batchSize);
|
|
17
|
+
const batchDiagnostics = [];
|
|
18
|
+
const worker = async () => {
|
|
19
|
+
while (!budget.signal.aborted && nextBatch < batchCount) {
|
|
20
|
+
const batch = nextBatch++;
|
|
21
|
+
const selected = unique.slice(batch * batchSize, (batch + 1) * batchSize);
|
|
22
|
+
const outcome = await queryAll({
|
|
23
|
+
where: `file.path in ${JSON.stringify(selected)}`,
|
|
24
|
+
...(options.types ? { types: options.types } : {}),
|
|
25
|
+
...(options.includeBody === undefined ? {} : { includeBody: options.includeBody }),
|
|
26
|
+
...(options.frontmatterMode ? { frontmatterMode: options.frontmatterMode } : {})
|
|
27
|
+
}, {
|
|
28
|
+
pageSize: selected.length,
|
|
29
|
+
signal: budget.signal,
|
|
30
|
+
timeoutMs: null,
|
|
31
|
+
coordination: options.coordination
|
|
32
|
+
});
|
|
33
|
+
if (!outcome.ok) {
|
|
34
|
+
errors.push({ batch, paths: selected, failure: outcome });
|
|
35
|
+
for (const path of selected)
|
|
36
|
+
entries.set(path, { status: "error", path, batch });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
batchDiagnostics[batch] = outcome.diagnostics;
|
|
40
|
+
const records = new Map(outcome.value.results.map(record => [record.path, record]));
|
|
41
|
+
for (const path of selected) {
|
|
42
|
+
const record = records.get(path);
|
|
43
|
+
entries.set(path, record ? { status: "found", path, record } : { status: "missing", path });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, batchCount) }, worker));
|
|
49
|
+
if (budget.signal.aborted)
|
|
50
|
+
throw requestAbortReason(budget.signal);
|
|
51
|
+
return connectSuccess({
|
|
52
|
+
results: inputs.map(path => entries.get(path)),
|
|
53
|
+
errors: errors.sort((a, b) => a.batch - b.batch)
|
|
54
|
+
}, batchDiagnostics.flat());
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error instanceof MdbaseConnectError)
|
|
58
|
+
return connectFailure(error.problem);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
budget.dispose();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function boundedInteger(value, max, name) {
|
|
66
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
67
|
+
throw new TypeError(`${name} must be an integer between 1 and ${max}.`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
package/dist/record-session.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { type ConnectOutcome } from "./outcomes.js";
|
|
|
7
7
|
* - `saving`: a write or exact recovery is in flight.
|
|
8
8
|
* - `conflict`: a newer record changed what was edited locally; see `remote`.
|
|
9
9
|
* - `recovery`: a write's outcome is unknown; no new write is sent until it is recovered.
|
|
10
|
-
* - `error`:
|
|
10
|
+
* - `error`: a write or refresh failed; local changes and the last accepted record are kept.
|
|
11
11
|
* - `deleted`: the record no longer exists; local changes are kept.
|
|
12
12
|
*/
|
|
13
13
|
export type MdbaseRecordSessionState = "saved" | "unsaved" | "saving" | "conflict" | "recovery" | "error" | "deleted";
|
|
@@ -51,7 +51,7 @@ export interface MdbaseRecordSessionSnapshot<R> {
|
|
|
51
51
|
readonly remote: R | null;
|
|
52
52
|
/** Local changes not yet acknowledged, including a write in flight or awaiting recovery. */
|
|
53
53
|
readonly dirty: boolean;
|
|
54
|
-
/** Why
|
|
54
|
+
/** Why a write, recovery or refresh failed. */
|
|
55
55
|
readonly problem: ConnectProblem | null;
|
|
56
56
|
/** The interrupted write's request ID, while `state` is `recovery`. */
|
|
57
57
|
readonly pendingRequestId?: string;
|
|
@@ -76,6 +76,7 @@ export declare class MdbaseRecordSession<R> {
|
|
|
76
76
|
private patch;
|
|
77
77
|
private remote;
|
|
78
78
|
private problem;
|
|
79
|
+
private refreshProblem;
|
|
79
80
|
private deleted;
|
|
80
81
|
private busy;
|
|
81
82
|
private pending;
|
|
@@ -87,6 +88,7 @@ export declare class MdbaseRecordSession<R> {
|
|
|
87
88
|
private inFlight;
|
|
88
89
|
private timer;
|
|
89
90
|
private lastEdit;
|
|
91
|
+
private disposed;
|
|
90
92
|
private readonly idleMs;
|
|
91
93
|
private current;
|
|
92
94
|
constructor(record: R, adapter: MdbaseRecordSessionAdapter<R>, options?: MdbaseRecordSessionOptions);
|
|
@@ -104,6 +106,8 @@ export declare class MdbaseRecordSession<R> {
|
|
|
104
106
|
restore(draft: {
|
|
105
107
|
body: string;
|
|
106
108
|
baseBody?: string;
|
|
109
|
+
patch?: JsonObject;
|
|
110
|
+
baseFrontmatter?: JsonObject;
|
|
107
111
|
}): void;
|
|
108
112
|
/**
|
|
109
113
|
* Continue a write interrupted before this session existed, such as one
|
|
@@ -111,6 +115,8 @@ export declare class MdbaseRecordSession<R> {
|
|
|
111
115
|
* settles; with autosave, recovery is attempted after one idle interval.
|
|
112
116
|
*/
|
|
113
117
|
resumeRecovery(requestId: string): void;
|
|
118
|
+
/** Stop automatic writes and subscriptions when the owning view closes. In-flight writes still settle. */
|
|
119
|
+
dispose(): void;
|
|
114
120
|
/** Schedule an autosave one idle interval from now. */
|
|
115
121
|
autosave(): void;
|
|
116
122
|
/**
|