chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* §19. Offline Command and Background Sync contract.
|
||||
*
|
||||
* The product capability is `NOT_SELECTED` (§19.1). An operation only becomes
|
||||
* queueable when the external package contribution provides `KEYED` retry
|
||||
* semantics plus a non-null recovery descriptor (§19.3); the frontend never
|
||||
* defines server idempotency or an inspect protocol of its own.
|
||||
*/
|
||||
|
||||
export const OFFLINE_COMMAND_BOUNDS = Object.freeze({
|
||||
operations: 64,
|
||||
defaultRequestBytes: 262_144,
|
||||
hardRequestBytes: 1_048_576,
|
||||
records: 1_000,
|
||||
datasetBytes: 50 * 1024 * 1024,
|
||||
senderLeaseMs: 30_000,
|
||||
leaseRenewMs: 10_000,
|
||||
batchCount: 10,
|
||||
batchWindowMs: 30_000,
|
||||
parallelSend: 1,
|
||||
retryBaseMs: 1_000,
|
||||
retryMaxMs: 300_000,
|
||||
attemptCap: 10,
|
||||
ordinaryRetentionMs: 7 * 24 * 60 * 60 * 1_000,
|
||||
conflictRetentionMs: 30 * 24 * 60 * 60 * 1_000,
|
||||
ackedSummaryRetentionMs: 24 * 60 * 60 * 1_000,
|
||||
syncReregisterMinimumMs: 60_000,
|
||||
});
|
||||
|
||||
/** §19.18. Background Sync is a wake-up hint only; it never sends a command. */
|
||||
export const OFFLINE_SYNC_TAG = "ca-outbox-v1" as const;
|
||||
|
||||
export interface InstalledOfflineOperation {
|
||||
readonly operationId: string;
|
||||
readonly contractPackageId: string;
|
||||
readonly maximumRequestBytes: number;
|
||||
readonly retentionClass: "STANDARD_7D";
|
||||
}
|
||||
|
||||
export interface InstalledOfflineCommandContribution {
|
||||
readonly datasetId: "OFFLINE_COMMANDS_V1";
|
||||
readonly operations: readonly InstalledOfflineOperation[];
|
||||
}
|
||||
|
||||
export type OfflineCommandState =
|
||||
| "PENDING"
|
||||
| "LEASED"
|
||||
| "FOREGROUND_REQUIRED"
|
||||
| "SENDING"
|
||||
| "RETRY_WAIT"
|
||||
| "ACKED"
|
||||
| "CONFLICT"
|
||||
| "EFFECT_UNKNOWN"
|
||||
| "EXPIRED";
|
||||
|
||||
export interface OfflineCommandRecordV1 {
|
||||
readonly recordVersion: 1;
|
||||
readonly commandId: string;
|
||||
readonly operationId: string;
|
||||
readonly contractPackageId: string;
|
||||
readonly contractPackageVersion: string;
|
||||
readonly contractPackageDigest: `sha256:${string}`;
|
||||
readonly scopePartition: string;
|
||||
readonly requestDigest: `sha256:${string}`;
|
||||
readonly requestPayload: Uint8Array;
|
||||
readonly idempotencyKey: string;
|
||||
readonly state: OfflineCommandState;
|
||||
readonly attempt: number;
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
readonly nextAttemptAt?: string;
|
||||
readonly leaseOwner?: string;
|
||||
readonly leaseExpiresAt?: string;
|
||||
readonly terminalCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* §19.10. Anything not listed is corruption. In particular an expired
|
||||
* `SENDING` record is never reset to `PENDING`: it becomes `EFFECT_UNKNOWN`.
|
||||
*/
|
||||
const ALLOWED_TRANSITIONS = Object.freeze({
|
||||
PENDING: Object.freeze(["LEASED", "FOREGROUND_REQUIRED", "EXPIRED"]),
|
||||
LEASED: Object.freeze(["SENDING", "PENDING"]),
|
||||
SENDING: Object.freeze([
|
||||
"ACKED",
|
||||
"RETRY_WAIT",
|
||||
"CONFLICT",
|
||||
"EFFECT_UNKNOWN",
|
||||
]),
|
||||
RETRY_WAIT: Object.freeze(["LEASED", "FOREGROUND_REQUIRED", "EXPIRED"]),
|
||||
FOREGROUND_REQUIRED: Object.freeze(["LEASED", "EXPIRED", "PENDING"]),
|
||||
ACKED: Object.freeze([]),
|
||||
CONFLICT: Object.freeze([]),
|
||||
EFFECT_UNKNOWN: Object.freeze(["ACKED", "PENDING"]),
|
||||
EXPIRED: Object.freeze([]),
|
||||
} satisfies Readonly<Record<OfflineCommandState, readonly OfflineCommandState[]>>);
|
||||
|
||||
export function isAllowedOfflineTransition(
|
||||
from: OfflineCommandState,
|
||||
to: OfflineCommandState,
|
||||
): boolean {
|
||||
const allowed: readonly OfflineCommandState[] = ALLOWED_TRANSITIONS[from];
|
||||
return allowed.includes(to);
|
||||
}
|
||||
|
||||
/** §19.21. Nothing sensitive reaches the UI: no payload, key, digest or partition. */
|
||||
export interface OfflineCommandSummary {
|
||||
readonly commandId: string;
|
||||
readonly operationLabelKey: string;
|
||||
readonly state: OfflineCommandState;
|
||||
readonly createdAt: string;
|
||||
readonly nextAction:
|
||||
| "WAIT"
|
||||
| "OPEN_APP"
|
||||
| "CHECK_STATUS"
|
||||
| "RESOLVE_CONFLICT"
|
||||
| "CONTACT_SUPPORT"
|
||||
| "DISMISS";
|
||||
}
|
||||
|
||||
export function validateOfflineCommandContribution(
|
||||
contribution: InstalledOfflineCommandContribution,
|
||||
): InstalledOfflineCommandContribution {
|
||||
if (contribution.datasetId !== "OFFLINE_COMMANDS_V1") {
|
||||
throw new TypeError("Offline command dataset identity is invalid.");
|
||||
}
|
||||
if (
|
||||
contribution.operations.length === 0 ||
|
||||
contribution.operations.length > OFFLINE_COMMAND_BOUNDS.operations
|
||||
) {
|
||||
throw new TypeError("Offline command operation count is out of range.");
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const operation of contribution.operations) {
|
||||
if (!operation.operationId || seen.has(operation.operationId)) {
|
||||
throw new TypeError("Duplicate offline command operation.");
|
||||
}
|
||||
seen.add(operation.operationId);
|
||||
if (
|
||||
!Number.isSafeInteger(operation.maximumRequestBytes) ||
|
||||
operation.maximumRequestBytes < 1 ||
|
||||
operation.maximumRequestBytes > OFFLINE_COMMAND_BOUNDS.hardRequestBytes ||
|
||||
operation.retentionClass !== "STANDARD_7D"
|
||||
) {
|
||||
throw new TypeError(
|
||||
`Offline command operation bounds invalid: ${operation.operationId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
datasetId: contribution.datasetId,
|
||||
operations: Object.freeze([...contribution.operations]),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user