@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.
Files changed (39) hide show
  1. package/README.md +86 -6
  2. package/dist/application-session.js +45 -27
  3. package/dist/browser/integrity.json +1 -1
  4. package/dist/browser/mdbase-connect.min.js +4 -4
  5. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  6. package/dist/change-events.d.ts +6 -0
  7. package/dist/change-events.js +102 -0
  8. package/dist/collection-client.d.ts +7 -2
  9. package/dist/collection-client.js +35 -54
  10. package/dist/connection.d.ts +5 -2
  11. package/dist/connection.js +4 -3
  12. package/dist/description-cache.d.ts +14 -0
  13. package/dist/description-cache.js +73 -0
  14. package/dist/file-crypto.js +10 -6
  15. package/dist/file-descriptor.d.ts +12 -0
  16. package/dist/file-descriptor.js +12 -0
  17. package/dist/file-sha256.js +3 -3
  18. package/dist/files.d.ts +2 -11
  19. package/dist/files.js +36 -30
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.js +2 -0
  22. package/dist/operation-types.d.ts +127 -6
  23. package/dist/query-pagination-internals.d.ts +9 -0
  24. package/dist/query-pagination-internals.js +52 -0
  25. package/dist/query-pagination.js +28 -26
  26. package/dist/query-predicates.d.ts +11 -0
  27. package/dist/query-predicates.js +15 -0
  28. package/dist/read-many.d.ts +6 -0
  29. package/dist/read-many.js +70 -0
  30. package/dist/record-session.d.ts +8 -2
  31. package/dist/record-session.js +27 -5
  32. package/dist/records.d.ts +15 -2
  33. package/dist/records.js +109 -7
  34. package/dist/saved-view-pagination.js +20 -20
  35. package/dist/startup-timing.d.ts +2 -0
  36. package/dist/startup-timing.js +20 -0
  37. package/dist/structured-readiness.d.ts +3 -1
  38. package/dist/structured-readiness.js +21 -1
  39. package/package.json +2 -2
@@ -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
- if (draft.body === current)
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
- if (draft.baseBody !== current)
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 is saved.
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
- * Returns a function that stops following.
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
- * Returns a function that stops following.
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
- return watch.subscribe((change) => this.apply(change), (status) => {
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
- void entry.session.refresh();
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
- void entry.session.refresh();
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
- void entry.session.refresh();
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
- let cursorToRelease = requestedCursor;
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
- loaded += result.results.length;
45
- const complete = !result.meta.hasMore || result.results.length === 0;
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
- if (cursorMode && cursorToRelease)
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,2 @@
1
+ /** Bounded, browser-local timings. Names contain no collection/grant identifiers. */
2
+ export declare function timeStartupRead<T>(stage: "registration" | "setup-assessment" | "contracts", operation: () => Promise<T>): Promise<T>;
@@ -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.120",
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.120"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.123"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",