@mdbase-dev/connect 0.1.0-beta.126 → 0.1.0-beta.148
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/dist/application-session.d.ts +1 -1
- package/dist/application-session.js +1 -1
- package/dist/async.d.ts +6 -0
- package/dist/async.js +39 -0
- package/dist/authorization-refresh.d.ts +2 -0
- package/dist/authorization-refresh.js +13 -1
- 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/capabilities.d.ts +1 -1
- package/dist/collection-client.d.ts +2 -0
- package/dist/collection-client.js +8 -4
- package/dist/connect-options.d.ts +1 -1
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +1 -1
- package/dist/connection.d.ts +2 -1
- package/dist/connection.js +3 -1
- package/dist/control-timers.d.ts +43 -0
- package/dist/control-timers.js +290 -0
- package/dist/errors.d.ts +2 -28
- package/dist/errors.js +2 -52
- package/dist/file-transfer-internals.d.ts +2 -1
- package/dist/file-transfer-internals.js +10 -3
- package/dist/files.d.ts +4 -0
- package/dist/files.js +17 -18
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/internal-types.d.ts +2 -2
- package/dist/internal-types.js +1 -1
- package/dist/mdbase-connect.d.ts +1 -1
- package/dist/observe.d.ts +81 -0
- package/dist/observe.js +390 -0
- package/dist/operation-types.d.ts +3 -1
- package/dist/outcomes.d.ts +13 -13
- package/dist/outcomes.js +17 -0
- package/dist/query-pagination-internals.d.ts +1 -1
- package/dist/query-pagination-internals.js +4 -2
- package/dist/query-pagination.js +1 -1
- package/dist/query-wire.d.ts +16 -10
- package/dist/query-wire.js +44 -13
- package/dist/read-many.js +4 -2
- package/dist/runtime-utils.d.ts +1 -1
- package/dist/saved-view-pagination.js +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +1 -1
- package/dist/structured-readiness.d.ts +1 -1
- package/package.json +6 -2
- package/dist/application-contract.d.ts +0 -22
- package/dist/application-contract.js +0 -75
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384-
|
|
1
|
+
sha384-rwS4Cs8c/aA1ZdOgYHyRuQz90zVNe1GLeCYRKxXrHsuo777O2T/z+Bu0Mjpf0tBD
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ApplicationCapabilityRequirements, type LegacyApplicationCapabilityRequirements } from "@mdbase-dev/connect-protocol";
|
|
2
|
-
import type { MdbaseApplicationCapabilityId as ApplicationCapabilityId, MdbaseApplicationManifest as MdbaseAppManifest } from "
|
|
2
|
+
import type { MdbaseApplicationCapabilityId as ApplicationCapabilityId, MdbaseApplicationManifest as MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
3
3
|
import type { MdbaseConnectionInfo } from "./connection-types.js";
|
|
4
4
|
export type MdbaseCapabilityState = "available" | "requires_authorization" | "requires_setup" | "temporarily_unavailable" | "unsupported";
|
|
5
5
|
export interface MdbaseCapabilityEvidence {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionOperation, CollectionTypeDocument, JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { MdbaseQueryObserver, type ObserveOptions } from "./observe.js";
|
|
2
3
|
import { type CollectionChangesProblemCode, type CollectionDescriptionProblemCode, type CollectionMutationProblemCode, type CollectionQueryProblemCode, type CollectionReadProblemCode, type CollectionTypeProblemCode, type ConnectOutcome } from "./outcomes.js";
|
|
3
4
|
import type { ChangesInput, DescribeOptions, CollectionChange, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, AssessCollectionSetupInput, AssessTypePackInput, ApplyCollectionSetupInput, ApplyTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseCollectionTransport, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, QueryAllOptions, QueryInput, QueryMetadataInput, QueryMetadataRecord, QueryMetadataResult, QueryMetadataPage, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadManyOptions, ReadManyResult, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, WatchOptions } from "./operation-types.js";
|
|
4
5
|
/**
|
|
@@ -21,6 +22,7 @@ export declare class MdbaseCollectionClient<Frontmatter extends JsonObject = Jso
|
|
|
21
22
|
changes(input?: ChangesInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<CollectionChangesPage, CollectionChangesProblemCode>>;
|
|
22
23
|
read(input: ReadInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<RecordDocument<Frontmatter>, CollectionReadProblemCode>>;
|
|
23
24
|
readMany(paths: readonly string[], options?: ReadManyOptions): Promise<ConnectOutcome<ReadManyResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
25
|
+
observe(query?: QueryInput, options?: ObserveOptions): MdbaseQueryObserver<Frontmatter>;
|
|
24
26
|
query(input: QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryMetadataResult, CollectionQueryProblemCode>>;
|
|
25
27
|
query(input?: QueryInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter>, CollectionQueryProblemCode>>;
|
|
26
28
|
query(input: QueryInput | QueryMetadataInput, options?: ConnectRequestOptions): Promise<ConnectOutcome<QueryResult<Frontmatter> | QueryMetadataResult, CollectionQueryProblemCode>>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { abortableDelay } from "./async.js";
|
|
1
|
+
import { abortableDelay, pollDelay } from "./async.js";
|
|
2
2
|
import { invalidatesDescription, normalizeChangesPage } from "./change-events.js";
|
|
3
3
|
import { CollectionDescriptionCache } from "./description-cache.js";
|
|
4
4
|
import { coordinatedQueryPages } from "./query-pagination.js";
|
|
5
5
|
import { readMany as readManyRecords } from "./read-many.js";
|
|
6
|
+
import { MdbaseQueryObserver } from "./observe.js";
|
|
6
7
|
import { wireDataContractIdentity, wireQueryInput, wireQueryRecord, wireQueryResult } from "./query-wire.js";
|
|
7
8
|
import { coordinatedSavedViewPages } from "./saved-view-pagination.js";
|
|
8
9
|
import { MdbaseConnectError, connectError, connectProblem, operationProblem } from "./errors.js";
|
|
@@ -54,13 +55,16 @@ export class MdbaseCollectionClient {
|
|
|
54
55
|
readMany(paths, options = {}) {
|
|
55
56
|
return readManyRecords((input, requestOptions) => this.queryAll(input, requestOptions), (selected, includeBody, requestOptions) => this.envelopeOperation("read", { paths: selected, include_body: includeBody, include_document: false }, COLLECTION_READ_PROBLEM_CODES, requestOptions), this.supportsAuthorityFeature, paths, options, this.requestTimeoutMs);
|
|
56
57
|
}
|
|
58
|
+
observe(query = {}, options = {}) {
|
|
59
|
+
return new MdbaseQueryObserver(this, query, options, this.supportsAuthorityFeature);
|
|
60
|
+
}
|
|
57
61
|
async query(input = {}, options) {
|
|
58
62
|
if (input.output !== undefined && input.output !== "metadata") {
|
|
59
63
|
return connectFailure(connectProblem("invalid_request", "Unknown query output mode."));
|
|
60
64
|
}
|
|
61
65
|
if (input.output !== "metadata") {
|
|
62
66
|
const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, options);
|
|
63
|
-
return mapOutcome(outcome, value => wireQueryResult(value, undefined));
|
|
67
|
+
return mapOutcome(outcome, value => wireQueryResult(value, undefined, input));
|
|
64
68
|
}
|
|
65
69
|
const captured = await captureConnectOutcome(() => withRequestBudget(options, this.requestTimeoutMs, async (budget) => {
|
|
66
70
|
const requestOptions = requestOptionsWithinBudget({ ...options, timeoutMs: null }, budget);
|
|
@@ -72,7 +76,7 @@ export class MdbaseCollectionClient {
|
|
|
72
76
|
if (!support?.value)
|
|
73
77
|
throw connectError("unsupported_operation", "This authority does not support metadata queries. Use an ordinary query on legacy authorities.");
|
|
74
78
|
const outcome = await this.envelopeOperation("query", wireQueryInput(input), COLLECTION_QUERY_PROBLEM_CODES, requestOptions);
|
|
75
|
-
return mapOutcome(outcome, value => wireQueryResult(value, input.output));
|
|
79
|
+
return mapOutcome(outcome, value => wireQueryResult(value, input.output, input));
|
|
76
80
|
}), COLLECTION_QUERY_PROBLEM_CODES);
|
|
77
81
|
return captured.ok ? captured.value : captured;
|
|
78
82
|
}
|
|
@@ -309,7 +313,7 @@ export class MdbaseCollectionClient {
|
|
|
309
313
|
cursor = page.cursor;
|
|
310
314
|
if (!page.hasMore) {
|
|
311
315
|
try {
|
|
312
|
-
await
|
|
316
|
+
await pollDelay(pollInterval, options.signal);
|
|
313
317
|
}
|
|
314
318
|
catch {
|
|
315
319
|
if (!options.signal?.aborted)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MdbaseApplicationManifest as MdbaseAppManifest } from "
|
|
1
|
+
import type { MdbaseApplicationManifest as MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import type { JsonObject } from "@mdbase-dev/connect-protocol";
|
|
3
3
|
import type { GrantKeyStore } from "./crypto.js";
|
|
4
4
|
import type { ApplicationIdentityStore } from "./application-identity.js";
|
package/dist/connect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
-
import { type MdbaseApplicationManifest as MdbaseAppManifest } from "
|
|
2
|
+
import { type MdbaseApplicationManifest as MdbaseAppManifest } from "@mdbase-dev/connect-protocol";
|
|
3
3
|
import { type ApplicationIdentityStore } from "./application-identity.js";
|
|
4
4
|
import { MdbaseConnection, type MdbaseAuthorizationOutcome, type MdbaseAuthorizationResult, type MdbaseAuthorizeOptions, type MdbaseConnectEnvironment } from "./connection.js";
|
|
5
5
|
import type { MdbaseConnectOptions } from "./connect-options.js";
|
package/dist/connect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { APPLICATION_AUTHORIZATION_PROTOCOL_VERSION, authorizationContractRequirements, DEFAULT_LOOPBACK_PORT, GRANT_ENCRYPTION_PROTOCOL_VERSION, RELAY_ENCRYPTION_SUITE } from "@mdbase-dev/connect-protocol";
|
|
2
|
-
import { authorizationOperations, authorizationFiles, semanticContractVersion, validateAuthorizationSelection } from "
|
|
2
|
+
import { authorizationOperations, authorizationFiles, semanticContractVersion, validateAuthorizationSelection } from "@mdbase-dev/connect-protocol";
|
|
3
3
|
import { abortableDelay } from "./async.js";
|
|
4
4
|
import { authorizationCallbackState } from "./authorization-url.js";
|
|
5
5
|
import { randomBase64Url } from "./base64.js";
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MdbasePeopleClient } from "./people-client.js";
|
|
2
2
|
import type { CollectionOperation, CollectionTypeDocument, FileCapability, GrantScope, JsonObject } from "@mdbase-dev/connect-protocol";
|
|
3
|
-
import { type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "
|
|
3
|
+
import { type MdbaseApplicationCapabilityId as ApplicationCapabilityId } from "@mdbase-dev/connect-protocol";
|
|
4
4
|
import { MdbaseCollectionClient } from "./collection-client.js";
|
|
5
5
|
import { type ConnectionTransportInternals } from "./connection-transport.js";
|
|
6
6
|
import type { DirectAccessStatus, MdbaseAuthorizationCapabilities, MdbaseConnectionInfo, MdbaseConnectionRoute, MdbaseSyncConnection } from "./connection-types.js";
|
|
@@ -82,6 +82,7 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
|
|
|
82
82
|
readonly records: MdbaseRecords<Frontmatter>;
|
|
83
83
|
readonly people: MdbasePeopleClient;
|
|
84
84
|
readonly readMany: MdbaseCollectionClient<Frontmatter>["readMany"];
|
|
85
|
+
readonly observe: MdbaseCollectionClient<Frontmatter>["observe"];
|
|
85
86
|
private readonly connectionListeners;
|
|
86
87
|
constructor(internals: MdbaseConnectionInternals<Frontmatter>, collectionId: string);
|
|
87
88
|
get displayName(): string;
|
package/dist/connection.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MdbasePeopleClient, requestPeople } from "./people-client.js";
|
|
2
2
|
import { capabilityOperationsForContractVersion } from "@mdbase-dev/connect-protocol";
|
|
3
|
-
import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "
|
|
3
|
+
import { retainedCapabilities, semanticContractVersion, validateAuthorizationSelection } from "@mdbase-dev/connect-protocol";
|
|
4
4
|
import { MdbaseCollectionClient } from "./collection-client.js";
|
|
5
5
|
import { AuthorityFeatures, authorityCapabilities } from "./authority-features.js";
|
|
6
6
|
import { filePage } from "./file-metadata.js";
|
|
@@ -28,6 +28,7 @@ export class MdbaseConnection {
|
|
|
28
28
|
records;
|
|
29
29
|
people;
|
|
30
30
|
readMany;
|
|
31
|
+
observe;
|
|
31
32
|
connectionListeners = new Set();
|
|
32
33
|
constructor(internals, collectionId) {
|
|
33
34
|
this.internals = internals;
|
|
@@ -88,6 +89,7 @@ export class MdbaseConnection {
|
|
|
88
89
|
operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
|
|
89
90
|
}, internals.timeouts.requestMs), internals.timeouts.requestMs, (id, options) => this.supportsAuthorityFeature(id, options));
|
|
90
91
|
this.readMany = this.collectionClient.readMany.bind(this.collectionClient);
|
|
92
|
+
this.observe = this.collectionClient.observe.bind(this.collectionClient);
|
|
91
93
|
this.notifications = new ConnectionNotifications({
|
|
92
94
|
serverUrl: internals.serverUrl,
|
|
93
95
|
storage: internals.storage,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Fixed control-plane timer/channel requests, never data-replica operations. */
|
|
2
|
+
import type { MdbaseConnection } from "./connection.js";
|
|
3
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
4
|
+
export interface TimerChannelRegistration {
|
|
5
|
+
channelId: string;
|
|
6
|
+
installationId: string;
|
|
7
|
+
criteria: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface WebPushTimerOptions extends ConnectRequestOptions {
|
|
10
|
+
serviceWorker: ServiceWorkerRegistration;
|
|
11
|
+
criteria?: string[];
|
|
12
|
+
installationId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface FcmTimerOptions extends ConnectRequestOptions {
|
|
15
|
+
token: string;
|
|
16
|
+
criteria?: string[];
|
|
17
|
+
installationId?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Structurally implements the NEXT SDK's AppTimersPort without an SDK dependency. */
|
|
20
|
+
export interface ConnectAppTimersPort {
|
|
21
|
+
list(namespace: string, options: ConnectRequestOptions): Promise<unknown>;
|
|
22
|
+
put(namespace: string, id: string, body: {
|
|
23
|
+
criterion_id: string;
|
|
24
|
+
fire_at: string;
|
|
25
|
+
}, options: ConnectRequestOptions): Promise<unknown>;
|
|
26
|
+
cancel(namespace: string, id: string, generation: number | undefined, options: ConnectRequestOptions): Promise<unknown>;
|
|
27
|
+
reconcile(namespace: string, body: {
|
|
28
|
+
criterion_id: string;
|
|
29
|
+
timers: {
|
|
30
|
+
id: string;
|
|
31
|
+
fire_at: string;
|
|
32
|
+
}[];
|
|
33
|
+
}, options: ConnectRequestOptions): Promise<unknown>;
|
|
34
|
+
registerWebPush(options: WebPushTimerOptions): Promise<TimerChannelRegistration>;
|
|
35
|
+
unregisterWebPush(serviceWorker: ServiceWorkerRegistration | undefined, options: ConnectRequestOptions): Promise<void>;
|
|
36
|
+
registerFcm(options: FcmTimerOptions): Promise<TimerChannelRegistration & {
|
|
37
|
+
transport: "fcm";
|
|
38
|
+
}>;
|
|
39
|
+
unregisterFcm(options: ConnectRequestOptions): Promise<void>;
|
|
40
|
+
close(): void;
|
|
41
|
+
}
|
|
42
|
+
/** Bind one app consent; scope/mode decisions remain the control plane's authority. */
|
|
43
|
+
export declare function appTimers(connection: MdbaseConnection): ConnectAppTimersPort;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { connectError, MdbaseConnectError } from "./errors.js";
|
|
2
|
+
import { base64UrlBytes, bytesToBase64Url, randomBase64Url } from "./base64.js";
|
|
3
|
+
import { requestAbortReason, withCooperativeRequestBudget } from "./request-budget.js";
|
|
4
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
5
|
+
const NS = /^[A-Za-z0-9._-]{1,64}$/u;
|
|
6
|
+
const ID = /^[A-Za-z0-9._:-]{1,128}$/u;
|
|
7
|
+
const fail = () => connectError("invalid_request", "Invalid opaque timer or channel input.");
|
|
8
|
+
function segment(s, pattern) {
|
|
9
|
+
if (typeof s !== "string" || !pattern.test(s))
|
|
10
|
+
throw fail();
|
|
11
|
+
return encodeURIComponent(s);
|
|
12
|
+
}
|
|
13
|
+
function desired(body) {
|
|
14
|
+
if (!body || typeof body.criterion_id !== "string" || !body.criterion_id.length || body.criterion_id.length > 100
|
|
15
|
+
|| typeof body.fire_at !== "string" || !Number.isFinite(Date.parse(body.fire_at)))
|
|
16
|
+
throw fail();
|
|
17
|
+
return { criterion_id: body.criterion_id, fire_at: body.fire_at };
|
|
18
|
+
}
|
|
19
|
+
function origin(s) {
|
|
20
|
+
const u = new URL(s);
|
|
21
|
+
if (u.protocol !== "https:" || u.origin !== s.replace(/\/$/u, ""))
|
|
22
|
+
throw fail();
|
|
23
|
+
return u.origin;
|
|
24
|
+
}
|
|
25
|
+
async function boundedJSON(response, signal) {
|
|
26
|
+
if (response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json")
|
|
27
|
+
throw fail();
|
|
28
|
+
const reader = response.body?.getReader();
|
|
29
|
+
if (!reader)
|
|
30
|
+
throw fail();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
const abort = () => { void reader.cancel().catch(() => { }); };
|
|
34
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
35
|
+
try {
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
throw requestAbortReason(signal);
|
|
38
|
+
for (;;) {
|
|
39
|
+
const r = await reader.read();
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
throw requestAbortReason(signal);
|
|
42
|
+
if (r.done)
|
|
43
|
+
break;
|
|
44
|
+
size += r.value.length;
|
|
45
|
+
if (size > 1024 * 1024)
|
|
46
|
+
throw connectError("invalid_operation_response", "Control-plane metadata exceeds the bounded response budget.");
|
|
47
|
+
chunks.push(r.value);
|
|
48
|
+
}
|
|
49
|
+
const bytes = new Uint8Array(size);
|
|
50
|
+
let at = 0;
|
|
51
|
+
for (const chunk of chunks) {
|
|
52
|
+
bytes.set(chunk, at);
|
|
53
|
+
at += chunk.length;
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
chunks.length = 0;
|
|
59
|
+
signal.removeEventListener("abort", abort);
|
|
60
|
+
await reader.cancel().catch(() => { });
|
|
61
|
+
reader.releaseLock();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Bind one app consent; scope/mode decisions remain the control plane's authority. */
|
|
65
|
+
export function appTimers(connection) {
|
|
66
|
+
const transport = connection["transport"], internals = connection["internals"];
|
|
67
|
+
const server = origin(transport["serverUrl"]); // Before reading credentials.
|
|
68
|
+
const initial = transport.currentToken();
|
|
69
|
+
if (!initial?.grantId || !UUID.test(initial.grantId) || !UUID.test(initial.collectionId) || !initial.keyHandle) {
|
|
70
|
+
throw connectError("not_authorized", "A retained app consent is required for timers.");
|
|
71
|
+
}
|
|
72
|
+
const identity = (t) => JSON.stringify([
|
|
73
|
+
t.grantId, t.collectionId, t.clientId, t.keyHandle, t.applicationOrigin,
|
|
74
|
+
t.operations, t.scope, t.encryption, t.nextNoise,
|
|
75
|
+
t.authority && [t.authority.replicaId, t.authority.operationsUrl, t.authority.syncUrl, t.authority.filesUrl, t.authority.proofPublicKey]
|
|
76
|
+
]);
|
|
77
|
+
const pinned = identity(initial), lifetime = new AbortController();
|
|
78
|
+
const prefix = `/v1/next/collections/${initial.collectionId}/timers`;
|
|
79
|
+
const run = async (options, write, operation) => {
|
|
80
|
+
const signal = options.signal ? AbortSignal.any([options.signal, lifetime.signal]) : lifetime.signal;
|
|
81
|
+
return withCooperativeRequestBudget({
|
|
82
|
+
...options, signal,
|
|
83
|
+
timeoutMs: options.timeoutMs == null ? 10_000 : Math.min(options.timeoutMs, 10_000)
|
|
84
|
+
}, 10_000, async (budget) => {
|
|
85
|
+
const token = transport.currentToken();
|
|
86
|
+
if (!token || identity(token) !== pinned || !Number.isFinite(token.expiresAt) || token.expiresAt <= Date.now()) {
|
|
87
|
+
throw connectError("not_authorized", "The retained timer consent is unavailable.");
|
|
88
|
+
}
|
|
89
|
+
const snapshot = JSON.stringify(token), leases = transport["grantKeyLeases"]();
|
|
90
|
+
let dispatched = false;
|
|
91
|
+
const check = () => {
|
|
92
|
+
if (budget.signal.aborted)
|
|
93
|
+
throw requestAbortReason(budget.signal);
|
|
94
|
+
const current = transport.currentToken();
|
|
95
|
+
if (!current || !Number.isFinite(current.expiresAt) || current.expiresAt <= Date.now()
|
|
96
|
+
|| JSON.stringify(current) !== snapshot || origin(transport["serverUrl"]) !== server) {
|
|
97
|
+
throw connectError("authority_authorization_changed", "The retained timer consent changed.");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
await leases.retain(token, budget.signal);
|
|
102
|
+
check();
|
|
103
|
+
// Private fixed-route helper: never returned or accepted from the caller.
|
|
104
|
+
const request = async (path, method = "GET", data, noBody = false) => {
|
|
105
|
+
check();
|
|
106
|
+
const body = data === undefined ? undefined : JSON.stringify(data);
|
|
107
|
+
if (body && new TextEncoder().encode(body).length > 2 * 1024 * 1024)
|
|
108
|
+
throw fail();
|
|
109
|
+
dispatched ||= write && method !== "GET";
|
|
110
|
+
const response = await fetch(server + path, { method, signal: budget.signal, redirect: "error", credentials: "omit", cache: "no-store",
|
|
111
|
+
headers: { authorization: `Bearer ${token.accessToken}`, ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
|
112
|
+
...(body === undefined ? {} : { body }) });
|
|
113
|
+
try {
|
|
114
|
+
check();
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw connectError(response.status === 401 || response.status === 403 ? "not_authorized" : "operation_failed", "Control-plane timer or channel request failed.", { status: response.status,
|
|
117
|
+
operationOutcome: dispatched && response.status >= 500 ? "unknown" : "rejected" });
|
|
118
|
+
const result = noBody ? undefined : await boundedJSON(response, budget.signal);
|
|
119
|
+
check();
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (!response.body?.locked)
|
|
124
|
+
await response.body?.cancel().catch(() => { });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const result = await operation(request, check, budget.signal);
|
|
128
|
+
check();
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof MdbaseConnectError && error.problem.operation_outcome === "rejected")
|
|
133
|
+
throw error;
|
|
134
|
+
if (dispatched)
|
|
135
|
+
throw connectError("operation_failed", "Timer or channel write outcome is unknown.", { operationOutcome: "unknown" });
|
|
136
|
+
if (budget.signal.aborted)
|
|
137
|
+
throw requestAbortReason(budget.signal);
|
|
138
|
+
if (error instanceof MdbaseConnectError)
|
|
139
|
+
throw error;
|
|
140
|
+
throw connectError("operation_failed", "Control-plane timer or channel request failed.", { operationOutcome: "not_sent" });
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
leases.release();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
const registration = async (options, native) => run(options, true, async (request, check, signal) => {
|
|
148
|
+
const application = await internals.register({ signal, timeoutMs: null });
|
|
149
|
+
check();
|
|
150
|
+
const declared = application.notifications?.criteria.map(c => c.id) ?? [];
|
|
151
|
+
const criteria = [...new Set(options.criteria ?? declared)];
|
|
152
|
+
if (!criteria.length || criteria.length > 100 || criteria.some(c => !declared.includes(c)))
|
|
153
|
+
throw fail();
|
|
154
|
+
if (native && application.notifications?.native_delivery?.mode !== "managed_fcm")
|
|
155
|
+
throw fail();
|
|
156
|
+
const storageKey = `${internals.notificationKey(initial.collectionId, native ? "fcm" : "web_push")}:control-timers`;
|
|
157
|
+
const previous = internals.storage.getItem(storageKey);
|
|
158
|
+
const saved = previous ? JSON.parse(previous) : null;
|
|
159
|
+
const installationId = options.installationId ?? (saved && saved.grantId === initial.grantId ? saved.installationId : undefined) ?? randomBase64Url(24);
|
|
160
|
+
if (typeof installationId !== "string" || !/^[A-Za-z0-9._:-]{1,200}$/u.test(installationId))
|
|
161
|
+
throw fail();
|
|
162
|
+
if (saved && saved.grantId === initial.grantId) {
|
|
163
|
+
if (!saved.channelId || saved.previousTarget !== undefined || saved.pending || !Array.isArray(saved.criteria)) {
|
|
164
|
+
throw connectError("operation_failed", "Channel registration requires reconciliation before replacement.", { operationOutcome: "unknown" });
|
|
165
|
+
}
|
|
166
|
+
if (saved.installationId !== installationId) {
|
|
167
|
+
throw connectError("operation_failed", "Unregister the acknowledged installation before replacing it.", { operationOutcome: "not_sent" });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let data;
|
|
171
|
+
if (native) {
|
|
172
|
+
const token = options.token;
|
|
173
|
+
if (typeof token !== "string" || !token.length || token.length > 4096)
|
|
174
|
+
throw fail();
|
|
175
|
+
data = { installation_id: installationId, criteria, transport: "fcm", token };
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const worker = options.serviceWorker;
|
|
179
|
+
const keyBody = await request("/v1/notifications/vapid-public-key");
|
|
180
|
+
if (typeof keyBody.public_key !== "string" || !/^[A-Za-z0-9_-]{87}$/u.test(keyBody.public_key))
|
|
181
|
+
throw fail();
|
|
182
|
+
const vapid = base64UrlBytes(keyBody.public_key);
|
|
183
|
+
if (vapid.length !== 65 || vapid[0] !== 4 || bytesToBase64Url(vapid) !== keyBody.public_key)
|
|
184
|
+
throw fail();
|
|
185
|
+
let subscription = await worker.pushManager.getSubscription();
|
|
186
|
+
check();
|
|
187
|
+
if (!subscription) {
|
|
188
|
+
subscription = await worker.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapid });
|
|
189
|
+
try {
|
|
190
|
+
check();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
await subscription.unsubscribe().catch(() => false);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const sub = subscription.toJSON();
|
|
198
|
+
if (!sub.endpoint || !sub.keys?.p256dh || !sub.keys.auth)
|
|
199
|
+
throw fail();
|
|
200
|
+
data = { installation_id: installationId, criteria, subscription: { endpoint: sub.endpoint, expirationTime: sub.expirationTime ?? null, keys: sub.keys } };
|
|
201
|
+
}
|
|
202
|
+
// Persist only this target. A different acknowledged installation must
|
|
203
|
+
// first be explicitly unregistered; uncertain targets cannot be replaced.
|
|
204
|
+
check();
|
|
205
|
+
if (internals.storage.getItem(storageKey) !== previous) {
|
|
206
|
+
throw connectError("authority_authorization_changed", "Channel registration context changed.", { operationOutcome: "not_sent" });
|
|
207
|
+
}
|
|
208
|
+
const operationNonce = randomBase64Url(24);
|
|
209
|
+
const pending = JSON.stringify({
|
|
210
|
+
installationId, grantId: initial.grantId, pending: true, operationNonce,
|
|
211
|
+
...(saved && saved.grantId === initial.grantId && saved.installationId === installationId && saved.channelId ? { channelId: saved.channelId } : {})
|
|
212
|
+
});
|
|
213
|
+
internals.storage.setItem(storageKey, pending);
|
|
214
|
+
const result = await request("/v1/notifications/channels", "POST", data);
|
|
215
|
+
if (typeof result.channel_id !== "string" || !UUID.test(result.channel_id))
|
|
216
|
+
throw fail();
|
|
217
|
+
check();
|
|
218
|
+
if (internals.storage.getItem(storageKey) !== pending) {
|
|
219
|
+
throw connectError("authority_authorization_changed", "Channel registration context changed.");
|
|
220
|
+
}
|
|
221
|
+
const registration = { channelId: result.channel_id, installationId, criteria };
|
|
222
|
+
internals.storage.setItem(storageKey, JSON.stringify({ ...registration, grantId: initial.grantId, operationNonce }));
|
|
223
|
+
return registration;
|
|
224
|
+
});
|
|
225
|
+
const unregister = async (native, worker, options) => run(options, true, async (request, check) => {
|
|
226
|
+
const key = `${internals.notificationKey(initial.collectionId, native ? "fcm" : "web_push")}:control-timers`;
|
|
227
|
+
const saved = internals.storage.getItem(key);
|
|
228
|
+
if (saved) {
|
|
229
|
+
const r = JSON.parse(saved);
|
|
230
|
+
if (r.grantId !== initial.grantId)
|
|
231
|
+
throw fail();
|
|
232
|
+
if (!r.channelId || r.previousTarget !== undefined || r.pending || !Array.isArray(r.criteria)) {
|
|
233
|
+
// An uncertain registration is not proof that no remote channel exists.
|
|
234
|
+
throw connectError("operation_failed", "Channel registration requires reconciliation before removal.", { operationOutcome: "unknown" });
|
|
235
|
+
}
|
|
236
|
+
if (!UUID.test(r.channelId))
|
|
237
|
+
throw fail();
|
|
238
|
+
// Serialize mutations through a unique pending marker, including DELETE.
|
|
239
|
+
// An old channel ID is not a receipt for an uncertain same-target update.
|
|
240
|
+
const pending = JSON.stringify({ ...r, pending: true, operationNonce: randomBase64Url(24) });
|
|
241
|
+
internals.storage.setItem(key, pending);
|
|
242
|
+
await request(`/v1/notifications/channels/${r.channelId}`, "DELETE", undefined, true);
|
|
243
|
+
check();
|
|
244
|
+
if (internals.storage.getItem(key) !== pending) {
|
|
245
|
+
throw connectError("authority_authorization_changed", "Channel registration context changed.");
|
|
246
|
+
}
|
|
247
|
+
internals.storage.removeItem(key);
|
|
248
|
+
}
|
|
249
|
+
if (worker) {
|
|
250
|
+
const sub = await worker.pushManager.getSubscription();
|
|
251
|
+
check();
|
|
252
|
+
if (internals.storage.getItem(key) !== null) {
|
|
253
|
+
throw connectError("authority_authorization_changed", "Channel registration context changed.");
|
|
254
|
+
}
|
|
255
|
+
if (sub) {
|
|
256
|
+
await sub.unsubscribe();
|
|
257
|
+
check();
|
|
258
|
+
if (internals.storage.getItem(key) !== null) {
|
|
259
|
+
throw connectError("authority_authorization_changed", "Channel registration context changed.");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
list: (ns, o) => run(o, false, request => request(`${prefix}/${segment(ns, NS)}`)),
|
|
266
|
+
put: (ns, id, body, o) => {
|
|
267
|
+
const path = `${prefix}/${segment(ns, NS)}/${segment(id, ID)}`, input = desired(body);
|
|
268
|
+
return run(o, true, request => request(path, "PUT", input));
|
|
269
|
+
},
|
|
270
|
+
cancel: (ns, id, generation, o) => run(o, true, request => {
|
|
271
|
+
if (generation !== undefined && (!Number.isSafeInteger(generation) || generation < 1))
|
|
272
|
+
throw fail();
|
|
273
|
+
return request(`${prefix}/${segment(ns, NS)}/${segment(id, ID)}${generation === undefined ? "" : `?generation=${generation}`}`, "DELETE");
|
|
274
|
+
}),
|
|
275
|
+
reconcile: (ns, body, o) => {
|
|
276
|
+
const path = `${prefix}/${segment(ns, NS)}/reconcile`, c = body.criterion_id;
|
|
277
|
+
if (typeof c !== "string" || !c.length || c.length > 100 || !Array.isArray(body.timers) || body.timers.length > 10_000)
|
|
278
|
+
throw fail();
|
|
279
|
+
const timers = body.timers.map(t => { segment(t.id, ID); return { id: t.id, fire_at: desired({ criterion_id: c, fire_at: t.fire_at }).fire_at }; });
|
|
280
|
+
if (new Set(timers.map(t => t.id)).size !== timers.length)
|
|
281
|
+
throw fail();
|
|
282
|
+
return run(o, true, request => request(path, "POST", { criterion_id: c, timers }));
|
|
283
|
+
},
|
|
284
|
+
registerWebPush: o => registration({ ...o, criteria: o.criteria ? [...o.criteria] : undefined }, false),
|
|
285
|
+
unregisterWebPush: (worker, o) => unregister(false, worker, o),
|
|
286
|
+
registerFcm: async (o) => ({ ...await registration({ ...o, criteria: o.criteria ? [...o.criteria] : undefined }, true), transport: "fcm" }),
|
|
287
|
+
unregisterFcm: o => unregister(true, undefined, o),
|
|
288
|
+
close: () => lifetime.abort(),
|
|
289
|
+
});
|
|
290
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
|
-
import { type ConnectOperationOutcome, type ConnectProblem, type
|
|
1
|
+
import { MdbaseConnectError, type ConnectOperationOutcome, type ConnectProblem, type KnownConnectProblem, type MdbaseOperationEnvelope } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
interface ProblemOptions {
|
|
3
3
|
operationOutcome?: ConnectOperationOutcome;
|
|
4
4
|
traceId?: string;
|
|
5
5
|
status?: number;
|
|
6
6
|
cause?: unknown;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
details?: never;
|
|
10
|
-
} : {} extends ConnectProblemDetailsByCode[Code] ? ProblemOptions & {
|
|
11
|
-
details?: ConnectProblemDetailsByCode[Code];
|
|
12
|
-
} : ProblemOptions & {
|
|
13
|
-
details: ConnectProblemDetailsByCode[Code];
|
|
14
|
-
};
|
|
15
|
-
type ProblemArguments<Code extends ConnectProblemCode> = ConnectProblemDetailsByCode[Code] extends undefined ? [options?: ProblemOptionsFor<Code>] : {} extends ConnectProblemDetailsByCode[Code] ? [options?: ProblemOptionsFor<Code>] : [options: ProblemOptionsFor<Code>];
|
|
16
|
-
export interface ConnectErrorContext {
|
|
17
|
-
status?: number;
|
|
18
|
-
cause?: unknown;
|
|
19
|
-
}
|
|
20
|
-
/** Internal carrier used while adapting throwing transports into public outcomes. */
|
|
21
|
-
export declare class MdbaseConnectError extends Error {
|
|
22
|
-
readonly problem: ConnectProblem;
|
|
23
|
-
readonly code: ConnectProblem["code"];
|
|
24
|
-
readonly status?: number;
|
|
25
|
-
constructor(problem: ConnectProblem, context?: ConnectErrorContext);
|
|
26
|
-
get retryable(): boolean;
|
|
27
|
-
get requiresAuthorization(): boolean;
|
|
28
|
-
get outcomeUnknown(): boolean;
|
|
29
|
-
get recovery(): "choose_collection" | "contact_support" | "fix_request" | "none" | "reauthorize" | "refresh" | "repair_collection" | "resolve_conflict" | "resolve_outcome" | "resume_connector_access" | "retry" | "upgrade_application" | "upgrade_collection" | "upgrade_connector";
|
|
30
|
-
get details(): unknown;
|
|
31
|
-
}
|
|
32
|
-
export declare function connectProblem<Code extends ConnectProblemCode>(code: Code, message: string, ...[options]: ProblemArguments<Code>): KnownConnectProblem<Code>;
|
|
8
|
+
export { MdbaseConnectError, connectError, connectProblem, type ConnectErrorContext } from "@mdbase-dev/connect-protocol";
|
|
33
9
|
export declare function unknownConnectProblem(serverCode: string, message: string, options?: ProblemOptions & {
|
|
34
10
|
details?: unknown;
|
|
35
11
|
}): ConnectProblem;
|
|
36
|
-
export declare function connectError<Code extends ConnectProblemCode>(code: Code, message: string, ...args: ProblemArguments<Code>): MdbaseConnectError;
|
|
37
12
|
export declare function serverConnectError(serverCode: string, message: string, options?: ProblemOptions & {
|
|
38
13
|
details?: unknown;
|
|
39
14
|
status?: number;
|
|
@@ -42,4 +17,3 @@ export declare function serverConnectError(serverCode: string, message: string,
|
|
|
42
17
|
export declare function operationProblem<Result>(envelope: MdbaseOperationEnvelope<Result>): KnownConnectProblem<"collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported" | "concurrent_modification" | "file_not_found" | "invalid_path" | "operation_invalid">;
|
|
43
18
|
/** True only when repeating an operation is safe without asking the user. */
|
|
44
19
|
export declare function isRetryableConnectError(error: unknown): boolean;
|
|
45
|
-
export {};
|
package/dist/errors.js
CHANGED
|
@@ -1,48 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export class MdbaseConnectError extends Error {
|
|
4
|
-
problem;
|
|
5
|
-
code;
|
|
6
|
-
status;
|
|
7
|
-
constructor(problem, context = {}) {
|
|
8
|
-
super(problem.message, context.cause === undefined ? undefined : { cause: context.cause });
|
|
9
|
-
this.problem = problem;
|
|
10
|
-
this.name = "MdbaseConnectError";
|
|
11
|
-
this.code = problem.code;
|
|
12
|
-
this.status = context.status;
|
|
13
|
-
}
|
|
14
|
-
get retryable() {
|
|
15
|
-
return this.problem.recovery === "retry"
|
|
16
|
-
&& this.problem.operation_outcome !== "unknown";
|
|
17
|
-
}
|
|
18
|
-
get requiresAuthorization() {
|
|
19
|
-
return this.problem.category === "authorization";
|
|
20
|
-
}
|
|
21
|
-
get outcomeUnknown() {
|
|
22
|
-
return this.problem.operation_outcome === "unknown";
|
|
23
|
-
}
|
|
24
|
-
get recovery() {
|
|
25
|
-
return this.problem.recovery;
|
|
26
|
-
}
|
|
27
|
-
get details() {
|
|
28
|
-
return this.problem.details;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export function connectProblem(code, message, ...[options = {}]) {
|
|
32
|
-
const definition = CONNECT_PROBLEM_CATALOG[code];
|
|
33
|
-
return {
|
|
34
|
-
problem_version: CONNECT_PROBLEM_VERSION,
|
|
35
|
-
code,
|
|
36
|
-
category: definition.category,
|
|
37
|
-
recovery: definition.recovery,
|
|
38
|
-
message,
|
|
39
|
-
...(options.details === undefined ? {} : { details: options.details }),
|
|
40
|
-
...(options.operationOutcome === undefined
|
|
41
|
-
? {}
|
|
42
|
-
: { operation_outcome: options.operationOutcome }),
|
|
43
|
-
...(options.traceId === undefined ? {} : { trace_id: options.traceId })
|
|
44
|
-
};
|
|
45
|
-
}
|
|
1
|
+
import { MdbaseConnectError, connectProblem, normalizeConnectProblem } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
export { MdbaseConnectError, connectError, connectProblem } from "@mdbase-dev/connect-protocol";
|
|
46
3
|
export function unknownConnectProblem(serverCode, message, options = {}) {
|
|
47
4
|
return normalizeConnectProblem(serverCode, message, {
|
|
48
5
|
details: options.details,
|
|
@@ -50,13 +7,6 @@ export function unknownConnectProblem(serverCode, message, options = {}) {
|
|
|
50
7
|
trace_id: options.traceId
|
|
51
8
|
});
|
|
52
9
|
}
|
|
53
|
-
export function connectError(code, message, ...args) {
|
|
54
|
-
const options = args[0];
|
|
55
|
-
return new MdbaseConnectError(connectProblem(code, message, ...args), {
|
|
56
|
-
status: options?.status,
|
|
57
|
-
cause: options?.cause
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
10
|
export function serverConnectError(serverCode, message, options = {}) {
|
|
61
11
|
const { status, cause, ...problemOptions } = options;
|
|
62
12
|
return new MdbaseConnectError(normalizeConnectProblem(serverCode, message, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileTransferSession, FileTransferStatus, PreparedFilePart } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import { MdbaseConnectError } from "./errors.js";
|
|
3
3
|
export declare const MAX_OBJECT_ATTEMPTS = 3;
|
|
4
|
+
export declare function validUploadAttempts(value: number | undefined, noAutoAbort?: boolean): number | undefined;
|
|
4
5
|
export declare const MAX_BUFFERED_DOWNLOAD_BYTES: number;
|
|
5
6
|
export declare const UUID: RegExp;
|
|
6
7
|
export declare const SHA256_DIGEST: RegExp;
|
|
@@ -15,7 +16,7 @@ export declare function validPageSize(value: number): number;
|
|
|
15
16
|
export declare function requireTransferSession(session: FileTransferSession, transferId: string, direction: "upload" | "download"): void;
|
|
16
17
|
export declare function requireTransferStatus(status: FileTransferStatus, session: FileTransferSession): void;
|
|
17
18
|
export declare function chunkLength(totalSize: number, partSize: number, index: number): number;
|
|
18
|
-
export declare function retryChunk<Result>(work: () => Promise<Result>, signal?: AbortSignal): Promise<Result>;
|
|
19
|
+
export declare function retryChunk<Result>(work: () => Promise<Result>, signal?: AbortSignal, maxAttempts?: number): Promise<Result>;
|
|
19
20
|
export declare function requirePreparedPart(part: PreparedFilePart, transferId: string, partIndex: number, offset: number, contentLength: number, method: "PUT"): void;
|
|
20
21
|
export declare function browserObjectHeaders(headers: Record<string, string>): Headers;
|
|
21
22
|
export declare function mapConcurrent<Result>(count: number, concurrency: number, work: (index: number) => Promise<Result>): Promise<Result[]>;
|
|
@@ -5,6 +5,13 @@ const HASH_CHUNK_BYTES = 1024 * 1024;
|
|
|
5
5
|
const DEFAULT_CONCURRENCY = 4;
|
|
6
6
|
const MAX_CONCURRENCY = 8;
|
|
7
7
|
export const MAX_OBJECT_ATTEMPTS = 3;
|
|
8
|
+
export function validUploadAttempts(value, noAutoAbort) {
|
|
9
|
+
if ((noAutoAbort !== undefined && typeof noAutoAbort !== "boolean")
|
|
10
|
+
|| (value !== undefined && (!Number.isInteger(value) || value < 1 || value > MAX_TRANSIENT_CHUNK_ATTEMPTS))) {
|
|
11
|
+
throw connectError("invalid_request", "Invalid upload options.");
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
8
15
|
export const MAX_BUFFERED_DOWNLOAD_BYTES = 64 * 1024 * 1024;
|
|
9
16
|
export const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
10
17
|
export const SHA256_DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
@@ -144,15 +151,15 @@ function validTransferStrategy(session) {
|
|
|
144
151
|
export function chunkLength(totalSize, partSize, index) {
|
|
145
152
|
return Math.min(partSize, Math.max(0, totalSize - index * partSize));
|
|
146
153
|
}
|
|
147
|
-
export async function retryChunk(work, signal) {
|
|
148
|
-
for (let attempt = 1; attempt <=
|
|
154
|
+
export async function retryChunk(work, signal, maxAttempts = MAX_TRANSIENT_CHUNK_ATTEMPTS) {
|
|
155
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
149
156
|
throwIfAborted(signal);
|
|
150
157
|
try {
|
|
151
158
|
return await work();
|
|
152
159
|
}
|
|
153
160
|
catch (error) {
|
|
154
161
|
if (signal?.aborted
|
|
155
|
-
|| attempt ===
|
|
162
|
+
|| attempt === maxAttempts
|
|
156
163
|
|| (error instanceof MdbaseConnectError && !error.retryable))
|
|
157
164
|
throw error;
|
|
158
165
|
await waitForTransientChunkRetry(attempt, signal);
|