@mdbase-dev/connect 0.1.0-beta.123 → 0.1.0-beta.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -2
- package/dist/application-session.js +9 -3
- package/dist/authority-features.d.ts +29 -0
- package/dist/authority-features.js +84 -0
- 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.js +2 -9
- package/dist/collection-client.d.ts +7 -2
- package/dist/collection-client.js +76 -97
- package/dist/connection.d.ts +8 -1
- package/dist/connection.js +55 -116
- package/dist/description-cache.js +1 -28
- package/dist/file-descriptor.d.ts +2 -0
- package/dist/file-descriptor.js +17 -0
- package/dist/file-metadata.d.ts +21 -0
- package/dist/file-metadata.js +112 -0
- package/dist/files.d.ts +7 -8
- package/dist/files.js +13 -45
- package/dist/index.d.ts +2 -2
- package/dist/local-file-transport.js +18 -16
- package/dist/operation-types.d.ts +44 -16
- package/dist/query-pagination.d.ts +7 -3
- package/dist/query-pagination.js +3 -1
- package/dist/query-wire.d.ts +61 -0
- package/dist/query-wire.js +110 -0
- package/dist/read-many.d.ts +4 -3
- package/dist/read-many.js +160 -55
- package/dist/watch-policy.d.ts +22 -1
- package/dist/watch-policy.js +104 -0
- package/package.json +3 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CollectionDescription as WireDescription, DataContractViewIdentity as WireIdentity, JsonObject, QueryRecord as WireQueryRecord } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { CollectionDescription, DataContractViewIdentity, QueryInput, QueryMetadataInput, QueryMetadataResult, QueryRecord, QueryResult } from "./operation-types.js";
|
|
3
|
+
export interface WireQueryResult<Frontmatter extends JsonObject> {
|
|
4
|
+
output?: "metadata";
|
|
5
|
+
results: Array<WireQueryRecord<Frontmatter> | import("@mdbase-dev/connect-protocol").QueryMetadataRecord>;
|
|
6
|
+
meta?: {
|
|
7
|
+
total_count?: number | null;
|
|
8
|
+
total_count_outcome?: {
|
|
9
|
+
status: "deferred";
|
|
10
|
+
budget: "eager_summary_rows";
|
|
11
|
+
limit: number;
|
|
12
|
+
};
|
|
13
|
+
has_more: boolean;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
snapshot?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function wireQueryInput(input: QueryInput | QueryMetadataInput): {
|
|
19
|
+
output?: "metadata" | undefined;
|
|
20
|
+
types?: string[] | undefined;
|
|
21
|
+
timezone?: string | undefined;
|
|
22
|
+
context?: {
|
|
23
|
+
this: {
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
} | undefined;
|
|
27
|
+
projections?: {
|
|
28
|
+
[k: string]: {
|
|
29
|
+
expr: string;
|
|
30
|
+
description?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
} | undefined;
|
|
33
|
+
where?: string | undefined;
|
|
34
|
+
select?: (string | {
|
|
35
|
+
name: string;
|
|
36
|
+
expr: string;
|
|
37
|
+
label?: string | undefined;
|
|
38
|
+
description?: string | undefined;
|
|
39
|
+
})[] | undefined;
|
|
40
|
+
order_by?: import("./operation-types.js").QueryOrder[] | undefined;
|
|
41
|
+
group_by?: import("./operation-types.js").QueryOrder[] | undefined;
|
|
42
|
+
summary_functions?: {
|
|
43
|
+
[k: string]: {
|
|
44
|
+
expr: string;
|
|
45
|
+
description?: string | undefined;
|
|
46
|
+
};
|
|
47
|
+
} | undefined;
|
|
48
|
+
summaries?: import("./operation-types.js").QuerySummary[] | undefined;
|
|
49
|
+
limit?: number | undefined;
|
|
50
|
+
offset?: number | undefined;
|
|
51
|
+
pagination?: "cursor" | undefined;
|
|
52
|
+
cursor?: string | undefined;
|
|
53
|
+
snapshot?: string | undefined;
|
|
54
|
+
include_body?: boolean | undefined;
|
|
55
|
+
frontmatter_mode?: "both" | "effective" | "persisted" | undefined;
|
|
56
|
+
contract?: import("./operation-types.js").DataContractSelector | undefined;
|
|
57
|
+
};
|
|
58
|
+
export declare function wireDataContractIdentity(value: WireIdentity): DataContractViewIdentity;
|
|
59
|
+
export declare function wireQueryRecord<Frontmatter extends JsonObject>(value: WireQueryRecord<Frontmatter>): QueryRecord<Frontmatter>;
|
|
60
|
+
export declare function wireQueryResult<Frontmatter extends JsonObject>(value: WireQueryResult<Frontmatter>, output: "metadata" | undefined): QueryResult<Frontmatter> | QueryMetadataResult;
|
|
61
|
+
export declare function wireCollectionDescription(value: WireDescription): CollectionDescription;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { authorityCapabilities } from "./authority-features.js";
|
|
2
|
+
import { connectError } from "./errors.js";
|
|
3
|
+
export function wireQueryInput(input) {
|
|
4
|
+
return {
|
|
5
|
+
...(input.output ? { output: input.output } : {}),
|
|
6
|
+
...(input.types ? { types: input.types } : {}),
|
|
7
|
+
...(input.timezone ? { timezone: input.timezone } : {}),
|
|
8
|
+
...(input.context ? { context: input.context } : {}),
|
|
9
|
+
...(input.projections ? {
|
|
10
|
+
projections: Object.fromEntries(Object.entries(input.projections).map(([name, projection]) => [
|
|
11
|
+
name, { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
|
|
12
|
+
]))
|
|
13
|
+
} : {}),
|
|
14
|
+
...(input.where ? { where: input.where } : {}),
|
|
15
|
+
...(input.select ? {
|
|
16
|
+
select: input.select.map(selection => typeof selection === "string" ? selection : {
|
|
17
|
+
name: selection.name, expr: selection.expression,
|
|
18
|
+
...(selection.label ? { label: selection.label } : {}),
|
|
19
|
+
...(selection.description ? { description: selection.description } : {})
|
|
20
|
+
})
|
|
21
|
+
} : {}),
|
|
22
|
+
...(input.orderBy ? { order_by: input.orderBy } : {}),
|
|
23
|
+
...(input.groupBy ? { group_by: input.groupBy } : {}),
|
|
24
|
+
...(input.summaryFunctions ? {
|
|
25
|
+
summary_functions: Object.fromEntries(Object.entries(input.summaryFunctions).map(([name, projection]) => [
|
|
26
|
+
name, { expr: projection.expression, ...(projection.description ? { description: projection.description } : {}) }
|
|
27
|
+
]))
|
|
28
|
+
} : {}),
|
|
29
|
+
...(input.summaries ? { summaries: input.summaries } : {}),
|
|
30
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
31
|
+
...(input.offset === undefined ? {} : { offset: input.offset }),
|
|
32
|
+
...(input.pagination ? { pagination: input.pagination } : {}),
|
|
33
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
34
|
+
...(input.snapshot ? { snapshot: input.snapshot } : {}),
|
|
35
|
+
...(input.includeBody === undefined ? {} : { include_body: input.includeBody }),
|
|
36
|
+
...(input.frontmatterMode ? { frontmatter_mode: input.frontmatterMode } : {}),
|
|
37
|
+
...(input.contract ? { contract: input.contract } : {})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function wireDataContractIdentity(value) {
|
|
41
|
+
return {
|
|
42
|
+
id: value.id, version: value.version, digest: value.digest,
|
|
43
|
+
type: value.type, implementationDigest: value.implementation_digest
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function wireQueryRecord(value) {
|
|
47
|
+
const { effective_frontmatter, contract, ...record } = value;
|
|
48
|
+
return {
|
|
49
|
+
...record,
|
|
50
|
+
...(effective_frontmatter === undefined ? {} : { effectiveFrontmatter: effective_frontmatter }),
|
|
51
|
+
...(contract ? { contract: wireDataContractIdentity(contract) } : {})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function metadataRecord(value) {
|
|
55
|
+
if (!value || typeof value.path !== "string" || typeof value.revision !== "string" || !value.revision
|
|
56
|
+
|| !Array.isArray(value.types) || value.types.some(type => typeof type !== "string")
|
|
57
|
+
|| !value.values || typeof value.values !== "object" || Array.isArray(value.values)
|
|
58
|
+
|| Object.keys(value).some(key => !["path", "types", "revision", "values", "contract"].includes(key))
|
|
59
|
+
|| (value.contract !== undefined && (!value.contract
|
|
60
|
+
|| ["id", "version", "digest", "type", "implementation_digest"].some(key => typeof value.contract[key] !== "string")))) {
|
|
61
|
+
throw connectError("invalid_operation_response", "The authority returned an invalid revision-bearing metadata row.");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
path: value.path, types: value.types, revision: value.revision, values: value.values,
|
|
65
|
+
...(value.contract ? { contract: wireDataContractIdentity(value.contract) } : {})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function wireQueryResult(value, output) {
|
|
69
|
+
if (!value || !Array.isArray(value.results) || value.output !== output) {
|
|
70
|
+
throw connectError("invalid_operation_response", "The authority returned an invalid query output mode.");
|
|
71
|
+
}
|
|
72
|
+
const meta = value.meta ? {
|
|
73
|
+
...(typeof value.meta.total_count === "number" ? { totalCount: value.meta.total_count } : {}),
|
|
74
|
+
...(value.meta.total_count_outcome === undefined ? {} : { totalCountOutcome: value.meta.total_count_outcome }),
|
|
75
|
+
hasMore: value.meta.has_more,
|
|
76
|
+
...(value.meta.cursor ? { cursor: value.meta.cursor } : {}),
|
|
77
|
+
...(value.meta.snapshot ? { snapshot: value.meta.snapshot } : {})
|
|
78
|
+
} : undefined;
|
|
79
|
+
if (output === "metadata")
|
|
80
|
+
return {
|
|
81
|
+
output, results: value.results.map(row => metadataRecord(row)),
|
|
82
|
+
...(meta ? { meta } : {})
|
|
83
|
+
};
|
|
84
|
+
return { results: value.results.map(row => wireQueryRecord(row)), ...(meta ? { meta } : {}) };
|
|
85
|
+
}
|
|
86
|
+
export function wireCollectionDescription(value) {
|
|
87
|
+
if (!value || value.protocol_version !== 1 || typeof value.collection_id !== "string"
|
|
88
|
+
|| typeof value.display_name !== "string" || typeof value.spec_version !== "string"
|
|
89
|
+
|| !Array.isArray(value.operations) || !Array.isArray(value.types) || !Array.isArray(value.contracts)) {
|
|
90
|
+
throw connectError("invalid_operation_response", "The authority returned an invalid collection description.");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
authorityCapabilities: authorityCapabilities(value.authority_capabilities),
|
|
94
|
+
protocolVersion: value.protocol_version, collectionId: value.collection_id,
|
|
95
|
+
displayName: value.display_name, specVersion: value.spec_version,
|
|
96
|
+
operations: value.operations, changeCursor: value.change_cursor, types: value.types,
|
|
97
|
+
contracts: value.contracts.map(contract => ({
|
|
98
|
+
contractType: contract.contract_type, id: contract.id, version: contract.version,
|
|
99
|
+
digest: contract.digest, schema: contract.schema,
|
|
100
|
+
...(contract.binding_schema ? { bindingSchema: contract.binding_schema } : {}),
|
|
101
|
+
implementations: contract.implementations.map(implementation => ({
|
|
102
|
+
typeName: implementation.type_name, typeVersion: implementation.type_version,
|
|
103
|
+
...(implementation.type_path ? { typePath: implementation.type_path } : {}),
|
|
104
|
+
digest: implementation.digest, fields: implementation.fields,
|
|
105
|
+
...(implementation.binding ? { binding: implementation.binding } : {})
|
|
106
|
+
}))
|
|
107
|
+
})),
|
|
108
|
+
...(value.configuration ? { configuration: value.configuration } : {})
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/read-many.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type JsonObject, type ReadManyDocumentsResult } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { type CollectionQueryProblemCode, type ConnectOutcome } from "./outcomes.js";
|
|
3
|
-
import type { QueryAllOptions, QueryInput, QueryResult, ReadManyOptions, ReadManyResult } from "./operation-types.js";
|
|
3
|
+
import type { ConnectRequestOptions, QueryAllOptions, QueryInput, QueryResult, ReadManyOptions, ReadManyResult } from "./operation-types.js";
|
|
4
4
|
type QueryAll<Frontmatter extends JsonObject> = (input: QueryInput, options: QueryAllOptions<Frontmatter>) => Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
5
|
-
|
|
5
|
+
type ReadDocuments<Frontmatter extends JsonObject> = (paths: string[], includeBody: boolean, options: ConnectRequestOptions) => Promise<ConnectOutcome<ReadManyDocumentsResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
6
|
+
export declare function readMany<Frontmatter extends JsonObject>(queryAll: QueryAll<Frontmatter>, readDocuments: ReadDocuments<Frontmatter>, supports: ((id: string, options: ConnectRequestOptions) => Promise<ConnectOutcome<boolean>>) | undefined, paths: readonly string[], options: ReadManyOptions, defaultTimeoutMs: number | null): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
6
7
|
export {};
|
package/dist/read-many.js
CHANGED
|
@@ -1,70 +1,175 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { MAX_READ_MANY_PATHS } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { MdbaseConnectError, connectError, connectProblem } from "./errors.js";
|
|
3
|
+
import { COLLECTION_QUERY_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
|
|
4
|
+
import { requestOptionsWithinBudget, withRequestBudget } from "./request-budget.js";
|
|
5
|
+
export async function readMany(queryAll, readDocuments, supports, paths, options, defaultTimeoutMs) {
|
|
6
|
+
const requestedBatchSize = boundedInteger(options.batchSize ?? 100, 1_000, "batchSize");
|
|
6
7
|
const concurrency = boundedInteger(options.concurrency ?? 4, 4, "concurrency");
|
|
7
|
-
if (options.coordination?.latestWins)
|
|
8
|
+
if (options.coordination?.latestWins)
|
|
8
9
|
throw new TypeError("readMany batches cannot use latestWins coordination.");
|
|
9
|
-
|
|
10
|
+
if (options.includeBody !== undefined && typeof options.includeBody !== "boolean")
|
|
11
|
+
throw new TypeError("includeBody must be boolean.");
|
|
12
|
+
if (options.frontmatterMode !== undefined && !["effective", "persisted", "both"].includes(options.frontmatterMode))
|
|
13
|
+
throw new TypeError("Invalid frontmatterMode.");
|
|
10
14
|
const inputs = [...paths];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
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 });
|
|
15
|
+
try {
|
|
16
|
+
return await withRequestBudget(options, defaultTimeoutMs, async (budget) => {
|
|
17
|
+
if (!inputs.length)
|
|
18
|
+
return connectSuccess({ results: [], errors: [] });
|
|
19
|
+
const requestOptions = requestOptionsWithinBudget({ ...options, timeoutMs: null, coordination: { ...options.coordination, coalesce: false } }, budget);
|
|
20
|
+
const unique = [...new Set(inputs)];
|
|
21
|
+
const entries = new Map();
|
|
22
|
+
const errors = [];
|
|
23
|
+
const batchDiagnostics = [];
|
|
24
|
+
let nextBatch = 0;
|
|
25
|
+
let nextPath = 0;
|
|
26
|
+
const failBatch = (batch, selected, failure) => {
|
|
27
|
+
errors.push({ batch, paths: selected, failure });
|
|
35
28
|
for (const path of selected)
|
|
36
29
|
entries.set(path, { status: "error", path, batch });
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
30
|
+
};
|
|
31
|
+
const worker = async () => {
|
|
32
|
+
while (!budget.signal.aborted && nextPath < unique.length) {
|
|
33
|
+
// Recheck cached lifetime evidence at each admission: queued work must
|
|
34
|
+
// not borrow a retired route's support after reconnect/reauthorization.
|
|
35
|
+
const support = await supports?.("read-many-documents-v1", requestOptions);
|
|
36
|
+
if (support && !support.ok) {
|
|
37
|
+
nextPath = unique.length;
|
|
38
|
+
throw new MdbaseConnectError(support.problem);
|
|
39
|
+
}
|
|
40
|
+
if (budget.signal.aborted || nextPath >= unique.length)
|
|
41
|
+
return;
|
|
42
|
+
let documents = support?.value === true;
|
|
43
|
+
// Writer, Reader, TaskNotes, editor and standalone legacy providers
|
|
44
|
+
// retain typed path queries until B1's minimum-authority, consumer-pin
|
|
45
|
+
// adoption, N-1/rollback and connection-cache windows close. No probing.
|
|
46
|
+
const batchSize = documents ? Math.min(requestedBatchSize, MAX_READ_MANY_PATHS) : requestedBatchSize;
|
|
47
|
+
const batch = nextBatch++;
|
|
48
|
+
const selected = unique.slice(nextPath, nextPath + batchSize);
|
|
49
|
+
nextPath += selected.length;
|
|
50
|
+
const criteria = { where: `file.path in ${JSON.stringify(selected)}`, ...(options.types ? { types: options.types } : {}) };
|
|
51
|
+
try {
|
|
52
|
+
let targets = selected;
|
|
53
|
+
let diagnostics = [];
|
|
54
|
+
if (documents && options.types !== undefined) {
|
|
55
|
+
// Only the authority evaluates type membership. This selection
|
|
56
|
+
// is discovery, not a snapshot pinned across the subsequent read.
|
|
57
|
+
const selection = await queryAll({ ...criteria, select: ["file.path"], includeBody: false }, { ...requestOptions, pageSize: selected.length });
|
|
58
|
+
if (!selection.ok) {
|
|
59
|
+
failBatch(batch, selected, selection);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const matches = new Set(selection.value.results.map(record => record.path));
|
|
63
|
+
targets = selected.filter(path => matches.has(path));
|
|
64
|
+
diagnostics = selection.diagnostics;
|
|
65
|
+
// Selection can change the route. Reuse the real legacy query
|
|
66
|
+
// only on fresh unsupported evidence, never on selection errors.
|
|
67
|
+
const current = await supports("read-many-documents-v1", requestOptions);
|
|
68
|
+
if (!current.ok)
|
|
69
|
+
throw new MdbaseConnectError(current.problem);
|
|
70
|
+
documents = current.value;
|
|
71
|
+
}
|
|
72
|
+
if (!documents) {
|
|
73
|
+
const outcome = await queryAll({
|
|
74
|
+
...criteria,
|
|
75
|
+
...(options.includeBody === undefined ? {} : { includeBody: options.includeBody }),
|
|
76
|
+
...(options.frontmatterMode ? { frontmatterMode: options.frontmatterMode } : {})
|
|
77
|
+
}, { ...requestOptions, pageSize: selected.length });
|
|
78
|
+
if (!outcome.ok) {
|
|
79
|
+
failBatch(batch, selected, outcome);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
batchDiagnostics[batch] = outcome.diagnostics;
|
|
83
|
+
const records = new Map(outcome.value.results.map(record => [record.path, record]));
|
|
84
|
+
for (const path of selected) {
|
|
85
|
+
const record = records.get(path);
|
|
86
|
+
entries.set(path, record ? { status: "found", path, record } : { status: "missing", path });
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (budget.signal.aborted)
|
|
91
|
+
return;
|
|
92
|
+
const outcome = targets.length
|
|
93
|
+
? await readDocuments(targets, options.includeBody ?? false, requestOptions)
|
|
94
|
+
: connectSuccess({ items: [] });
|
|
95
|
+
if (!outcome.ok) {
|
|
96
|
+
failBatch(batch, selected, outcome);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Parse the entire batch before installing any result: malformed or
|
|
100
|
+
// incoherently bound data never becomes successful partial/missing data.
|
|
101
|
+
const parsed = documentEntries(outcome.value, targets, batch, options);
|
|
102
|
+
for (const path of selected)
|
|
103
|
+
entries.set(path, parsed.entries.get(path) ?? { status: "missing", path });
|
|
104
|
+
batchDiagnostics[batch] = [...diagnostics, ...outcome.diagnostics];
|
|
105
|
+
if (parsed.errors.length)
|
|
106
|
+
errors.push({
|
|
107
|
+
batch, paths: parsed.errors.map(error => error.path),
|
|
108
|
+
failure: connectFailure(connectProblem("operation_invalid", "Some batch records could not be read.", { details: { diagnostics: parsed.errors } }))
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof MdbaseConnectError))
|
|
113
|
+
throw error;
|
|
114
|
+
const failure = await captureConnectOutcome(async () => { throw error; }, COLLECTION_QUERY_PROBLEM_CODES);
|
|
115
|
+
failBatch(batch, selected, failure);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, unique.length) }, worker));
|
|
120
|
+
return connectSuccess({ results: inputs.map(path => entries.get(path)), errors: errors.sort((a, b) => a.batch - b.batch) }, batchDiagnostics.flat());
|
|
121
|
+
});
|
|
55
122
|
}
|
|
56
123
|
catch (error) {
|
|
57
124
|
if (error instanceof MdbaseConnectError)
|
|
58
|
-
return
|
|
125
|
+
return captureConnectOutcome(async () => { throw error; }, COLLECTION_QUERY_PROBLEM_CODES);
|
|
59
126
|
throw error;
|
|
60
127
|
}
|
|
61
|
-
finally {
|
|
62
|
-
budget.dispose();
|
|
63
|
-
}
|
|
64
128
|
}
|
|
129
|
+
function documentEntries(value, paths, batch, options) {
|
|
130
|
+
if (!value || !Array.isArray(value.items) || value.items.length !== paths.length)
|
|
131
|
+
invalidResponse();
|
|
132
|
+
const entries = new Map();
|
|
133
|
+
const errors = [];
|
|
134
|
+
value.items.forEach((item, index) => {
|
|
135
|
+
if (!item || item.path !== paths[index])
|
|
136
|
+
invalidResponse();
|
|
137
|
+
switch (item.status) {
|
|
138
|
+
case "found":
|
|
139
|
+
entries.set(item.path, { status: "found", path: item.path, record: documentRecord(item.record, item.path, options) });
|
|
140
|
+
break;
|
|
141
|
+
case "missing":
|
|
142
|
+
entries.set(item.path, { status: "missing", path: item.path });
|
|
143
|
+
break;
|
|
144
|
+
case "error":
|
|
145
|
+
if (!item.error || typeof item.error.code !== "string" || typeof item.error.message !== "string")
|
|
146
|
+
invalidResponse();
|
|
147
|
+
entries.set(item.path, { status: "error", path: item.path, batch });
|
|
148
|
+
errors.push({ severity: "error", code: item.error.code, message: item.error.message, path: item.path });
|
|
149
|
+
break;
|
|
150
|
+
default: invalidResponse();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return { entries, errors };
|
|
154
|
+
}
|
|
155
|
+
function documentRecord(record, path, options) {
|
|
156
|
+
if (!record || record.path !== path || typeof record.revision !== "string" || !record.revision
|
|
157
|
+
|| !Array.isArray(record.types) || record.types.some(type => typeof type !== "string")
|
|
158
|
+
|| !object(record.frontmatter) || !object(record.effective_frontmatter) || !object(record.file)
|
|
159
|
+
|| ((options.includeBody ?? false) && typeof record.body !== "string") || record.contract !== undefined)
|
|
160
|
+
invalidResponse();
|
|
161
|
+
const mode = options.frontmatterMode ?? "effective";
|
|
162
|
+
return {
|
|
163
|
+
path: record.path, revision: record.revision, types: record.types, file: record.file,
|
|
164
|
+
...(mode !== "effective" ? { frontmatter: record.frontmatter } : {}),
|
|
165
|
+
...(mode !== "persisted" ? { effectiveFrontmatter: record.effective_frontmatter } : {}),
|
|
166
|
+
...(options.includeBody ? { body: record.body } : {})
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function object(value) { return !!value && typeof value === "object" && !Array.isArray(value); }
|
|
170
|
+
function invalidResponse() { throw connectError("invalid_operation_response", "The authority returned an invalid or incoherently bound document batch."); }
|
|
65
171
|
function boundedInteger(value, max, name) {
|
|
66
|
-
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
172
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
67
173
|
throw new TypeError(`${name} must be an integer between 1 and ${max}.`);
|
|
68
|
-
}
|
|
69
174
|
return value;
|
|
70
175
|
}
|
package/dist/watch-policy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { WatchOptions } from "./operation-types.js";
|
|
1
|
+
import type { CollectionChange, MdbaseWatchSubscription, WatchInput, WatchOptions, WatchStatus } from "./operation-types.js";
|
|
2
|
+
import type { ConnectOutcome } from "./outcomes.js";
|
|
2
3
|
export interface ResolvedWatchRetryOptions {
|
|
3
4
|
initialDelayMs: number;
|
|
4
5
|
maxDelayMs: number;
|
|
@@ -6,3 +7,23 @@ export interface ResolvedWatchRetryOptions {
|
|
|
6
7
|
maxAttempts?: number;
|
|
7
8
|
}
|
|
8
9
|
export declare function watchRetryPolicy(options: WatchOptions["retry"]): ResolvedWatchRetryOptions | undefined;
|
|
10
|
+
export declare class CollectionWatchSubscription implements MdbaseWatchSubscription {
|
|
11
|
+
private readonly watch;
|
|
12
|
+
private readonly input;
|
|
13
|
+
private readonly watchStartTimeoutMs;
|
|
14
|
+
private readonly changes;
|
|
15
|
+
private readonly statuses;
|
|
16
|
+
private readonly problems;
|
|
17
|
+
private readonly controller;
|
|
18
|
+
private removeLifetimeAbort?;
|
|
19
|
+
private currentStatus;
|
|
20
|
+
private currentProblem;
|
|
21
|
+
private pendingChanges;
|
|
22
|
+
constructor(watch: (options: WatchOptions) => AsyncIterable<ConnectOutcome<CollectionChange>>, cursor: number, input: WatchInput, pendingChanges: CollectionChange[], watchStartTimeoutMs: number | null);
|
|
23
|
+
get status(): WatchStatus;
|
|
24
|
+
get problem(): import("@mdbase-dev/connect-protocol").ConnectProblem | null;
|
|
25
|
+
subscribe(listener: (change: CollectionChange) => void, onStatus?: (status: WatchStatus) => void, onProblem?: (problem: import("@mdbase-dev/connect-protocol").ConnectProblem) => void): () => void;
|
|
26
|
+
close(): void;
|
|
27
|
+
private run;
|
|
28
|
+
private publishStatus;
|
|
29
|
+
}
|
package/dist/watch-policy.js
CHANGED
|
@@ -10,3 +10,107 @@ export function watchRetryPolicy(options) {
|
|
|
10
10
|
: { maxAttempts: Math.max(0, options.maxAttempts) })
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
export class CollectionWatchSubscription {
|
|
14
|
+
watch;
|
|
15
|
+
input;
|
|
16
|
+
watchStartTimeoutMs;
|
|
17
|
+
changes = new Set();
|
|
18
|
+
statuses = new Set();
|
|
19
|
+
problems = new Set();
|
|
20
|
+
controller = new AbortController();
|
|
21
|
+
removeLifetimeAbort;
|
|
22
|
+
currentStatus;
|
|
23
|
+
currentProblem = null;
|
|
24
|
+
pendingChanges;
|
|
25
|
+
constructor(watch, cursor, input, pendingChanges, watchStartTimeoutMs) {
|
|
26
|
+
this.watch = watch;
|
|
27
|
+
this.input = input;
|
|
28
|
+
this.watchStartTimeoutMs = watchStartTimeoutMs;
|
|
29
|
+
this.pendingChanges = [...pendingChanges];
|
|
30
|
+
this.currentStatus = { state: "connected", cursor, recovered: false };
|
|
31
|
+
const lifetimeSignal = input.lifetimeSignal;
|
|
32
|
+
if (lifetimeSignal?.aborted)
|
|
33
|
+
this.close();
|
|
34
|
+
else if (lifetimeSignal) {
|
|
35
|
+
const close = () => this.close();
|
|
36
|
+
lifetimeSignal.addEventListener("abort", close, { once: true });
|
|
37
|
+
this.removeLifetimeAbort = () => lifetimeSignal.removeEventListener("abort", close);
|
|
38
|
+
}
|
|
39
|
+
if (!this.controller.signal.aborted)
|
|
40
|
+
void this.run(cursor);
|
|
41
|
+
}
|
|
42
|
+
get status() { return this.currentStatus; }
|
|
43
|
+
get problem() { return this.currentProblem; }
|
|
44
|
+
subscribe(listener, onStatus, onProblem) {
|
|
45
|
+
this.changes.add(listener);
|
|
46
|
+
if (onStatus) {
|
|
47
|
+
this.statuses.add(onStatus);
|
|
48
|
+
onStatus(this.currentStatus);
|
|
49
|
+
}
|
|
50
|
+
if (onProblem) {
|
|
51
|
+
this.problems.add(onProblem);
|
|
52
|
+
if (this.currentProblem)
|
|
53
|
+
onProblem(this.currentProblem);
|
|
54
|
+
}
|
|
55
|
+
for (const change of this.pendingChanges)
|
|
56
|
+
listener(change);
|
|
57
|
+
this.pendingChanges = [];
|
|
58
|
+
return () => {
|
|
59
|
+
this.changes.delete(listener);
|
|
60
|
+
if (onStatus)
|
|
61
|
+
this.statuses.delete(onStatus);
|
|
62
|
+
if (onProblem)
|
|
63
|
+
this.problems.delete(onProblem);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
close() {
|
|
67
|
+
if (this.controller.signal.aborted)
|
|
68
|
+
return;
|
|
69
|
+
this.controller.abort();
|
|
70
|
+
this.removeLifetimeAbort?.();
|
|
71
|
+
this.removeLifetimeAbort = undefined;
|
|
72
|
+
const cursor = "cursor" in this.currentStatus ? this.currentStatus.cursor : undefined;
|
|
73
|
+
this.publishStatus({ state: "closed", ...(cursor === undefined ? {} : { cursor }) });
|
|
74
|
+
}
|
|
75
|
+
async run(cursor) {
|
|
76
|
+
let firstStatus = true;
|
|
77
|
+
const iterator = this.watch({
|
|
78
|
+
cursor,
|
|
79
|
+
pollIntervalMs: this.input.pollIntervalMs,
|
|
80
|
+
retry: this.input.retry,
|
|
81
|
+
signal: this.controller.signal,
|
|
82
|
+
timeoutMs: this.watchStartTimeoutMs,
|
|
83
|
+
onStatus: (status) => {
|
|
84
|
+
if (firstStatus && status.state === "connecting") {
|
|
85
|
+
firstStatus = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
firstStatus = false;
|
|
89
|
+
this.publishStatus(status);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
for await (const outcome of iterator) {
|
|
94
|
+
if (this.controller.signal.aborted)
|
|
95
|
+
return;
|
|
96
|
+
if (!outcome.ok) {
|
|
97
|
+
this.currentProblem = outcome.problem;
|
|
98
|
+
for (const listener of this.problems)
|
|
99
|
+
listener(outcome.problem);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const listener of this.changes)
|
|
103
|
+
listener(outcome.value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
if (!this.controller.signal.aborted)
|
|
108
|
+
this.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
publishStatus(status) {
|
|
112
|
+
this.currentStatus = status;
|
|
113
|
+
for (const listener of this.statuses)
|
|
114
|
+
listener(status);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mdbase-dev/connect",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.125",
|
|
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.
|
|
36
|
+
"@mdbase-dev/connect-protocol": "0.1.0-beta.125"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/build-browser.mjs",
|
|
45
|
-
"test": "vitest
|
|
45
|
+
"test": "node ../../scripts/ci/vitest.mjs",
|
|
46
46
|
"test:browser-storage": "pnpm build && node scripts/browser-storage.test.mjs",
|
|
47
47
|
"test:consumer-spikes": "tsc -p tsconfig.consumer-spikes.json",
|
|
48
48
|
"test:public-api": "pnpm build && pnpm --filter @mdbase-dev/connect-protocol build && pnpm --filter @mdbase-dev/connect-testing build && tsc -p tsconfig.public-api.json && node scripts/check-public-api.mjs && node scripts/test-packed-api.mjs",
|