@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/record-session.js
CHANGED
|
@@ -15,6 +15,8 @@ export class MdbaseRecordSession {
|
|
|
15
15
|
patch = {};
|
|
16
16
|
remote = null;
|
|
17
17
|
problem = null;
|
|
18
|
+
// Editing/discarding a draft cannot establish freshness after a failed read.
|
|
19
|
+
refreshProblem = null;
|
|
18
20
|
deleted = false;
|
|
19
21
|
busy = false;
|
|
20
22
|
pending;
|
|
@@ -26,6 +28,7 @@ export class MdbaseRecordSession {
|
|
|
26
28
|
inFlight;
|
|
27
29
|
timer;
|
|
28
30
|
lastEdit = 0;
|
|
31
|
+
disposed = false;
|
|
29
32
|
idleMs;
|
|
30
33
|
current;
|
|
31
34
|
constructor(record, adapter, options = {}) {
|
|
@@ -62,11 +65,17 @@ export class MdbaseRecordSession {
|
|
|
62
65
|
*/
|
|
63
66
|
restore(draft) {
|
|
64
67
|
const current = this.adapter.body(this.record);
|
|
65
|
-
|
|
68
|
+
const frontmatter = this.adapter.frontmatter?.(this.record) ?? {};
|
|
69
|
+
const patch = Object.fromEntries(Object.entries(draft.patch ?? {}).filter(([key, value]) => JSON.stringify(frontmatter[key] ?? null) !== JSON.stringify(value)));
|
|
70
|
+
if (draft.body === current && Object.keys(patch).length === 0)
|
|
66
71
|
return;
|
|
67
72
|
this.body = draft.body;
|
|
68
|
-
|
|
73
|
+
this.patch = patch;
|
|
74
|
+
if ((draft.body !== current && draft.baseBody !== current)
|
|
75
|
+
|| Object.keys(patch).some((key) => !draft.baseFrontmatter
|
|
76
|
+
|| JSON.stringify(draft.baseFrontmatter[key] ?? null) !== JSON.stringify(frontmatter[key] ?? null))) {
|
|
69
77
|
this.remote = this.record;
|
|
78
|
+
}
|
|
70
79
|
this.emit();
|
|
71
80
|
}
|
|
72
81
|
/**
|
|
@@ -81,6 +90,12 @@ export class MdbaseRecordSession {
|
|
|
81
90
|
this.emit();
|
|
82
91
|
this.autosave();
|
|
83
92
|
}
|
|
93
|
+
/** Stop automatic writes and subscriptions when the owning view closes. In-flight writes still settle. */
|
|
94
|
+
dispose() {
|
|
95
|
+
this.disposed = true;
|
|
96
|
+
clearTimeout(this.timer);
|
|
97
|
+
this.listeners.clear();
|
|
98
|
+
}
|
|
84
99
|
/** Schedule an autosave one idle interval from now. */
|
|
85
100
|
autosave() {
|
|
86
101
|
this.lastEdit = Date.now();
|
|
@@ -116,8 +131,13 @@ export class MdbaseRecordSession {
|
|
|
116
131
|
if (!outcome.ok) {
|
|
117
132
|
if (outcome.problem.code === "file_not_found")
|
|
118
133
|
this.markDeleted();
|
|
134
|
+
else {
|
|
135
|
+
this.refreshProblem = outcome.problem;
|
|
136
|
+
this.emit();
|
|
137
|
+
}
|
|
119
138
|
return outcome;
|
|
120
139
|
}
|
|
140
|
+
this.refreshProblem = null;
|
|
121
141
|
this.seen.add(this.adapter.revision(outcome.value));
|
|
122
142
|
this.classify(outcome.value);
|
|
123
143
|
this.emit();
|
|
@@ -143,6 +163,7 @@ export class MdbaseRecordSession {
|
|
|
143
163
|
this.record = record;
|
|
144
164
|
this.remote = null;
|
|
145
165
|
this.problem = null;
|
|
166
|
+
this.refreshProblem = null;
|
|
146
167
|
this.emit();
|
|
147
168
|
}
|
|
148
169
|
markDeleted() {
|
|
@@ -227,7 +248,7 @@ export class MdbaseRecordSession {
|
|
|
227
248
|
}
|
|
228
249
|
schedule() {
|
|
229
250
|
clearTimeout(this.timer);
|
|
230
|
-
if (this.idleMs === undefined || this.remote || this.deleted)
|
|
251
|
+
if (this.disposed || this.idleMs === undefined || this.remote || this.deleted)
|
|
231
252
|
return;
|
|
232
253
|
const delay = Math.max(0, this.lastEdit + this.idleMs - Date.now());
|
|
233
254
|
this.timer = setTimeout(() => void this.save(), delay);
|
|
@@ -334,6 +355,7 @@ export class MdbaseRecordSession {
|
|
|
334
355
|
delete this.patch[key];
|
|
335
356
|
}
|
|
336
357
|
this.problem = null;
|
|
358
|
+
this.refreshProblem = null;
|
|
337
359
|
this.pending = undefined;
|
|
338
360
|
this.settle();
|
|
339
361
|
if (this.dirtyLocally())
|
|
@@ -406,7 +428,7 @@ export class MdbaseRecordSession {
|
|
|
406
428
|
: this.remote ? "conflict"
|
|
407
429
|
: this.busy ? "saving"
|
|
408
430
|
: this.pending ? "recovery"
|
|
409
|
-
: this.problem ? "error"
|
|
431
|
+
: this.problem || this.refreshProblem ? "error"
|
|
410
432
|
: dirty ? "unsaved" : "saved";
|
|
411
433
|
return {
|
|
412
434
|
state,
|
|
@@ -415,7 +437,7 @@ export class MdbaseRecordSession {
|
|
|
415
437
|
record: this.record,
|
|
416
438
|
remote: this.remote,
|
|
417
439
|
dirty,
|
|
418
|
-
problem: this.problem,
|
|
440
|
+
problem: this.problem ?? this.refreshProblem,
|
|
419
441
|
...(this.pending ? { pendingRequestId: this.pending.requestId } : {})
|
|
420
442
|
};
|
|
421
443
|
}
|
package/dist/records.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ export interface MdbaseRecordLease<Frontmatter extends JsonObject = JsonObject>
|
|
|
9
9
|
readonly session: MdbaseRecordSession<RecordDocument<Frontmatter>>;
|
|
10
10
|
/**
|
|
11
11
|
* Stop using the session from this view. Idempotent. Local changes are
|
|
12
|
-
* still saved; the session is dropped once no view holds it and it
|
|
12
|
+
* still saved; the session is dropped once no view holds it and it has
|
|
13
|
+
* nothing left to save.
|
|
13
14
|
*/
|
|
14
15
|
release(): void;
|
|
15
16
|
}
|
|
@@ -30,6 +31,10 @@ export declare class MdbaseRecords<Frontmatter extends JsonObject = JsonObject>
|
|
|
30
31
|
private readonly pendingUpdate;
|
|
31
32
|
private readonly entries;
|
|
32
33
|
private readonly opening;
|
|
34
|
+
private readonly refreshes;
|
|
35
|
+
private readonly refreshQueue;
|
|
36
|
+
private activeRefreshes;
|
|
37
|
+
private followers;
|
|
33
38
|
constructor(connection: RecordConnection<Frontmatter>,
|
|
34
39
|
/** Finds this record's interrupted update, which survives reloads in durable storage. */
|
|
35
40
|
pendingUpdate?: (path: string) => Promise<string | null>);
|
|
@@ -42,9 +47,17 @@ export declare class MdbaseRecords<Frontmatter extends JsonObject = JsonObject>
|
|
|
42
47
|
/**
|
|
43
48
|
* Keep open sessions current from a collection watch: changed records are
|
|
44
49
|
* refreshed, renamed records are followed and deleted records are marked.
|
|
45
|
-
*
|
|
50
|
+
* Refreshes are bounded and coalesced per session, with three backed-off
|
|
51
|
+
* retries for transient/not-sent reads. Failures remain in the snapshot.
|
|
52
|
+
* Stopping the last follower drops queued work; admitted reads still settle.
|
|
46
53
|
*/
|
|
47
54
|
follow(watch: Pick<MdbaseWatchSubscription, "subscribe">): () => void;
|
|
55
|
+
/** One invalidation owner per entry, including while it waits behind a write. */
|
|
56
|
+
private refresh;
|
|
57
|
+
/** Exact recovery must settle before refresh() can read the authoritative record. */
|
|
58
|
+
private enqueueRefresh;
|
|
59
|
+
private pumpRefreshes;
|
|
60
|
+
private runRefresh;
|
|
48
61
|
private load;
|
|
49
62
|
/** Writes and reads follow the entry's current path, so a followed rename needs no new session. */
|
|
50
63
|
private adapter;
|
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/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.123",
|
|
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.123"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"esbuild": "^0.28.1",
|