@mdbase-dev/connect 0.1.0-beta.111 → 0.1.0-beta.113
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 +47 -0
- package/dist/advanced.d.ts +2 -0
- package/dist/base64.js +11 -3
- package/dist/browser/integrity.json +1 -1
- package/dist/browser/mdbase-connect.min.js +4 -3
- package/dist/browser/mdbase-connect.min.js.sha384 +1 -1
- package/dist/connection-transport.d.ts +2 -0
- package/dist/connection-transport.js +13 -14
- package/dist/connection.d.ts +3 -0
- package/dist/connection.js +4 -0
- package/dist/crypto.js +3 -10
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/internal-types.d.ts +2 -0
- package/dist/outcomes.d.ts +5 -5
- package/dist/outcomes.js +1 -0
- package/dist/pending-mutation-store.d.ts +3 -0
- package/dist/pending-mutation-store.js +10 -0
- package/dist/record-digest.d.ts +14 -0
- package/dist/record-digest.js +20 -0
- package/dist/record-session.d.ts +164 -0
- package/dist/record-session.js +449 -0
- package/dist/records.d.ts +56 -0
- package/dist/records.js +150 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
sha384
|
|
1
|
+
sha384-uKL9FxAY8Ji2mQSA44JTIIarA2CnPGN3EFC4jxk1Kx+OfDsPbFr2J+iwlH/p77Qz
|
|
@@ -34,6 +34,8 @@ export declare class ConnectionTransport {
|
|
|
34
34
|
disableDirectAccess(): void;
|
|
35
35
|
/** Return pending writes without exposing authority transport credentials. */
|
|
36
36
|
pendingMutations(): readonly PendingMutationSummary[];
|
|
37
|
+
/** The request ID of an interrupted update to one record, if any. */
|
|
38
|
+
pendingUpdate(path: string): Promise<string | null>;
|
|
37
39
|
pendingMutation(requestId: string): PendingMutationSummary | null;
|
|
38
40
|
recoverPendingMutation<Result>(requestId: string, options?: ConnectRequestOptions): Promise<Result>;
|
|
39
41
|
performOperation<Result>(operation: CollectionOperation, input: unknown, options?: ConnectRequestOptions): Promise<Result>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { interruptedUpdate, updatedRecordDigest } from "./record-digest.js";
|
|
1
2
|
import { OPERATION_TRANSPORT_PROTOCOL_VERSION, isConnectProblem, mutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
3
|
import { decryptRelayResponse, encryptRelayRequest, RelayCryptoError } from "./crypto.js";
|
|
3
4
|
import { MdbaseConnectError, connectError, serverConnectError } from "./errors.js";
|
|
@@ -108,23 +109,16 @@ export class ConnectionTransport {
|
|
|
108
109
|
}
|
|
109
110
|
/** Return pending writes without exposing authority transport credentials. */
|
|
110
111
|
pendingMutations() {
|
|
111
|
-
return this.storedPendingMutations().map((pending) => (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}));
|
|
112
|
+
return this.storedPendingMutations().map((pending) => this.pendingMutationStore.summary(pending));
|
|
113
|
+
}
|
|
114
|
+
/** The request ID of an interrupted update to one record, if any. */
|
|
115
|
+
async pendingUpdate(path) {
|
|
116
|
+
const collectionId = this.currentToken()?.collectionId;
|
|
117
|
+
return collectionId ? interruptedUpdate(this.storedPendingMutations(), collectionId, path) : null;
|
|
118
118
|
}
|
|
119
119
|
pendingMutation(requestId) {
|
|
120
120
|
const pending = this.storedPendingMutation(requestId);
|
|
121
|
-
return pending ?
|
|
122
|
-
requestId: pending.requestId,
|
|
123
|
-
operation: this.pendingMutationStore.identifier(pending),
|
|
124
|
-
fingerprint: pending.inputFingerprint,
|
|
125
|
-
status: "outcome_unknown",
|
|
126
|
-
createdAt: new Date(pending.createdAt).toISOString()
|
|
127
|
-
} : null;
|
|
121
|
+
return pending ? this.pendingMutationStore.summary(pending) : null;
|
|
128
122
|
}
|
|
129
123
|
async recoverPendingMutation(requestId, options) {
|
|
130
124
|
const pending = this.storedPendingMutation(requestId);
|
|
@@ -372,10 +366,13 @@ export class ConnectionTransport {
|
|
|
372
366
|
let requestId = pendingRequestId ?? crypto.randomUUID();
|
|
373
367
|
let pending = null;
|
|
374
368
|
let inputFingerprint;
|
|
369
|
+
let updatedRecord;
|
|
375
370
|
if (pendingRequestId !== undefined) {
|
|
376
371
|
pending = this.storedPendingMutation(pendingRequestId);
|
|
377
372
|
inputFingerprint = pending?.inputFingerprint
|
|
378
373
|
?? await operationFingerprint(operation, input);
|
|
374
|
+
if (!pending)
|
|
375
|
+
updatedRecord = await updatedRecordDigest(token.collectionId, operation, input);
|
|
379
376
|
if (pending) {
|
|
380
377
|
if (pending.collectionId !== token.collectionId
|
|
381
378
|
|| pending.operation !== operation
|
|
@@ -420,6 +417,7 @@ export class ConnectionTransport {
|
|
|
420
417
|
operation,
|
|
421
418
|
mutation: mutation,
|
|
422
419
|
inputFingerprint: inputFingerprint,
|
|
420
|
+
...(updatedRecord ? { recordDigest: updatedRecord } : {}),
|
|
423
421
|
requestId,
|
|
424
422
|
envelope: encryptedRequest,
|
|
425
423
|
createdAt: Date.now()
|
|
@@ -452,6 +450,7 @@ export class ConnectionTransport {
|
|
|
452
450
|
operation,
|
|
453
451
|
mutation: mutation,
|
|
454
452
|
inputFingerprint: inputFingerprint,
|
|
453
|
+
...(updatedRecord ? { recordDigest: updatedRecord } : {}),
|
|
455
454
|
requestId,
|
|
456
455
|
request,
|
|
457
456
|
createdAt: Date.now()
|
package/dist/connection.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type ConnectionTransportInternals } from "./connection-transport.js";
|
|
|
4
4
|
import type { DirectAccessStatus, MdbaseAuthorizationCapabilities, MdbaseConnectionInfo, MdbaseConnectionRoute, MdbaseSyncConnection } from "./connection-types.js";
|
|
5
5
|
import type { GrantKeyStore } from "./crypto.js";
|
|
6
6
|
import { MdbaseFileClient } from "./files.js";
|
|
7
|
+
import { MdbaseRecords } from "./records.js";
|
|
7
8
|
import { type Application } from "./internal-types.js";
|
|
8
9
|
import type { MdbaseNativeNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNotificationRegistrationOptions } from "./notifications.js";
|
|
9
10
|
import type { ChangesInput, CollectionChangesPage, CollectionDescription, CollectionSetupApplyResult, CollectionSetupAssessment, ApplyCollectionSetupInput, ApplyTypePackInput, AssessCollectionSetupInput, AssessTypePackInput, CreateInput, CreateTypeInput, CreateViewSourceInput, DeleteInput, DeletePreflightResult, DeleteProgressOptions, DeleteResult, DeleteViewSourceInput, DeleteViewSourceResult, ExecuteViewInput, MdbaseDesiredTimer, MdbaseTimer, MdbaseTimerList, MdbaseTimerReconciliation, ConnectRequestOptions, PendingMutation, QueryAllOptions, QueryInput, QueryPage, QueryPagesOptions, QueryResult, ReadInput, ReadTypeInput, ReadViewSourceInput, RenameInput, RenamePreflightResult, RenameProgressOptions, RenameResult, RecordDocument, SavedViewExecution, SavedViewPage, SavedViewPagesOptions, SavedViewList, SavedViewSourceDocument, TypePackApplyResult, TypePackAssessment, UpdateInput, UpdateTypeInput, UpdateViewSourceInput, MdbaseWatchSubscription, WatchInput } from "./operation-types.js";
|
|
@@ -74,6 +75,8 @@ export declare class MdbaseConnection<Frontmatter extends JsonObject = JsonObjec
|
|
|
74
75
|
private readonly notifications;
|
|
75
76
|
private readonly transport;
|
|
76
77
|
readonly files: MdbaseFileClient;
|
|
78
|
+
/** Editable record sessions shared by every view of this connection. */
|
|
79
|
+
readonly records: MdbaseRecords<Frontmatter>;
|
|
77
80
|
private readonly connectionListeners;
|
|
78
81
|
constructor(internals: MdbaseConnectionInternals<Frontmatter>, collectionId: string);
|
|
79
82
|
get displayName(): string;
|
package/dist/connection.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ConnectionNotifications } from "./connection-notifications.js";
|
|
|
5
5
|
import { ConnectionTransport } from "./connection-transport.js";
|
|
6
6
|
import { MdbaseConnectError, connectProblem, operationProblem } from "./errors.js";
|
|
7
7
|
import { MdbaseFileClient } from "./files.js";
|
|
8
|
+
import { MdbaseRecords } from "./records.js";
|
|
8
9
|
import { DEFAULT_OPERATIONS } from "./internal-types.js";
|
|
9
10
|
import { assertDeletePreview, assertRenamePreview, deleteEstimate, isCancellation, renameEstimate, throwIfCancelled, uniqueOperations } from "./operation-helpers.js";
|
|
10
11
|
import { AUTHORIZATION_PROBLEM_CODES, COLLECTION_CHANGES_PROBLEM_CODES, COLLECTION_MUTATION_PROBLEM_CODES, DIRECT_ACCESS_PROBLEM_CODES, NOTIFICATION_PROBLEM_CODES, REGISTRATION_PROBLEM_CODES, captureConnectOutcome, connectFailure, connectSuccess } from "./outcomes.js";
|
|
@@ -17,6 +18,8 @@ export class MdbaseConnection {
|
|
|
17
18
|
notifications;
|
|
18
19
|
transport;
|
|
19
20
|
files;
|
|
21
|
+
/** Editable record sessions shared by every view of this connection. */
|
|
22
|
+
records;
|
|
20
23
|
connectionListeners = new Set();
|
|
21
24
|
constructor(internals, collectionId) {
|
|
22
25
|
this.internals = internals;
|
|
@@ -38,6 +41,7 @@ export class MdbaseConnection {
|
|
|
38
41
|
}, {
|
|
39
42
|
downloadPart: (session, partIndex, expectedLength, signal) => this.transport.files.downloadHostedPart(session, partIndex, expectedLength, signal)
|
|
40
43
|
}, internals.timeouts);
|
|
44
|
+
this.records = new MdbaseRecords(this, (path) => this.transport.pendingUpdate(path));
|
|
41
45
|
this.collectionClient = new MdbaseCollectionClient(new CollectionRequestCoordinator({
|
|
42
46
|
operation: (operation, input, requestOptions) => this.transport.performOperation(operation, input, requestOptions)
|
|
43
47
|
}, internals.timeouts.requestMs), internals.timeouts.requestMs);
|
package/dist/crypto.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OPERATION_TRANSPORT_PROTOCOL_VERSION, GRANT_ENCRYPTION_PROTOCOL_VERSION, AUTHORITY_PROOF_DOMAIN, AUTHORITY_PROOF_HEADERS, AUTHORITY_PROOF_VERSION, RELAY_ENCRYPTION_SUITE, isConnectProblem } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import { base64UrlBytes, bytesToBase64Url } from "./base64.js";
|
|
2
3
|
const DATABASE_NAME = "mdbase-connect-keys";
|
|
3
4
|
const STORE_NAME = "grant-keys";
|
|
4
5
|
const MAX_U64 = 18446744073709551615n;
|
|
@@ -206,7 +207,7 @@ export async function decryptRelayResponse(store, handle, binding, request, resp
|
|
|
206
207
|
iv: nonce(request.counter),
|
|
207
208
|
additionalData: new TextEncoder().encode(aad(metadata, "response")),
|
|
208
209
|
tagLength: 128
|
|
209
|
-
}, key,
|
|
210
|
+
}, key, base64UrlToBytes(response.ciphertext));
|
|
210
211
|
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
|
|
211
212
|
if (value?.ok === true && "result" in value)
|
|
212
213
|
return value;
|
|
@@ -364,12 +365,6 @@ function nonce(counter) {
|
|
|
364
365
|
view.setBigUint64(4, value, false);
|
|
365
366
|
return result;
|
|
366
367
|
}
|
|
367
|
-
function bytesToBase64Url(bytes) {
|
|
368
|
-
let binary = "";
|
|
369
|
-
for (const byte of bytes)
|
|
370
|
-
binary += String.fromCharCode(byte);
|
|
371
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
372
|
-
}
|
|
373
368
|
async function sha256Base64Url(value) {
|
|
374
369
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
375
370
|
return bytesToBase64Url(new Uint8Array(digest));
|
|
@@ -377,9 +372,7 @@ async function sha256Base64Url(value) {
|
|
|
377
372
|
function base64UrlToBytes(value) {
|
|
378
373
|
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
379
374
|
throw new RelayCryptoError("invalid_base64", "Encrypted relay data is malformed.");
|
|
380
|
-
|
|
381
|
-
const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + padding);
|
|
382
|
-
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
375
|
+
return base64UrlBytes(value);
|
|
383
376
|
}
|
|
384
377
|
function p256PublicKey(value) {
|
|
385
378
|
const bytes = base64UrlToBytes(value);
|
package/dist/errors.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export declare function serverConnectError(serverCode: string, message: string,
|
|
|
39
39
|
status?: number;
|
|
40
40
|
cause?: unknown;
|
|
41
41
|
}): MdbaseConnectError;
|
|
42
|
-
export declare function operationProblem<Result>(envelope: MdbaseOperationEnvelope<Result>): KnownConnectProblem<"collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported" | "operation_invalid">;
|
|
42
|
+
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
43
|
/** True only when repeating an operation is safe without asking the user. */
|
|
44
44
|
export declare function isRetryableConnectError(error: unknown): boolean;
|
|
45
45
|
export {};
|
package/dist/errors.js
CHANGED
|
@@ -68,6 +68,11 @@ export function serverConnectError(serverCode, message, options = {}) {
|
|
|
68
68
|
export function operationProblem(envelope) {
|
|
69
69
|
const code = diagnosticProblemCode(envelope.diagnostics);
|
|
70
70
|
const message = diagnosticMessage(envelope.diagnostics);
|
|
71
|
+
// mdbase reports these record conditions as diagnostics; hosted authorities
|
|
72
|
+
// report them as problems. Surface one code for both.
|
|
73
|
+
if (code === "concurrent_modification" || code === "file_not_found" || code === "invalid_path") {
|
|
74
|
+
return connectProblem(code, message, { operationOutcome: "rejected" });
|
|
75
|
+
}
|
|
71
76
|
if (code === "collection_version_unsupported") {
|
|
72
77
|
const versions = diagnosticVersions(envelope.diagnostics);
|
|
73
78
|
return connectProblem(code, message, {
|
|
@@ -110,6 +115,10 @@ function diagnosticProblemCode(diagnostics) {
|
|
|
110
115
|
if (["collection_invalid", "collection_open_failed"]
|
|
111
116
|
.some((code) => codes.has(code)))
|
|
112
117
|
return "collection_invalid";
|
|
118
|
+
for (const code of ["concurrent_modification", "file_not_found", "invalid_path"]) {
|
|
119
|
+
if (codes.has(code))
|
|
120
|
+
return code;
|
|
121
|
+
}
|
|
113
122
|
return "operation_invalid";
|
|
114
123
|
}
|
|
115
124
|
function diagnosticVersions(diagnostics) {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export type { MdbaseCapabilityState, MdbaseCapabilityEvidence, MdbaseEffectiveCa
|
|
|
11
11
|
export { MdbaseConnectError, isRetryableConnectError } from "./errors.js";
|
|
12
12
|
export type { ConnectErrorContext } from "./errors.js";
|
|
13
13
|
export { MdbaseFileClient } from "./files.js";
|
|
14
|
+
export { MdbaseRecords } from "./records.js";
|
|
15
|
+
export type { MdbaseRecordLease, MdbaseRecordOpenOptions } from "./records.js";
|
|
16
|
+
export { MdbaseRecordSession } from "./record-session.js";
|
|
17
|
+
export type { MdbaseRecordResolution, MdbaseRecordSessionOptions, MdbaseRecordSessionSnapshot, MdbaseRecordSessionState } from "./record-session.js";
|
|
14
18
|
export type { MdbaseFileSource, CollectionFileDescriptor, MdbaseFileDeleteReceipt, MdbaseFileStreamSource, MdbaseFileProgress, MdbaseFileListOptions, MdbaseFileUploadOptions, MdbaseFileStreamUploadOptions, MdbaseFileDownloadOptions, MdbaseFileMoveOptions, MdbaseFileDeleteOptions } from "./files.js";
|
|
15
19
|
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
16
20
|
export type { MdbaseNotificationRegistrationOptions, MdbaseNotificationRegistration, MdbaseNativeNotificationRegistrationOptions, MdbaseNativeNotificationRegistration, MdbaseNativeNotificationData, MdbasePushPayload } from "./notifications.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { MdbaseConnection } from "./connection.js";
|
|
|
4
4
|
export { effectiveCapabilities } from "./capabilities.js";
|
|
5
5
|
export { MdbaseConnectError, isRetryableConnectError } from "./errors.js";
|
|
6
6
|
export { MdbaseFileClient } from "./files.js";
|
|
7
|
+
export { MdbaseRecords } from "./records.js";
|
|
8
|
+
export { MdbaseRecordSession } from "./record-session.js";
|
|
7
9
|
export { parseMdbasePushPayload, parseMdbaseNativeNotificationData, showMdbasePushNotification } from "./notifications.js";
|
|
8
10
|
export { externalStore } from "./external-store.js";
|
|
9
11
|
export { MdbaseBrowserSelection, MdbaseMemorySelection } from "./selection.js";
|
package/dist/internal-types.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface PendingMutation {
|
|
|
82
82
|
operation: CollectionOperation;
|
|
83
83
|
mutation?: MutationOperationIdentifier;
|
|
84
84
|
inputFingerprint: string;
|
|
85
|
+
/** `recordDigest(collectionId, path)` of an update's record, never the path itself. */
|
|
86
|
+
recordDigest?: string;
|
|
85
87
|
requestId: string;
|
|
86
88
|
envelope?: EncryptedRelayOperationRequest;
|
|
87
89
|
/** Exact plaintext protocol request for transports that do not use a grant envelope. */
|
package/dist/outcomes.d.ts
CHANGED
|
@@ -28,18 +28,18 @@ export type SessionProblemCode = AuthorizationProblemCode | "collection_not_read
|
|
|
28
28
|
export declare const SESSION_PROBLEM_CODES: readonly ["discovery_failed", "invalid_operation_response", "invalid_application_manifest", "invalid_request", "manifest_load_failed", "rate_limited", "temporarily_unavailable", "timeout", "access_denied", "application_declaration_mismatch", "application_identity_unavailable", "approval_window_blocked", "authorization_binding_incompatible", "authorization_cancelled", "authorization_replayed", "browser_required", "capability_contract_incompatible", "collection_access_denied", "collection_configuration_invalid", "collection_contracts_missing", "collection_incompatible", "collection_invalid", "collection_kind_unsupported", "collection_mismatch", "collection_not_found", "collection_type_registry_invalid", "collection_version_unsupported", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "device_authorization_failed", "durable_mutation_unsupported", "encryption_required", "expired_token", "invalid_application_authorization", "invalid_callback", "invalid_device_authorization_response", "invalid_token_response", "legacy_scope_reauthorization_required", "scope_denied", "reconnect_required", "token_exchange_failed", "transport_protocol_incompatible", "collection_not_ready", "collection_not_selected", "operation_cancelled", "session_destroyed", "session_not_started", "session_starting", "unknown_collection"];
|
|
29
29
|
export type CollectionSetupProblemCode = "collection_configuration_invalid" | "collection_invalid" | "collection_type_registry_invalid" | "collection_version_unsupported";
|
|
30
30
|
export type CollectionDescriptionProblemCode = CommonOperationProblemCode | CollectionSetupProblemCode;
|
|
31
|
-
export type CollectionReadProblemCode = CollectionDescriptionProblemCode | "invalid_path" | "operation_invalid";
|
|
31
|
+
export type CollectionReadProblemCode = CollectionDescriptionProblemCode | "file_not_found" | "invalid_path" | "operation_invalid";
|
|
32
32
|
export type CollectionQueryProblemCode = CollectionReadProblemCode | "cursor_capacity_exhausted" | "generation_expired" | "invalid_read_cursor" | "query_snapshot_changed" | "sandbox_unsupported";
|
|
33
33
|
export type CollectionMutationProblemCode = CollectionReadProblemCode | "concurrent_modification" | "invalid_preflight" | "pending_mutation_unresolved";
|
|
34
34
|
export type CollectionChangesProblemCode = CollectionDescriptionProblemCode | "change_cursor_reset";
|
|
35
35
|
export type CollectionTypeProblemCode = CollectionMutationProblemCode | "type_pack_provision_failed";
|
|
36
36
|
export declare const COLLECTION_SETUP_PROBLEM_CODES: readonly ["collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported"];
|
|
37
37
|
export declare const COLLECTION_DESCRIPTION_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported"];
|
|
38
|
-
export declare const COLLECTION_READ_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid"];
|
|
39
|
-
export declare const COLLECTION_QUERY_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "cursor_capacity_exhausted", "generation_expired", "invalid_read_cursor", "query_snapshot_changed", "sandbox_unsupported"];
|
|
40
|
-
export declare const COLLECTION_MUTATION_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved"];
|
|
38
|
+
export declare const COLLECTION_READ_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "file_not_found", "invalid_path", "operation_invalid"];
|
|
39
|
+
export declare const COLLECTION_QUERY_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "file_not_found", "invalid_path", "operation_invalid", "cursor_capacity_exhausted", "generation_expired", "invalid_read_cursor", "query_snapshot_changed", "sandbox_unsupported"];
|
|
40
|
+
export declare const COLLECTION_MUTATION_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "file_not_found", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved"];
|
|
41
41
|
export declare const COLLECTION_CHANGES_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "change_cursor_reset"];
|
|
42
|
-
export declare const COLLECTION_TYPE_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved", "type_pack_provision_failed"];
|
|
42
|
+
export declare const COLLECTION_TYPE_PROBLEM_CODES: readonly ["access_denied", "access_paused", "application_declaration_mismatch", "authorization_binding_incompatible", "authorization_expired", "capability_contract_incompatible", "collection_access_denied", "connector_identity_changed", "connector_offline", "connector_upgrade_required", "direct_operation_rejected", "durable_mutation_unsupported", "operation_outcome_unknown", "encryption_required", "encrypted_relay_rejected", "hosted_provider_unavailable", "insufficient_access", "invalid_encrypted_response", "invalid_operation_response", "invalid_request", "missing_grant_key", "mutation_recovery_expired", "mutation_request_conflict", "not_authorized", "operation_cancelled", "operation_failed", "rate_limited", "relay_authorization_expired", "relay_unavailable", "temporarily_unavailable", "timeout", "transport_protocol_incompatible", "unsupported_operation", "collection_configuration_invalid", "collection_invalid", "collection_type_registry_invalid", "collection_version_unsupported", "file_not_found", "invalid_path", "operation_invalid", "concurrent_modification", "invalid_preflight", "pending_mutation_unresolved", "type_pack_provision_failed"];
|
|
43
43
|
export declare function connectSuccess<Value>(value: Value, diagnostics?: MdbaseDiagnostic[]): ConnectSuccess<Value>;
|
|
44
44
|
export declare function connectFailure<Code extends ConnectProblemCode>(problem: ConnectProblem<Code>): ConnectFailure<Code>;
|
|
45
45
|
export declare function captureConnectOutcome<Value, Code extends ConnectProblemCode>(operation: () => Promise<Value>, allowedCodes: readonly Code[]): Promise<ConnectOutcome<Value, Code>>;
|
package/dist/outcomes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MutationOperationIdentifier } from "@mdbase-dev/connect-protocol";
|
|
2
2
|
import type { PendingMutation } from "./internal-types.js";
|
|
3
|
+
import type { PendingMutationSummary } from "./operation-types.js";
|
|
3
4
|
export declare class PendingMutationStore {
|
|
4
5
|
private readonly storage;
|
|
5
6
|
private readonly baseKey;
|
|
@@ -7,6 +8,8 @@ export declare class PendingMutationStore {
|
|
|
7
8
|
list(collectionId: string | null): PendingMutation[];
|
|
8
9
|
find(collectionId: string | null, requestId: string): PendingMutation | null;
|
|
9
10
|
identifier(pending: PendingMutation): MutationOperationIdentifier;
|
|
11
|
+
/** A pending write without its authority transport credentials. */
|
|
12
|
+
summary(pending: PendingMutation): PendingMutationSummary;
|
|
10
13
|
store(pending: PendingMutation): void;
|
|
11
14
|
take(requestId: string): PendingMutation | null;
|
|
12
15
|
private key;
|
|
@@ -37,6 +37,16 @@ export class PendingMutationStore {
|
|
|
37
37
|
identifier(pending) {
|
|
38
38
|
return pending.mutation ?? mutationOperationIdentifier(pending.operation, pending.request?.input ?? (pending.operation === "sync" ? { action: "mutate" } : {})) ?? (pending.operation === "sync" ? "sync:mutate" : pending.operation);
|
|
39
39
|
}
|
|
40
|
+
/** A pending write without its authority transport credentials. */
|
|
41
|
+
summary(pending) {
|
|
42
|
+
return {
|
|
43
|
+
requestId: pending.requestId,
|
|
44
|
+
operation: this.identifier(pending),
|
|
45
|
+
fingerprint: pending.inputFingerprint,
|
|
46
|
+
status: "outcome_unknown",
|
|
47
|
+
createdAt: new Date(pending.createdAt).toISOString()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
40
50
|
store(pending) {
|
|
41
51
|
this.storage.setItem(this.key(pending.requestId), JSON.stringify(pending));
|
|
42
52
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An equality token for one record in one collection. Stored with a pending
|
|
3
|
+
* update so an editing session can find its own interrupted write after a
|
|
4
|
+
* reload without the record path ever being persisted.
|
|
5
|
+
*/
|
|
6
|
+
export declare function recordDigest(collectionId: string, path: string): Promise<string>;
|
|
7
|
+
/** The digest a new pending mutation stores: only updates name a record. */
|
|
8
|
+
export declare function updatedRecordDigest(collectionId: string, operation: string, input: unknown): Promise<string | undefined>;
|
|
9
|
+
/** The request ID of a stored interrupted update to one record, if any. */
|
|
10
|
+
export declare function interruptedUpdate(pending: readonly {
|
|
11
|
+
operation: string;
|
|
12
|
+
requestId: string;
|
|
13
|
+
recordDigest?: string;
|
|
14
|
+
}[], collectionId: string, path: string): Promise<string | null>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An equality token for one record in one collection. Stored with a pending
|
|
3
|
+
* update so an editing session can find its own interrupted write after a
|
|
4
|
+
* reload without the record path ever being persisted.
|
|
5
|
+
*/
|
|
6
|
+
export async function recordDigest(collectionId, path) {
|
|
7
|
+
const bytes = new TextEncoder().encode(`${collectionId}\n${path}`);
|
|
8
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
|
|
9
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
}
|
|
11
|
+
/** The digest a new pending mutation stores: only updates name a record. */
|
|
12
|
+
export async function updatedRecordDigest(collectionId, operation, input) {
|
|
13
|
+
const path = input?.path;
|
|
14
|
+
return operation === "update" && typeof path === "string" ? recordDigest(collectionId, path) : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** The request ID of a stored interrupted update to one record, if any. */
|
|
17
|
+
export async function interruptedUpdate(pending, collectionId, path) {
|
|
18
|
+
const digest = await recordDigest(collectionId, path);
|
|
19
|
+
return pending.find((entry) => entry.operation === "update" && entry.recordDigest === digest)?.requestId ?? null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ConnectProblem, JsonObject } from "@mdbase-dev/connect-protocol";
|
|
2
|
+
import type { ConnectRequestOptions } from "./operation-types.js";
|
|
3
|
+
import { type ConnectOutcome } from "./outcomes.js";
|
|
4
|
+
/**
|
|
5
|
+
* - `saved`: the draft is the acknowledged record.
|
|
6
|
+
* - `unsaved`: local changes wait for autosave, `save()` or `flush()`.
|
|
7
|
+
* - `saving`: a write or exact recovery is in flight.
|
|
8
|
+
* - `conflict`: a newer record changed what was edited locally; see `remote`.
|
|
9
|
+
* - `recovery`: a write's outcome is unknown; no new write is sent until it is recovered.
|
|
10
|
+
* - `error`: the last write failed; local changes are kept.
|
|
11
|
+
* - `deleted`: the record no longer exists; local changes are kept.
|
|
12
|
+
*/
|
|
13
|
+
export type MdbaseRecordSessionState = "saved" | "unsaved" | "saving" | "conflict" | "recovery" | "error" | "deleted";
|
|
14
|
+
/** The parts of a record a session writes. Only locally changed parts are present. */
|
|
15
|
+
export interface MdbaseRecordChange {
|
|
16
|
+
body?: string;
|
|
17
|
+
patch?: JsonObject;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How a session reads and writes one record. `R` is the application's record
|
|
21
|
+
* value; the session reads it only through these accessors. Every expected
|
|
22
|
+
* failure is a `ConnectOutcome` failure; an adapter that throws has a bug.
|
|
23
|
+
*/
|
|
24
|
+
export interface MdbaseRecordSessionAdapter<R> {
|
|
25
|
+
revision(record: R): string;
|
|
26
|
+
body(record: R): string;
|
|
27
|
+
frontmatter?(record: R): JsonObject;
|
|
28
|
+
/** Apply `change` to the record at `base`'s revision, or fail without writing. */
|
|
29
|
+
write(base: R, change: MdbaseRecordChange, options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
30
|
+
/** The current record. A `file_not_found` failure means it was deleted. */
|
|
31
|
+
read?(base: R, options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
32
|
+
/** Exact continuation of a write whose outcome is unknown. Never a new write. */
|
|
33
|
+
recover?(requestId: string, options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
34
|
+
/** Whether that write's durable pending mutation is still unsettled. */
|
|
35
|
+
isPending?(requestId: string): boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface MdbaseRecordSessionOptions {
|
|
38
|
+
/** Write after this much idle time following an edit. Default `{ idleMs: 1000 }`. */
|
|
39
|
+
autosave?: {
|
|
40
|
+
idleMs: number;
|
|
41
|
+
} | false;
|
|
42
|
+
}
|
|
43
|
+
export interface MdbaseRecordSessionSnapshot<R> {
|
|
44
|
+
readonly state: MdbaseRecordSessionState;
|
|
45
|
+
readonly body: string;
|
|
46
|
+
/** The record's frontmatter with local patches applied. */
|
|
47
|
+
readonly frontmatter: JsonObject;
|
|
48
|
+
/** The latest record this session has accepted as its base. */
|
|
49
|
+
readonly record: R;
|
|
50
|
+
/** The newer record that conflicts with local changes, while `state` is `conflict`. */
|
|
51
|
+
readonly remote: R | null;
|
|
52
|
+
/** Local changes not yet acknowledged, including a write in flight or awaiting recovery. */
|
|
53
|
+
readonly dirty: boolean;
|
|
54
|
+
/** Why the last write or recovery failed. */
|
|
55
|
+
readonly problem: ConnectProblem | null;
|
|
56
|
+
/** The interrupted write's request ID, while `state` is `recovery`. */
|
|
57
|
+
readonly pendingRequestId?: string;
|
|
58
|
+
}
|
|
59
|
+
export type MdbaseRecordResolution = {
|
|
60
|
+
keep: "mine" | "theirs";
|
|
61
|
+
} | {
|
|
62
|
+
body: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* One open, editable record: debounced autosave, one write queue, own
|
|
66
|
+
* acknowledgements recognized by revision after each write settles,
|
|
67
|
+
* field-level conflict detection, and exact recovery of writes whose outcome
|
|
68
|
+
* is unknown. Share one session between every view of the same record.
|
|
69
|
+
*/
|
|
70
|
+
export declare class MdbaseRecordSession<R> {
|
|
71
|
+
private readonly adapter;
|
|
72
|
+
private record;
|
|
73
|
+
/** The local text `record` acknowledges; it differs from the record's body after server normalization. */
|
|
74
|
+
private baseBody;
|
|
75
|
+
private body;
|
|
76
|
+
private patch;
|
|
77
|
+
private remote;
|
|
78
|
+
private problem;
|
|
79
|
+
private deleted;
|
|
80
|
+
private busy;
|
|
81
|
+
private pending;
|
|
82
|
+
private incoming;
|
|
83
|
+
private readonly seen;
|
|
84
|
+
private readonly listeners;
|
|
85
|
+
private tail;
|
|
86
|
+
private queued;
|
|
87
|
+
private inFlight;
|
|
88
|
+
private timer;
|
|
89
|
+
private lastEdit;
|
|
90
|
+
private readonly idleMs;
|
|
91
|
+
private current;
|
|
92
|
+
constructor(record: R, adapter: MdbaseRecordSessionAdapter<R>, options?: MdbaseRecordSessionOptions);
|
|
93
|
+
get snapshot(): MdbaseRecordSessionSnapshot<R>;
|
|
94
|
+
getSnapshot: () => MdbaseRecordSessionSnapshot<R>;
|
|
95
|
+
/** Listeners run synchronously after every change, including each edit. */
|
|
96
|
+
subscribe: (listener: () => void) => (() => void);
|
|
97
|
+
setBody(body: string): void;
|
|
98
|
+
patchFrontmatter(patch: JsonObject): void;
|
|
99
|
+
/**
|
|
100
|
+
* Offer a draft kept outside the session, such as a crash-recovery copy. It
|
|
101
|
+
* is never written by itself: call `autosave()` or `save()`. A draft whose
|
|
102
|
+
* base is unknown or has since changed is a conflict with the current record.
|
|
103
|
+
*/
|
|
104
|
+
restore(draft: {
|
|
105
|
+
body: string;
|
|
106
|
+
baseBody?: string;
|
|
107
|
+
}): void;
|
|
108
|
+
/**
|
|
109
|
+
* Continue a write interrupted before this session existed, such as one
|
|
110
|
+
* found in durable storage after a reload. No new write is sent until it
|
|
111
|
+
* settles; with autosave, recovery is attempted after one idle interval.
|
|
112
|
+
*/
|
|
113
|
+
resumeRecovery(requestId: string): void;
|
|
114
|
+
/** Schedule an autosave one idle interval from now. */
|
|
115
|
+
autosave(): void;
|
|
116
|
+
/**
|
|
117
|
+
* A record published by another view or cache. It may be stale, so a
|
|
118
|
+
* revision this session has already seen is ignored, and records arriving
|
|
119
|
+
* while a write is unsettled wait until that write's acknowledgement is known.
|
|
120
|
+
*/
|
|
121
|
+
receive(record: R): void;
|
|
122
|
+
/**
|
|
123
|
+
* Read the current record after any queued write and apply it. Unlike a
|
|
124
|
+
* publication this is never stale, so a revision seen before is still
|
|
125
|
+
* applied (revisions repeat when content is reverted).
|
|
126
|
+
*/
|
|
127
|
+
refresh(options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
128
|
+
/**
|
|
129
|
+
* Accept a record this application produced outside the session's writes,
|
|
130
|
+
* such as a rename's result. It is never a conflict: local changes made
|
|
131
|
+
* meanwhile are kept on top of it.
|
|
132
|
+
*/
|
|
133
|
+
accept(record: R): void;
|
|
134
|
+
markDeleted(): void;
|
|
135
|
+
/**
|
|
136
|
+
* Write local changes now, or continue an interrupted write's exact
|
|
137
|
+
* recovery. Joins a save already in flight. Resolves with the accepted record.
|
|
138
|
+
*/
|
|
139
|
+
save(options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
140
|
+
/** Save until every local change is acknowledged, or report why it cannot be. */
|
|
141
|
+
flush(options?: ConnectRequestOptions): Promise<ConnectOutcome<R>>;
|
|
142
|
+
/** Run another operation on this record (rename, delete, …) in the session's write queue. */
|
|
143
|
+
run<Result>(operation: () => Promise<Result>): Promise<Result>;
|
|
144
|
+
/** Settle a conflict: keep local changes on top of the remote record, take the remote record, or use merged text. */
|
|
145
|
+
resolve(choice: MdbaseRecordResolution): void;
|
|
146
|
+
/** Drop local changes and return to the latest record. */
|
|
147
|
+
discard(): void;
|
|
148
|
+
private edited;
|
|
149
|
+
private schedule;
|
|
150
|
+
private dirtyLocally;
|
|
151
|
+
private write;
|
|
152
|
+
private recoverPending;
|
|
153
|
+
/** An adapter that throws breaks its contract: rethrow, but leave the queue usable. */
|
|
154
|
+
private guard;
|
|
155
|
+
private failure;
|
|
156
|
+
private acknowledge;
|
|
157
|
+
/** Read once to learn whether a failed write met a conflict, a convergent edit or deletion. */
|
|
158
|
+
private classifyFailure;
|
|
159
|
+
private settle;
|
|
160
|
+
/** Field-level three-way comparison of a newer record with the base and local changes. */
|
|
161
|
+
private classify;
|
|
162
|
+
private compute;
|
|
163
|
+
private emit;
|
|
164
|
+
}
|