@mdbase-dev/connect 0.1.0-beta.121 → 0.1.0-beta.124
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 +161 -6
- package/dist/application-session.js +51 -27
- 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.d.ts +6 -0
- package/dist/change-events.js +95 -0
- package/dist/collection-client.d.ts +13 -3
- package/dist/collection-client.js +98 -140
- package/dist/connection.d.ts +12 -2
- package/dist/connection.js +57 -117
- package/dist/description-cache.d.ts +14 -0
- package/dist/description-cache.js +46 -0
- package/dist/file-crypto.js +10 -6
- package/dist/file-descriptor.d.ts +14 -0
- package/dist/file-descriptor.js +29 -0
- package/dist/file-metadata.d.ts +21 -0
- package/dist/file-metadata.js +112 -0
- package/dist/file-sha256.js +3 -3
- package/dist/files.d.ts +9 -19
- package/dist/files.js +49 -75
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/local-file-transport.js +18 -16
- package/dist/operation-types.d.ts +155 -14
- package/dist/query-pagination-internals.d.ts +9 -0
- package/dist/query-pagination-internals.js +52 -0
- package/dist/query-pagination.d.ts +7 -3
- package/dist/query-pagination.js +31 -27
- package/dist/query-predicates.d.ts +11 -0
- package/dist/query-predicates.js +15 -0
- package/dist/query-wire.d.ts +61 -0
- package/dist/query-wire.js +110 -0
- package/dist/read-many.d.ts +7 -0
- package/dist/read-many.js +175 -0
- package/dist/record-session.d.ts +3 -2
- package/dist/record-session.js +11 -2
- 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/dist/watch-policy.d.ts +22 -1
- package/dist/watch-policy.js +104 -0
- package/package.json +2 -2
package/dist/records.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { connectProblem } from "./errors.js";
|
|
2
2
|
import { connectFailure, connectSuccess } from "./outcomes.js";
|
|
3
3
|
import { MdbaseRecordSession } from "./record-session.js";
|
|
4
|
+
// Match the coordinator's four foreground slots, never its 32-slot backlog.
|
|
5
|
+
const REFRESH_CONCURRENCY = 4;
|
|
6
|
+
const REFRESH_ATTEMPTS = 4;
|
|
4
7
|
/**
|
|
5
8
|
* Editable record sessions for one connection: `connection.records`. Every
|
|
6
9
|
* view that opens the same path shares one session, so a record never has
|
|
@@ -11,6 +14,10 @@ export class MdbaseRecords {
|
|
|
11
14
|
pendingUpdate;
|
|
12
15
|
entries = new Map();
|
|
13
16
|
opening = new Map();
|
|
17
|
+
refreshes = new Map();
|
|
18
|
+
refreshQueue = new Set();
|
|
19
|
+
activeRefreshes = 0;
|
|
20
|
+
followers = 0;
|
|
14
21
|
constructor(connection,
|
|
15
22
|
/** Finds this record's interrupted update, which survives reloads in durable storage. */
|
|
16
23
|
pendingUpdate = async () => null) {
|
|
@@ -45,16 +52,104 @@ export class MdbaseRecords {
|
|
|
45
52
|
/**
|
|
46
53
|
* Keep open sessions current from a collection watch: changed records are
|
|
47
54
|
* refreshed, renamed records are followed and deleted records are marked.
|
|
48
|
-
*
|
|
55
|
+
* Refreshes are bounded and coalesced per session, with three backed-off
|
|
56
|
+
* retries for transient/not-sent reads. Failures remain in the snapshot.
|
|
57
|
+
* Stopping the last follower drops queued work; admitted reads still settle.
|
|
49
58
|
*/
|
|
50
59
|
follow(watch) {
|
|
51
|
-
|
|
60
|
+
this.followers += 1;
|
|
61
|
+
const unsubscribe = watch.subscribe((change) => this.apply(change), (status) => {
|
|
52
62
|
// Changes were missed: every open record may be stale.
|
|
53
63
|
if (status.state === "reset_required") {
|
|
54
64
|
for (const entry of this.entries.values())
|
|
55
|
-
|
|
65
|
+
this.refresh(entry);
|
|
56
66
|
}
|
|
57
67
|
});
|
|
68
|
+
let stopped = false;
|
|
69
|
+
return () => {
|
|
70
|
+
if (stopped)
|
|
71
|
+
return;
|
|
72
|
+
stopped = true;
|
|
73
|
+
unsubscribe();
|
|
74
|
+
if (--this.followers === 0) {
|
|
75
|
+
for (const refresh of this.refreshes.values()) {
|
|
76
|
+
clearTimeout(refresh.timer);
|
|
77
|
+
refresh.stopWaiting?.();
|
|
78
|
+
}
|
|
79
|
+
this.refreshes.clear();
|
|
80
|
+
this.refreshQueue.clear();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** One invalidation owner per entry, including while it waits behind a write. */
|
|
85
|
+
refresh(entry) {
|
|
86
|
+
const existing = this.refreshes.get(entry);
|
|
87
|
+
if (existing) {
|
|
88
|
+
existing.dirty = true;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const refresh = { entry, dirty: false, attempts: 0 };
|
|
92
|
+
this.refreshes.set(entry, refresh);
|
|
93
|
+
this.enqueueRefresh(refresh);
|
|
94
|
+
}
|
|
95
|
+
/** Exact recovery must settle before refresh() can read the authoritative record. */
|
|
96
|
+
enqueueRefresh(refresh) {
|
|
97
|
+
if (refresh.entry.session.snapshot.pendingRequestId) {
|
|
98
|
+
refresh.stopWaiting = refresh.entry.session.subscribe(() => {
|
|
99
|
+
if (refresh.entry.session.snapshot.pendingRequestId)
|
|
100
|
+
return;
|
|
101
|
+
refresh.stopWaiting?.();
|
|
102
|
+
refresh.stopWaiting = undefined;
|
|
103
|
+
this.enqueueRefresh(refresh);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.refreshQueue.add(refresh);
|
|
108
|
+
this.pumpRefreshes();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
pumpRefreshes() {
|
|
112
|
+
while (this.activeRefreshes < REFRESH_CONCURRENCY && this.refreshQueue.size > 0) {
|
|
113
|
+
const refresh = this.refreshQueue.values().next().value;
|
|
114
|
+
this.refreshQueue.delete(refresh);
|
|
115
|
+
refresh.dirty = false;
|
|
116
|
+
this.activeRefreshes += 1;
|
|
117
|
+
void this.runRefresh(refresh);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async runRefresh(refresh) {
|
|
121
|
+
try {
|
|
122
|
+
refresh.attempts += 1;
|
|
123
|
+
const outcome = await refresh.entry.session.refresh();
|
|
124
|
+
if (this.refreshes.get(refresh.entry) !== refresh)
|
|
125
|
+
return;
|
|
126
|
+
// A queued write may have entered recovery before refresh() could run.
|
|
127
|
+
if (refresh.entry.session.snapshot.pendingRequestId) {
|
|
128
|
+
refresh.attempts -= 1;
|
|
129
|
+
this.enqueueRefresh(refresh);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Cancellation is deliberate, not an availability failure. Never retry
|
|
133
|
+
// authorization/input failures merely because they were not sent.
|
|
134
|
+
const retry = !outcome.ok && outcome.problem.code !== "operation_cancelled"
|
|
135
|
+
&& (outcome.problem.recovery === "retry"
|
|
136
|
+
|| (outcome.problem.operation_outcome === "not_sent" && outcome.problem.category === "availability"));
|
|
137
|
+
if (retry && refresh.attempts < REFRESH_ATTEMPTS) {
|
|
138
|
+
refresh.timer = setTimeout(() => this.enqueueRefresh(refresh), 100 * 2 ** (refresh.attempts - 1));
|
|
139
|
+
}
|
|
140
|
+
else if (refresh.dirty) {
|
|
141
|
+
// Any number of invalidations during a read needs just one follow-up.
|
|
142
|
+
refresh.attempts = 0;
|
|
143
|
+
this.enqueueRefresh(refresh);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.refreshes.delete(refresh.entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
this.activeRefreshes -= 1;
|
|
151
|
+
this.pumpRefreshes();
|
|
152
|
+
}
|
|
58
153
|
}
|
|
59
154
|
async load(path, options) {
|
|
60
155
|
const { autosave, ...request } = options;
|
|
@@ -115,13 +210,20 @@ export class MdbaseRecords {
|
|
|
115
210
|
if (entry.leases > 0)
|
|
116
211
|
return;
|
|
117
212
|
const drop = () => {
|
|
118
|
-
const { state } = entry.session.snapshot;
|
|
119
|
-
if (state !== "saved" && state !== "deleted")
|
|
213
|
+
const { state, dirty } = entry.session.snapshot;
|
|
214
|
+
if (state !== "saved" && state !== "deleted" && !(state === "error" && !dirty))
|
|
120
215
|
return false;
|
|
121
216
|
entry.stopRetiring?.();
|
|
122
217
|
entry.stopRetiring = undefined;
|
|
123
218
|
if (this.entries.get(entry.path) === entry)
|
|
124
219
|
this.entries.delete(entry.path);
|
|
220
|
+
const refresh = this.refreshes.get(entry);
|
|
221
|
+
if (refresh) {
|
|
222
|
+
clearTimeout(refresh.timer);
|
|
223
|
+
refresh.stopWaiting?.();
|
|
224
|
+
this.refreshQueue.delete(refresh);
|
|
225
|
+
this.refreshes.delete(entry);
|
|
226
|
+
}
|
|
125
227
|
return true;
|
|
126
228
|
};
|
|
127
229
|
if (!drop())
|
|
@@ -136,7 +238,7 @@ export class MdbaseRecords {
|
|
|
136
238
|
this.entries.delete(entry.path);
|
|
137
239
|
entry.path = payload.to;
|
|
138
240
|
this.entries.set(entry.path, entry);
|
|
139
|
-
|
|
241
|
+
this.refresh(entry);
|
|
140
242
|
return;
|
|
141
243
|
}
|
|
142
244
|
if (change.type !== "mdbase.record.modified" && change.type !== "mdbase.record.deleted")
|
|
@@ -145,6 +247,6 @@ export class MdbaseRecords {
|
|
|
145
247
|
// The echo of this session's own acknowledged write needs no read.
|
|
146
248
|
if (!entry || payload.revision === entry.session.snapshot.record.revision)
|
|
147
249
|
return;
|
|
148
|
-
|
|
250
|
+
this.refresh(entry);
|
|
149
251
|
}
|
|
150
252
|
}
|
|
@@ -1,28 +1,35 @@
|
|
|
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* coordinatedSavedViewPages(executeView, releaseQueryCursor, input, options = {}) {
|
|
4
5
|
const { offset: requestedOffset, limit: requestedLimit, cursor: requestedCursor, ...criteria } = input;
|
|
5
6
|
let offset = nonNegativeInteger(requestedOffset, 0);
|
|
6
|
-
const firstPageSize = positiveInteger(options.firstPageSize ?? requestedLimit, 200);
|
|
7
|
+
const firstPageSize = positiveInteger(options.firstPageSize ?? options.pageSize ?? requestedLimit, 200);
|
|
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 loaded = 0;
|
|
12
14
|
let pageNumber = 0;
|
|
13
15
|
try {
|
|
14
|
-
while (!options.signal?.aborted) {
|
|
16
|
+
while (!options.signal?.aborted && loaded < maxResults) {
|
|
17
|
+
lease.startRequest();
|
|
15
18
|
const pageCursor = cursor;
|
|
16
19
|
const outcome = await executeView({
|
|
17
20
|
...criteria,
|
|
18
|
-
limit: pageNumber === 0 ? firstPageSize : pageSize,
|
|
21
|
+
...(!cursorMode ? { limit: Math.min(pageNumber === 0 ? firstPageSize : pageSize, maxResults - loaded) } : {}),
|
|
19
22
|
...(cursorMode && pageCursor ? { cursor: pageCursor } : { offset })
|
|
20
23
|
}, {
|
|
21
24
|
signal: options.signal,
|
|
22
25
|
timeoutMs: options.pageTimeoutMs,
|
|
23
26
|
coordination: { ...options.coordination, coalesce: false }
|
|
24
27
|
});
|
|
28
|
+
lease.finishRequest(outcome.ok ? outcome.value.meta.cursor : undefined);
|
|
29
|
+
if (options.signal?.aborted)
|
|
30
|
+
return;
|
|
25
31
|
if (!outcome.ok) {
|
|
32
|
+
lease.dispose();
|
|
26
33
|
yield outcome;
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
@@ -31,26 +38,30 @@ export async function* coordinatedSavedViewPages(executeView, releaseQueryCursor
|
|
|
31
38
|
if (returnedCursor) {
|
|
32
39
|
cursorMode = true;
|
|
33
40
|
cursor = returnedCursor;
|
|
34
|
-
cursorToRelease = returnedCursor;
|
|
35
41
|
}
|
|
36
42
|
else if (cursorMode) {
|
|
37
43
|
cursor = undefined;
|
|
38
|
-
cursorToRelease = pageCursor;
|
|
39
44
|
}
|
|
40
45
|
if (cursorMode && result.meta.hasMore && !returnedCursor) {
|
|
46
|
+
lease.dispose();
|
|
41
47
|
yield connectFailure(connectProblem("invalid_operation_response", "The collection authority omitted the cursor required for the next saved-view page."));
|
|
42
48
|
return;
|
|
43
49
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
50
|
+
const remaining = maxResults - loaded;
|
|
51
|
+
const results = result.results.length > remaining ? result.results.slice(0, remaining) : result.results;
|
|
52
|
+
loaded += results.length;
|
|
53
|
+
const complete = loaded >= maxResults || !result.meta.hasMore || result.results.length === 0;
|
|
46
54
|
const page = {
|
|
47
55
|
...result,
|
|
56
|
+
results,
|
|
48
57
|
page: pageNumber,
|
|
49
58
|
offset,
|
|
50
59
|
loaded,
|
|
51
60
|
complete,
|
|
52
61
|
...(returnedCursor ? { cursor: returnedCursor } : {})
|
|
53
62
|
};
|
|
63
|
+
if (complete)
|
|
64
|
+
lease.dispose();
|
|
54
65
|
options.onProgress?.(page);
|
|
55
66
|
yield connectSuccess(page, outcome.diagnostics);
|
|
56
67
|
if (complete)
|
|
@@ -60,17 +71,6 @@ export async function* coordinatedSavedViewPages(executeView, releaseQueryCursor
|
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
finally {
|
|
63
|
-
|
|
64
|
-
await releaseQueryCursor(cursorToRelease);
|
|
74
|
+
lease.dispose();
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
|
-
function nonNegativeInteger(value, fallback) {
|
|
68
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
69
|
-
? Math.max(0, Math.floor(value))
|
|
70
|
-
: fallback;
|
|
71
|
-
}
|
|
72
|
-
function positiveInteger(value, fallback) {
|
|
73
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
74
|
-
? Math.floor(value)
|
|
75
|
-
: fallback;
|
|
76
|
-
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Bounded, browser-local timings. Names contain no collection/grant identifiers. */
|
|
2
|
+
export async function timeStartupRead(stage, operation) {
|
|
3
|
+
const clock = typeof window === "undefined" ? undefined : globalThis.performance;
|
|
4
|
+
const start = clock?.now();
|
|
5
|
+
try {
|
|
6
|
+
return await operation();
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
if (clock && start !== undefined) {
|
|
10
|
+
try {
|
|
11
|
+
const name = `mdbase:startup:${stage}`;
|
|
12
|
+
clock.clearMeasures(name);
|
|
13
|
+
clock.measure(name, { start, end: clock.now() });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Optional diagnostics must never replace an authoritative read outcome.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { MdbaseApplicationManifest } from "./application-contract.js";
|
|
|
3
3
|
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
4
4
|
import { type MdbaseEffectiveCapabilities, type MdbaseCapabilityEvidence } from "./capabilities.js";
|
|
5
5
|
import type { MdbaseConnection } from "./connection.js";
|
|
6
|
-
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
6
|
+
import type { CollectionSetupAssessment, ConnectRequestOptions } from "./operation-types.js";
|
|
7
7
|
type ReadinessPublication = {
|
|
8
8
|
status: "authorization_required";
|
|
9
9
|
} | {
|
|
@@ -37,6 +37,8 @@ export interface ApplicationSessionContext {
|
|
|
37
37
|
export declare function applicationReadinessContext(manifest: MdbaseApplicationManifest, collectionId: string, info: MdbaseConnectionInfo, connections: MdbaseConnectionInfo[]): ApplicationSessionContext;
|
|
38
38
|
export declare function verificationKey(manifest: MdbaseApplicationManifest, collectionId: string): string;
|
|
39
39
|
export declare function verificationValue(manifest: MdbaseApplicationManifest): string;
|
|
40
|
+
export declare function accessRequirementSatisfied(manifest: MdbaseApplicationManifest, connection: MdbaseConnectionInfo): boolean;
|
|
41
|
+
export declare function reviewableTypePackAdoptions(assessment: CollectionSetupAssessment): Record<string, Record<string, string>>;
|
|
40
42
|
export declare function collectionSetupInput(manifest: MdbaseApplicationManifest, applicationId: string, declarationDigest: string, typePackAdoptions?: Record<string, Record<string, string>>): {
|
|
41
43
|
applicationId: string;
|
|
42
44
|
declarationDigest: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { effectiveCapabilities } from "./capabilities.js";
|
|
3
3
|
import { MdbaseConnectError } from "./errors.js";
|
|
4
4
|
import { withRequestBudget } from "./request-budget.js";
|
|
5
|
+
import { timeStartupRead } from "./startup-timing.js";
|
|
5
6
|
/** Live verification owns no session state; callers supply identity and cancellation guards. */
|
|
6
7
|
export async function verifyStructuredReadiness(input) {
|
|
7
8
|
const { collectionId, readiness, contracts, connection, controller, options, requestMs, isCurrent, publish } = input;
|
|
@@ -11,7 +12,7 @@ export async function verifyStructuredReadiness(input) {
|
|
|
11
12
|
else
|
|
12
13
|
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
13
14
|
try {
|
|
14
|
-
const outcome = await withRequestBudget({ ...options, signal: controller.signal }, requestMs, request => connection.describe(request));
|
|
15
|
+
const outcome = await timeStartupRead("contracts", () => withRequestBudget({ ...options, signal: controller.signal }, requestMs, request => connection.describe(request)));
|
|
15
16
|
if (!isCurrent())
|
|
16
17
|
return false;
|
|
17
18
|
if (!outcome.ok) {
|
|
@@ -80,6 +81,25 @@ export function verificationValue(manifest) {
|
|
|
80
81
|
provisions: manifest.provisions ?? {}
|
|
81
82
|
});
|
|
82
83
|
}
|
|
84
|
+
export function accessRequirementSatisfied(manifest, connection) {
|
|
85
|
+
return manifest.requirements?.access === "full_collection"
|
|
86
|
+
&& connection.scope.access === "full_collection"
|
|
87
|
+
&& connection.scope.contracts.length === 0;
|
|
88
|
+
}
|
|
89
|
+
export function reviewableTypePackAdoptions(assessment) {
|
|
90
|
+
const adoptions = {};
|
|
91
|
+
for (const pack of assessment.typePacks) {
|
|
92
|
+
const resources = Object.fromEntries(pack.resources
|
|
93
|
+
.filter((resource) => resource.action === "conflict"
|
|
94
|
+
&& resource.mode === "managed"
|
|
95
|
+
&& resource.currentDigest !== undefined
|
|
96
|
+
&& resource.installedDigest === undefined)
|
|
97
|
+
.map((resource) => [resource.target, resource.currentDigest]));
|
|
98
|
+
if (Object.keys(resources).length > 0)
|
|
99
|
+
adoptions[pack.desired.id] = resources;
|
|
100
|
+
}
|
|
101
|
+
return adoptions;
|
|
102
|
+
}
|
|
83
103
|
export function collectionSetupInput(manifest, applicationId, declarationDigest, typePackAdoptions) {
|
|
84
104
|
return {
|
|
85
105
|
applicationId, declarationDigest,
|
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.124",
|
|
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.124"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|