@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.
Files changed (49) hide show
  1. package/README.md +161 -6
  2. package/dist/application-session.js +51 -27
  3. package/dist/authority-features.d.ts +29 -0
  4. package/dist/authority-features.js +84 -0
  5. package/dist/browser/integrity.json +1 -1
  6. package/dist/browser/mdbase-connect.min.js +4 -4
  7. package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
  8. package/dist/change-events.d.ts +6 -0
  9. package/dist/change-events.js +95 -0
  10. package/dist/collection-client.d.ts +13 -3
  11. package/dist/collection-client.js +98 -140
  12. package/dist/connection.d.ts +12 -2
  13. package/dist/connection.js +57 -117
  14. package/dist/description-cache.d.ts +14 -0
  15. package/dist/description-cache.js +46 -0
  16. package/dist/file-crypto.js +10 -6
  17. package/dist/file-descriptor.d.ts +14 -0
  18. package/dist/file-descriptor.js +29 -0
  19. package/dist/file-metadata.d.ts +21 -0
  20. package/dist/file-metadata.js +112 -0
  21. package/dist/file-sha256.js +3 -3
  22. package/dist/files.d.ts +9 -19
  23. package/dist/files.js +49 -75
  24. package/dist/index.d.ts +5 -2
  25. package/dist/index.js +2 -0
  26. package/dist/local-file-transport.js +18 -16
  27. package/dist/operation-types.d.ts +155 -14
  28. package/dist/query-pagination-internals.d.ts +9 -0
  29. package/dist/query-pagination-internals.js +52 -0
  30. package/dist/query-pagination.d.ts +7 -3
  31. package/dist/query-pagination.js +31 -27
  32. package/dist/query-predicates.d.ts +11 -0
  33. package/dist/query-predicates.js +15 -0
  34. package/dist/query-wire.d.ts +61 -0
  35. package/dist/query-wire.js +110 -0
  36. package/dist/read-many.d.ts +7 -0
  37. package/dist/read-many.js +175 -0
  38. package/dist/record-session.d.ts +3 -2
  39. package/dist/record-session.js +11 -2
  40. package/dist/records.d.ts +15 -2
  41. package/dist/records.js +109 -7
  42. package/dist/saved-view-pagination.js +20 -20
  43. package/dist/startup-timing.d.ts +2 -0
  44. package/dist/startup-timing.js +20 -0
  45. package/dist/structured-readiness.d.ts +3 -1
  46. package/dist/structured-readiness.js +21 -1
  47. package/dist/watch-policy.d.ts +22 -1
  48. package/dist/watch-policy.js +104 -0
  49. 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
- * 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,
@@ -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
+ }
@@ -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.121",
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.121"
36
+ "@mdbase-dev/connect-protocol": "0.1.0-beta.124"
37
37
  },
38
38
  "devDependencies": {
39
39
  "esbuild": "^0.28.1",