@xemahq/ui-kernel 0.11.1 → 0.12.0
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/errors/http-error-status.d.ts +17 -0
- package/dist/errors/http-error-status.d.ts.map +1 -0
- package/dist/errors/http-error-status.js +59 -0
- package/dist/errors/http-error-status.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/biome-host/errors.d.ts.map +1 -1
- package/dist/lib/biome-host/errors.js +1 -19
- package/dist/lib/biome-host/errors.js.map +1 -1
- package/dist/offline/index.d.ts.map +1 -1
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/online-controller.d.ts.map +1 -1
- package/dist/offline/online-controller.js +6 -1
- package/dist/offline/online-controller.js.map +1 -1
- package/dist/offline/outbox.d.ts.map +1 -1
- package/dist/offline/outbox.js +2 -24
- package/dist/offline/outbox.js.map +1 -1
- package/package.json +1 -2
- package/src/index.ts +0 -17
- package/src/lib/biome-host/agent-validation.ts +0 -239
- package/src/lib/biome-host/biome-builders.ts +0 -112
- package/src/lib/biome-host/biome-mode.ts +0 -6
- package/src/lib/biome-host/biome-navigation.ts +0 -37
- package/src/lib/biome-host/biome-registry.ts +0 -299
- package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
- package/src/lib/biome-host/biome-scope.tsx +0 -52
- package/src/lib/biome-host/biome-scoped-query.ts +0 -134
- package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
- package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
- package/src/lib/biome-host/define-web-biome.ts +0 -222
- package/src/lib/biome-host/errors.ts +0 -220
- package/src/lib/biome-host/frontend-biome.ts +0 -256
- package/src/lib/biome-host/host-bridge.ts +0 -281
- package/src/lib/biome-host/host-sources.ts +0 -54
- package/src/lib/biome-host/i18n-hooks.ts +0 -51
- package/src/lib/biome-host/i18n-port.test.ts +0 -50
- package/src/lib/biome-host/i18n-port.ts +0 -96
- package/src/lib/biome-host/index.ts +0 -35
- package/src/lib/biome-host/realtime-hooks.ts +0 -75
- package/src/lib/biome-host/realtime-port.ts +0 -115
- package/src/lib/biome-host/session-contributions.ts +0 -296
- package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
- package/src/lib/biome-host/use-page-state.ts +0 -248
- package/src/lib/capabilities/capability-context.ts +0 -25
- package/src/lib/capabilities/capability-provider.tsx +0 -73
- package/src/lib/capabilities/index.ts +0 -17
- package/src/lib/capabilities/types.ts +0 -69
- package/src/lib/capabilities/use-capability.ts +0 -79
- package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
- package/src/lib/contribution-registry/contribution-registry.ts +0 -204
- package/src/lib/contribution-registry/index.ts +0 -9
- package/src/lib/contribution-registry/renderable-registry.ts +0 -142
- package/src/lib/system-bus/capability-invoke.ts +0 -92
- package/src/lib/system-bus/deeplink.ts +0 -210
- package/src/lib/system-bus/enums.ts +0 -86
- package/src/lib/system-bus/host-ports.ts +0 -93
- package/src/lib/system-bus/index.ts +0 -16
- package/src/lib/system-bus/intent-registry.ts +0 -109
- package/src/lib/system-bus/intents.ts +0 -113
- package/src/lib/system-bus/palette.ts +0 -78
- package/src/lib/system-bus/system-bus-builder.ts +0 -159
- package/src/lib/system-bus/system-bus.ts +0 -37
- package/src/lib/system-bus/windows.ts +0 -51
- package/src/offline/id.ts +0 -16
- package/src/offline/index.ts +0 -54
- package/src/offline/mutation.ts +0 -75
- package/src/offline/offline-status-badge.tsx +0 -51
- package/src/offline/online-controller.ts +0 -105
- package/src/offline/outbox.test.ts +0 -172
- package/src/offline/outbox.ts +0 -348
- package/src/offline/react-hooks.ts +0 -40
- package/src/offline/react.tsx +0 -32
- package/src/offline/storage.ts +0 -112
- package/src/offline/types.ts +0 -77
- package/src/org-db/components/DbResultTable.tsx +0 -176
- package/src/org-db/index.ts +0 -1
- package/src/pdf/index.ts +0 -11
- package/src/registry/index.ts +0 -9
- package/src/registry/lib/agent-validation-host.ts +0 -36
- package/src/registry/lib/biome-slot.tsx +0 -50
- package/src/registry/lib/biomes-enabled-context.ts +0 -20
- package/src/registry/lib/extension-points.ts +0 -134
- package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
- package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
- package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
- package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
- package/src/registry/lib/primitives/index.ts +0 -18
- package/src/registry/lib/session-context-builder.ts +0 -84
- package/src/registry/lib/session-context-provider.tsx +0 -33
- package/src/registry/lib/session-context.ts +0 -21
- package/src/registry/lib/session-selectors.ts +0 -272
- package/src/session/comments/CommentRail.tsx +0 -164
- package/src/session/index.ts +0 -3
- package/src/session/lib/cn.ts +0 -11
- package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
- package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
- package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
- package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
- package/src/session-kit/approvals/Obligation.tsx +0 -111
- package/src/session-kit/approvals/ScopedBody.tsx +0 -392
- package/src/session-kit/approvals/approval-icons.ts +0 -31
- package/src/session-kit/approvals/approval-model.ts +0 -204
- package/src/session-kit/approvals/index.ts +0 -22
- package/src/session-kit/approvals/obligation-display.ts +0 -100
- package/src/session-kit/approvals/risk-accent.ts +0 -47
- package/src/session-kit/approvals/scope-icons.ts +0 -19
- package/src/session-kit/combobox/Combobox.tsx +0 -358
- package/src/session-kit/combobox/use-click-outside.ts +0 -21
- package/src/session-kit/display/ContextHeader.tsx +0 -148
- package/src/session-kit/display/FileDiffCard.tsx +0 -140
- package/src/session-kit/display/MD.tsx +0 -162
- package/src/session-kit/display/MessageTurn.tsx +0 -157
- package/src/session-kit/display/ThinkingPanel.tsx +0 -81
- package/src/session-kit/display/TodoChecklist.tsx +0 -120
- package/src/session-kit/display/TokenMeter.tsx +0 -89
- package/src/session-kit/display/ToolStrip.tsx +0 -278
- package/src/session-kit/display/TypingDots.tsx +0 -24
- package/src/session-kit/index.ts +0 -44
- package/src/session-kit/lib/enums.ts +0 -66
- package/src/session-kit/lib/portal-accent.ts +0 -30
- package/src/session-kit/lib/status-dot.ts +0 -68
- package/src/session-kit/primitives/Avatar.tsx +0 -44
- package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
- package/src/session-kit/primitives/RiskPill.tsx +0 -95
- package/src/session-kit/primitives/ScopeDot.tsx +0 -47
- package/src/session-kit/primitives/Segmented.tsx +0 -71
- package/src/ui/branding/app-branding-context.ts +0 -20
- package/src/ui/branding/app-branding-provider.tsx +0 -123
- package/src/ui/branding/sanitize.ts +0 -89
- package/src/ui/breakpoints.ts +0 -14
- package/src/ui/capture/document-preview.tsx +0 -166
- package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
- package/src/ui/capture/field-spec-to-zod.ts +0 -174
- package/src/ui/capture/field-spec-validate.ts +0 -236
- package/src/ui/capture/field-spec.test.ts +0 -49
- package/src/ui/capture/field-spec.ts +0 -274
- package/src/ui/capture/index.ts +0 -51
- package/src/ui/capture/schema-field-input.tsx +0 -620
- package/src/ui/capture/schema-form.tsx +0 -149
- package/src/ui/capture/submit-values.test.ts +0 -70
- package/src/ui/capture/submit-values.ts +0 -63
- package/src/ui/capture/use-prefill.ts +0 -186
- package/src/ui/capture/use-schema-validation.ts +0 -145
- package/src/ui/capture/use-wizard.ts +0 -180
- package/src/ui/capture/value-format.ts +0 -32
- package/src/ui/capture/wizard.tsx +0 -105
- package/src/ui/charts/bar-chart.tsx +0 -139
- package/src/ui/charts/chart-frame.tsx +0 -125
- package/src/ui/charts/chart-types.ts +0 -52
- package/src/ui/charts/chart-utils.ts +0 -190
- package/src/ui/charts/charts.test.tsx +0 -204
- package/src/ui/charts/donut-chart.tsx +0 -129
- package/src/ui/charts/index.ts +0 -27
- package/src/ui/charts/line-chart.tsx +0 -202
- package/src/ui/charts/sparkline.tsx +0 -109
- package/src/ui/chrome/AsyncBoundary.tsx +0 -74
- package/src/ui/chrome/EmptyState.tsx +0 -257
- package/src/ui/chrome/ErrorCard.tsx +0 -63
- package/src/ui/chrome/LoadingState.tsx +0 -61
- package/src/ui/chrome/PageHeader.tsx +0 -137
- package/src/ui/chrome/RecordCardList.tsx +0 -98
- package/src/ui/chrome/StateCard.tsx +0 -150
- package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
- package/src/ui/chrome/page-shell.spec.tsx +0 -117
- package/src/ui/chrome/page-shell.tsx +0 -221
- package/src/ui/chrome/scope-badge.tsx +0 -40
- package/src/ui/chrome/status-badge.tsx +0 -79
- package/src/ui/cn.ts +0 -32
- package/src/ui/design-tokens.ts +0 -372
- package/src/ui/hooks/use-debounced-value.ts +0 -16
- package/src/ui/hooks/use-is-mobile.ts +0 -28
- package/src/ui/index.ts +0 -110
- package/src/ui/pdf/pdf-preview.tsx +0 -122
- package/src/ui/primitives/alert-dialog.tsx +0 -104
- package/src/ui/primitives/async-combobox.tsx +0 -182
- package/src/ui/primitives/badge-variants.ts +0 -21
- package/src/ui/primitives/badge.tsx +0 -25
- package/src/ui/primitives/button-variants.ts +0 -31
- package/src/ui/primitives/button.tsx +0 -29
- package/src/ui/primitives/card.tsx +0 -43
- package/src/ui/primitives/checkbox.tsx +0 -26
- package/src/ui/primitives/collapsible.tsx +0 -9
- package/src/ui/primitives/dialog.tsx +0 -103
- package/src/ui/primitives/dropdown-menu.tsx +0 -179
- package/src/ui/primitives/form-stepper.tsx +0 -94
- package/src/ui/primitives/form-stepper.types.ts +0 -15
- package/src/ui/primitives/input.tsx +0 -22
- package/src/ui/primitives/label.tsx +0 -17
- package/src/ui/primitives/overflow-tabs.tsx +0 -331
- package/src/ui/primitives/popover.tsx +0 -33
- package/src/ui/primitives/preview-device-frame.tsx +0 -213
- package/src/ui/primitives/radio-group.tsx +0 -36
- package/src/ui/primitives/resizable.tsx +0 -67
- package/src/ui/primitives/scroll-area.tsx +0 -38
- package/src/ui/primitives/select.tsx +0 -143
- package/src/ui/primitives/separator.tsx +0 -20
- package/src/ui/primitives/sheet.tsx +0 -107
- package/src/ui/primitives/skeleton.tsx +0 -99
- package/src/ui/primitives/switch.tsx +0 -27
- package/src/ui/primitives/table.tsx +0 -72
- package/src/ui/primitives/tabs.tsx +0 -53
- package/src/ui/primitives/tag-multi-select.tsx +0 -261
- package/src/ui/primitives/textarea.tsx +0 -21
- package/src/ui/primitives/tooltip.tsx +0 -30
- package/src/ui/primitives/url-preview-chrome.tsx +0 -103
- package/src/vista/GenericCatalogueSurface.tsx +0 -179
- package/src/vista/SurfaceFallbackCard.tsx +0 -109
- package/src/vista/VistaPanel.tsx +0 -163
- package/src/vista/form-payload.spec.ts +0 -139
- package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
- package/src/vista/html/CustomUiFrame.tsx +0 -206
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
- package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
- package/src/vista/html/custom-ui-frame-contract.ts +0 -88
- package/src/vista/html/custom-ui-intents.spec.ts +0 -135
- package/src/vista/html/custom-ui-intents.ts +0 -155
- package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
- package/src/vista/html/custom-ui-srcdoc.ts +0 -193
- package/src/vista/html/index.ts +0 -13
- package/src/vista/index.ts +0 -52
- package/src/vista/surface-catalogue.spec.ts +0 -140
- package/src/vista/surface-catalogue.ts +0 -90
- package/src/vista/surface-icons.ts +0 -57
- package/src/vista/surface-registry.ts +0 -56
- package/src/vista/surfaces/ChartSurface.tsx +0 -34
- package/src/vista/surfaces/FilesSurface.tsx +0 -202
- package/src/vista/surfaces/FormSurface.tsx +0 -380
- package/src/vista/surfaces/HtmlSurface.tsx +0 -43
- package/src/vista/surfaces/TableSurface.tsx +0 -100
- package/src/vista/surfaces/UrlSurface.tsx +0 -40
- package/src/vista/surfaces/blob-surfaces.tsx +0 -139
- package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
- package/src/vista/surfaces/elicitation-transport.ts +0 -367
- package/src/vista/surfaces/file-surface-utils.ts +0 -69
- package/src/vista/surfaces/index.ts +0 -24
- package/src/vista/surfaces/register-surfaces.tsx +0 -139
- package/src/vista/surfaces/text-surfaces.tsx +0 -132
- package/src/vista/use-vista-tabs.ts +0 -196
- package/src/vista/vista-code-renderer-context.ts +0 -16
- package/src/vista/vista-code-renderer.tsx +0 -35
- package/src/vista/vista-file-context.ts +0 -14
- package/src/vista/vista-file-source.tsx +0 -141
- package/src/vista/vista-form-context.ts +0 -20
- package/src/vista/vista-form-host.tsx +0 -48
- package/src/vista/vista-model.ts +0 -137
- package/src/vista/vista-open-context.tsx +0 -27
- package/src/vista/vista-open-state.ts +0 -11
- package/src/vista/vista-payloads.ts +0 -635
- package/src/vista/vista-tabs-manifest.ts +0 -138
- package/src/vista/vista-tools.ts +0 -232
package/src/offline/outbox.ts
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { newId } from './id';
|
|
2
|
-
import { OnlineController, isNetworkUnavailableError } from './online-controller';
|
|
3
|
-
import { createDefaultOutboxStorage } from './storage';
|
|
4
|
-
import {
|
|
5
|
-
OutboxEntryStatus,
|
|
6
|
-
type OutboxEntry,
|
|
7
|
-
type OutboxExecutor,
|
|
8
|
-
type OutboxSnapshot,
|
|
9
|
-
type OutboxStorage,
|
|
10
|
-
} from './types';
|
|
11
|
-
|
|
12
|
-
/** Read an HTTP status off an error (Orval/fetch shapes), or undefined. */
|
|
13
|
-
function statusOf(err: unknown): number | undefined {
|
|
14
|
-
if (!err || typeof err !== 'object') {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const e = err as { status?: number; response?: { status?: number } };
|
|
18
|
-
if (typeof e.status === 'number') {
|
|
19
|
-
return e.status;
|
|
20
|
-
}
|
|
21
|
-
if (e.response && typeof e.response.status === 'number') {
|
|
22
|
-
return e.response.status;
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Default retry policy: retry transport failures, timeouts, 429 and 5xx; a
|
|
28
|
-
* 4xx client error is the caller's bug — dead-letter it, do not hammer. */
|
|
29
|
-
function defaultIsRetriable(err: unknown): boolean {
|
|
30
|
-
if (isNetworkUnavailableError(err)) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
const status = statusOf(err);
|
|
34
|
-
if (status === undefined) {
|
|
35
|
-
return true; // unknown/opaque failure — give it a few tries
|
|
36
|
-
}
|
|
37
|
-
return status >= 500 || status === 429 || status === 408;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface OutboxOptions {
|
|
41
|
-
storage?: OutboxStorage;
|
|
42
|
-
online?: OnlineController;
|
|
43
|
-
/** Attempts before an entry is dead-lettered (Failed). Default 5. */
|
|
44
|
-
maxAttempts?: number;
|
|
45
|
-
/** Base backoff (ms) — doubled each attempt, capped at maxBackoffMs. */
|
|
46
|
-
baseBackoffMs?: number;
|
|
47
|
-
maxBackoffMs?: number;
|
|
48
|
-
isRetriable?: (err: unknown) => boolean;
|
|
49
|
-
/** Injectable clock (tests). Default Date.now. */
|
|
50
|
-
now?: () => number;
|
|
51
|
-
/** Injectable timer (tests). Default setTimeout. */
|
|
52
|
-
scheduler?: (fn: () => void, ms: number) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type OutboxListener = (snapshot: OutboxSnapshot) => void;
|
|
56
|
-
|
|
57
|
-
export interface EnqueueOptions {
|
|
58
|
-
idempotencyKey?: string;
|
|
59
|
-
label?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* A durable, provider-agnostic mutation outbox. Register an executor per `kind`
|
|
64
|
-
* once; then `enqueue` mutations (they persist immediately). When online the
|
|
65
|
-
* queue drains in FIFO order with retry/backoff and idempotency; when the send
|
|
66
|
-
* fails as network-unavailable it stops and resumes on reconnect. A mutation
|
|
67
|
-
* that exhausts retries is dead-lettered for manual retry/discard — never
|
|
68
|
-
* silently dropped.
|
|
69
|
-
*/
|
|
70
|
-
export class Outbox {
|
|
71
|
-
private readonly storage: OutboxStorage;
|
|
72
|
-
private readonly online: OnlineController;
|
|
73
|
-
private readonly maxAttempts: number;
|
|
74
|
-
private readonly baseBackoffMs: number;
|
|
75
|
-
private readonly maxBackoffMs: number;
|
|
76
|
-
private readonly isRetriable: (err: unknown) => boolean;
|
|
77
|
-
private readonly now: () => number;
|
|
78
|
-
private readonly scheduler: (fn: () => void, ms: number) => void;
|
|
79
|
-
|
|
80
|
-
private readonly executors = new Map<string, OutboxExecutor>();
|
|
81
|
-
private readonly listeners = new Set<OutboxListener>();
|
|
82
|
-
private cache: OutboxEntry[] = [];
|
|
83
|
-
private snapshot: OutboxSnapshot = {
|
|
84
|
-
entries: [],
|
|
85
|
-
pendingCount: 0,
|
|
86
|
-
failedCount: 0,
|
|
87
|
-
draining: false,
|
|
88
|
-
online: true,
|
|
89
|
-
};
|
|
90
|
-
private draining = false;
|
|
91
|
-
private drainRequested = false;
|
|
92
|
-
private retryScheduled = false;
|
|
93
|
-
private drainPromise: Promise<void> | null = null;
|
|
94
|
-
private unsubscribeOnline: (() => void) | null = null;
|
|
95
|
-
|
|
96
|
-
constructor(opts: OutboxOptions = {}) {
|
|
97
|
-
this.storage = opts.storage ?? createDefaultOutboxStorage();
|
|
98
|
-
this.online = opts.online ?? new OnlineController();
|
|
99
|
-
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
100
|
-
this.baseBackoffMs = opts.baseBackoffMs ?? 1000;
|
|
101
|
-
this.maxBackoffMs = opts.maxBackoffMs ?? 60_000;
|
|
102
|
-
this.isRetriable = opts.isRetriable ?? defaultIsRetriable;
|
|
103
|
-
this.now = opts.now ?? (() => Date.now());
|
|
104
|
-
this.scheduler =
|
|
105
|
-
opts.scheduler ?? ((fn, ms) => void setTimeout(fn, ms));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Register the sender for a mutation `kind`. Registering re-triggers a drain
|
|
109
|
-
* (entries that were parked waiting for their executor can now go). */
|
|
110
|
-
registerExecutor<TPayload>(kind: string, executor: OutboxExecutor<TPayload>): void {
|
|
111
|
-
this.executors.set(kind, executor as OutboxExecutor);
|
|
112
|
-
void this.drain();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
hasExecutor(kind: string): boolean {
|
|
116
|
-
return this.executors.has(kind);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Begin: start connectivity listeners, hydrate from storage, drain. */
|
|
120
|
-
async start(): Promise<void> {
|
|
121
|
-
this.online.start();
|
|
122
|
-
this.unsubscribeOnline?.();
|
|
123
|
-
this.unsubscribeOnline = this.online.subscribe((online) => {
|
|
124
|
-
if (online) {
|
|
125
|
-
void this.drain();
|
|
126
|
-
} else {
|
|
127
|
-
this.notify();
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
await this.hydrate();
|
|
131
|
-
void this.drain();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
stop(): void {
|
|
135
|
-
this.online.stop();
|
|
136
|
-
this.unsubscribeOnline?.();
|
|
137
|
-
this.unsubscribeOnline = null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
isOnline(): boolean {
|
|
141
|
-
return this.online.isOnline();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Persist a mutation for delivery. Resolves once it is durably queued. */
|
|
145
|
-
async enqueue<TPayload>(
|
|
146
|
-
kind: string,
|
|
147
|
-
payload: TPayload,
|
|
148
|
-
opts: EnqueueOptions = {},
|
|
149
|
-
): Promise<OutboxEntry<TPayload>> {
|
|
150
|
-
const ts = this.now();
|
|
151
|
-
const entry: OutboxEntry<TPayload> = {
|
|
152
|
-
id: newId(),
|
|
153
|
-
kind,
|
|
154
|
-
payload,
|
|
155
|
-
idempotencyKey: opts.idempotencyKey ?? newId(),
|
|
156
|
-
status: OutboxEntryStatus.Pending,
|
|
157
|
-
attempts: 0,
|
|
158
|
-
createdAt: ts,
|
|
159
|
-
updatedAt: ts,
|
|
160
|
-
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
161
|
-
};
|
|
162
|
-
await this.storage.put(entry);
|
|
163
|
-
await this.refresh();
|
|
164
|
-
void this.drain();
|
|
165
|
-
return entry;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Reset a dead-lettered entry to pending and drain. */
|
|
169
|
-
async retry(id: string): Promise<void> {
|
|
170
|
-
const entry = this.cache.find((e) => e.id === id);
|
|
171
|
-
if (!entry) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
await this.storage.put({
|
|
175
|
-
...entry,
|
|
176
|
-
status: OutboxEntryStatus.Pending,
|
|
177
|
-
attempts: 0,
|
|
178
|
-
updatedAt: this.now(),
|
|
179
|
-
});
|
|
180
|
-
await this.refresh();
|
|
181
|
-
void this.drain();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Permanently discard a queued/dead-lettered entry. */
|
|
185
|
-
async discard(id: string): Promise<void> {
|
|
186
|
-
await this.storage.delete(id);
|
|
187
|
-
await this.refresh();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async clear(): Promise<void> {
|
|
191
|
-
await this.storage.clear();
|
|
192
|
-
await this.refresh();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Returns a STABLE reference between changes (safe for useSyncExternalStore). */
|
|
196
|
-
getSnapshot(): OutboxSnapshot {
|
|
197
|
-
return this.snapshot;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
subscribe(listener: OutboxListener): () => void {
|
|
201
|
-
this.listeners.add(listener);
|
|
202
|
-
listener(this.snapshot);
|
|
203
|
-
return () => this.listeners.delete(listener);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Attempt to deliver every deliverable entry once, in FIFO order. Safe to
|
|
207
|
-
* call concurrently — a single flight runs and re-runs if requested. */
|
|
208
|
-
async drain(): Promise<void> {
|
|
209
|
-
if (!this.online.isOnline()) {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
// Single-flight: a concurrent drain() awaits the in-flight one (and requests
|
|
213
|
-
// an extra pass) rather than returning early — so `await drain()` always
|
|
214
|
-
// resolves after the queue is fully processed.
|
|
215
|
-
if (this.drainPromise) {
|
|
216
|
-
this.drainRequested = true;
|
|
217
|
-
return this.drainPromise;
|
|
218
|
-
}
|
|
219
|
-
this.drainPromise = this.runDrain();
|
|
220
|
-
try {
|
|
221
|
-
await this.drainPromise;
|
|
222
|
-
} finally {
|
|
223
|
-
this.drainPromise = null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private async runDrain(): Promise<void> {
|
|
228
|
-
this.draining = true;
|
|
229
|
-
this.notify();
|
|
230
|
-
try {
|
|
231
|
-
do {
|
|
232
|
-
this.drainRequested = false;
|
|
233
|
-
await this.drainOnce();
|
|
234
|
-
} while (this.shouldContinueDrain());
|
|
235
|
-
} finally {
|
|
236
|
-
this.draining = false;
|
|
237
|
-
this.notify();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private shouldContinueDrain(): boolean {
|
|
242
|
-
return this.drainRequested && this.online.isOnline();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── internals ──────────────────────────────────────────────────────────────
|
|
246
|
-
|
|
247
|
-
private async hydrate(): Promise<void> {
|
|
248
|
-
const stored = await this.storage.getAll();
|
|
249
|
-
// Recover entries that were mid-flight when the tab/process died: they may
|
|
250
|
-
// or may not have applied server-side, but the idempotency key makes a
|
|
251
|
-
// redelivery safe, so re-queue them rather than drop them.
|
|
252
|
-
for (const entry of stored) {
|
|
253
|
-
if (entry.status === OutboxEntryStatus.Inflight) {
|
|
254
|
-
entry.status = OutboxEntryStatus.Pending;
|
|
255
|
-
await this.storage.put(entry);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
await this.refresh();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private async drainOnce(): Promise<void> {
|
|
262
|
-
const entries = await this.storage.getAll();
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
if (!this.online.isOnline()) {
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
if (entry.status === OutboxEntryStatus.Failed) {
|
|
268
|
-
continue; // dead-letter — needs manual retry
|
|
269
|
-
}
|
|
270
|
-
const executor = this.executors.get(entry.kind);
|
|
271
|
-
if (!executor) {
|
|
272
|
-
continue; // no sender registered yet — leave pending
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const attempt = entry.attempts + 1;
|
|
276
|
-
await this.write({ ...entry, status: OutboxEntryStatus.Inflight, attempts: attempt });
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
await executor(entry.payload, {
|
|
280
|
-
idempotencyKey: entry.idempotencyKey,
|
|
281
|
-
attempt,
|
|
282
|
-
entryId: entry.id,
|
|
283
|
-
});
|
|
284
|
-
await this.storage.delete(entry.id);
|
|
285
|
-
await this.refresh();
|
|
286
|
-
} catch (err) {
|
|
287
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
-
if (isNetworkUnavailableError(err)) {
|
|
289
|
-
// Network is down. This is NOT the payload's fault, so do NOT advance
|
|
290
|
-
// the attempt counter (being offline for a long time must never
|
|
291
|
-
// dead-letter a capture). Park the entry, stop the burst, and resume
|
|
292
|
-
// on reconnect or after a backoff.
|
|
293
|
-
await this.write({ ...entry, status: OutboxEntryStatus.Pending, attempts: entry.attempts, lastError: message });
|
|
294
|
-
this.scheduleRetry(entry.attempts + 1);
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
if (this.isRetriable(err) && attempt < this.maxAttempts) {
|
|
298
|
-
await this.write({ ...entry, status: OutboxEntryStatus.Retrying, attempts: attempt, lastError: message });
|
|
299
|
-
this.scheduleRetry(attempt);
|
|
300
|
-
} else {
|
|
301
|
-
// 4xx client error, or retries exhausted → dead-letter for manual
|
|
302
|
-
// retry/discard. Never silently dropped.
|
|
303
|
-
await this.write({ ...entry, status: OutboxEntryStatus.Failed, attempts: attempt, lastError: message });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private scheduleRetry(attempt: number): void {
|
|
310
|
-
if (this.retryScheduled) {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
this.retryScheduled = true;
|
|
314
|
-
const delay = Math.min(
|
|
315
|
-
this.maxBackoffMs,
|
|
316
|
-
this.baseBackoffMs * 2 ** Math.max(0, attempt - 1),
|
|
317
|
-
);
|
|
318
|
-
this.scheduler(() => {
|
|
319
|
-
this.retryScheduled = false;
|
|
320
|
-
void this.drain();
|
|
321
|
-
}, delay);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private async write(entry: OutboxEntry): Promise<void> {
|
|
325
|
-
await this.storage.put({ ...entry, updatedAt: this.now() });
|
|
326
|
-
await this.refresh();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private async refresh(): Promise<void> {
|
|
330
|
-
this.cache = await this.storage.getAll();
|
|
331
|
-
this.notify();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private notify(): void {
|
|
335
|
-
// Rebuild the cached snapshot ONCE per change so getSnapshot() returns a
|
|
336
|
-
// stable reference (required by React's useSyncExternalStore).
|
|
337
|
-
this.snapshot = {
|
|
338
|
-
entries: this.cache,
|
|
339
|
-
pendingCount: this.cache.filter((e) => e.status !== OutboxEntryStatus.Failed).length,
|
|
340
|
-
failedCount: this.cache.filter((e) => e.status === OutboxEntryStatus.Failed).length,
|
|
341
|
-
draining: this.draining,
|
|
342
|
-
online: this.online.isOnline(),
|
|
343
|
-
};
|
|
344
|
-
for (const l of this.listeners) {
|
|
345
|
-
l(this.snapshot);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useSyncExternalStore } from 'react';
|
|
2
|
-
|
|
3
|
-
import { type Outbox } from './outbox';
|
|
4
|
-
|
|
5
|
-
import type { OutboxSnapshot } from './types';
|
|
6
|
-
|
|
7
|
-
export const OutboxContext = createContext<Outbox | null>(null);
|
|
8
|
-
|
|
9
|
-
export function useOutbox(): Outbox {
|
|
10
|
-
const ctx = useContext(OutboxContext);
|
|
11
|
-
if (!ctx) {
|
|
12
|
-
throw new Error('useOutbox must be used within an <OutboxProvider>.');
|
|
13
|
-
}
|
|
14
|
-
return ctx;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveOutbox(
|
|
18
|
-
explicit: Outbox | undefined,
|
|
19
|
-
ctx: Outbox | null,
|
|
20
|
-
): Outbox {
|
|
21
|
-
const instance = explicit ?? ctx;
|
|
22
|
-
if (!instance) {
|
|
23
|
-
throw new Error('An <OutboxProvider> or an explicit outbox is required.');
|
|
24
|
-
}
|
|
25
|
-
return instance;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function useOutboxSnapshot(outbox?: Outbox): OutboxSnapshot {
|
|
29
|
-
const ctx = useContext(OutboxContext);
|
|
30
|
-
const instance = resolveOutbox(outbox, ctx);
|
|
31
|
-
return useSyncExternalStore(
|
|
32
|
-
(cb) => instance.subscribe(cb),
|
|
33
|
-
() => instance.getSnapshot(),
|
|
34
|
-
() => instance.getSnapshot(),
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function useOnlineStatus(outbox?: Outbox): boolean {
|
|
39
|
-
return useOutboxSnapshot(outbox).online;
|
|
40
|
-
}
|
package/src/offline/react.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useMemo,
|
|
4
|
-
useRef,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
} from 'react';
|
|
7
|
-
|
|
8
|
-
import { Outbox, type OutboxOptions } from './outbox';
|
|
9
|
-
import { OutboxContext } from './react-hooks';
|
|
10
|
-
|
|
11
|
-
export interface OutboxProviderProps {
|
|
12
|
-
/** Bring your own Outbox, or let the provider create one from `options`. */
|
|
13
|
-
outbox?: Outbox;
|
|
14
|
-
options?: OutboxOptions;
|
|
15
|
-
children: ReactNode;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Provides a shared {@link Outbox} and manages its lifecycle (starts
|
|
19
|
-
* connectivity listeners + hydrates on mount, stops on unmount). */
|
|
20
|
-
export function OutboxProvider({ outbox, options, children }: OutboxProviderProps): JSX.Element {
|
|
21
|
-
// Options are captured once; changing them after mount does not recreate the
|
|
22
|
-
// outbox (a durable queue must not be swapped underneath in-flight work).
|
|
23
|
-
const optionsRef = useRef(options);
|
|
24
|
-
const instance = useMemo(() => outbox ?? new Outbox(optionsRef.current), [outbox]);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
void instance.start();
|
|
28
|
-
return () => instance.stop();
|
|
29
|
-
}, [instance]);
|
|
30
|
-
|
|
31
|
-
return <OutboxContext.Provider value={instance}>{children}</OutboxContext.Provider>;
|
|
32
|
-
}
|
package/src/offline/storage.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type { OutboxEntry, OutboxStorage } from './types';
|
|
2
|
-
|
|
3
|
-
/** In-memory outbox storage — the SSR / test fallback (no persistence). */
|
|
4
|
-
export class InMemoryOutboxStorage implements OutboxStorage {
|
|
5
|
-
private readonly map = new Map<string, OutboxEntry>();
|
|
6
|
-
|
|
7
|
-
getAll(): Promise<OutboxEntry[]> {
|
|
8
|
-
return Promise.resolve(
|
|
9
|
-
[...this.map.values()].sort((a, b) => a.createdAt - b.createdAt),
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
put(entry: OutboxEntry): Promise<void> {
|
|
14
|
-
this.map.set(entry.id, { ...entry });
|
|
15
|
-
return Promise.resolve();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
delete(id: string): Promise<void> {
|
|
19
|
-
this.map.delete(id);
|
|
20
|
-
return Promise.resolve();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
clear(): Promise<void> {
|
|
24
|
-
this.map.clear();
|
|
25
|
-
return Promise.resolve();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const DB_VERSION = 1;
|
|
30
|
-
|
|
31
|
-
/** IndexedDB-backed outbox storage — durable across reloads and app restarts,
|
|
32
|
-
* so a mutation captured offline survives the tab being closed. */
|
|
33
|
-
export class IndexedDbOutboxStorage implements OutboxStorage {
|
|
34
|
-
private dbPromise: Promise<IDBDatabase> | null = null;
|
|
35
|
-
|
|
36
|
-
constructor(
|
|
37
|
-
private readonly dbName = 'xema-offline-outbox',
|
|
38
|
-
private readonly storeName = 'entries',
|
|
39
|
-
) {}
|
|
40
|
-
|
|
41
|
-
private open(): Promise<IDBDatabase> {
|
|
42
|
-
if (this.dbPromise) {
|
|
43
|
-
return this.dbPromise;
|
|
44
|
-
}
|
|
45
|
-
this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
46
|
-
const req = indexedDB.open(this.dbName, DB_VERSION);
|
|
47
|
-
req.onupgradeneeded = () => {
|
|
48
|
-
const db = req.result;
|
|
49
|
-
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
50
|
-
db.createObjectStore(this.storeName, { keyPath: 'id' });
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
req.onsuccess = () => resolve(req.result);
|
|
54
|
-
req.onerror = () => reject(req.error ?? new Error('indexedDB open failed'));
|
|
55
|
-
});
|
|
56
|
-
return this.dbPromise;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private async tx<T>(
|
|
60
|
-
mode: IDBTransactionMode,
|
|
61
|
-
run: (store: IDBObjectStore) => IDBRequest<T> | void,
|
|
62
|
-
): Promise<T | undefined> {
|
|
63
|
-
const db = await this.open();
|
|
64
|
-
return new Promise<T | undefined>((resolve, reject) => {
|
|
65
|
-
const transaction = db.transaction(this.storeName, mode);
|
|
66
|
-
const store = transaction.objectStore(this.storeName);
|
|
67
|
-
let request: IDBRequest<T> | void;
|
|
68
|
-
try {
|
|
69
|
-
request = run(store);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
reject(err);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
transaction.onabort = () =>
|
|
75
|
-
reject(transaction.error ?? new Error('indexedDB tx aborted'));
|
|
76
|
-
transaction.oncomplete = () =>
|
|
77
|
-
resolve(request ? request.result : undefined);
|
|
78
|
-
transaction.onerror = () =>
|
|
79
|
-
reject(transaction.error ?? new Error('indexedDB tx error'));
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async getAll(): Promise<OutboxEntry[]> {
|
|
84
|
-
const all = (await this.tx<OutboxEntry[]>('readonly', (s) => s.getAll())) ?? [];
|
|
85
|
-
return all.sort((a, b) => a.createdAt - b.createdAt);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async put(entry: OutboxEntry): Promise<void> {
|
|
89
|
-
await this.tx('readwrite', (s) => s.put(entry));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async delete(id: string): Promise<void> {
|
|
93
|
-
await this.tx('readwrite', (s) => s.delete(id));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async clear(): Promise<void> {
|
|
97
|
-
await this.tx('readwrite', (s) => s.clear());
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Pick the best available storage: IndexedDB in a browser, else in-memory
|
|
102
|
-
* (SSR / tests / private-mode fallbacks). */
|
|
103
|
-
export function createDefaultOutboxStorage(): OutboxStorage {
|
|
104
|
-
if (typeof indexedDB !== 'undefined') {
|
|
105
|
-
try {
|
|
106
|
-
return new IndexedDbOutboxStorage();
|
|
107
|
-
} catch {
|
|
108
|
-
// Some privacy modes throw on open — fall back to in-memory.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return new InMemoryOutboxStorage();
|
|
112
|
-
}
|
package/src/offline/types.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// @xemahq/ui-kernel/offline — types
|
|
3
|
-
//
|
|
4
|
-
// A generic offline field-capture kit: an IndexedDB-backed outbox that queues
|
|
5
|
-
// mutations while disconnected and drains them (with retry + idempotency) on
|
|
6
|
-
// reconnect. Provider-agnostic — any mutation (an app-forge record submit, a
|
|
7
|
-
// status change, …) is enqueued by a stable `kind` whose executor is registered
|
|
8
|
-
// once. The motivating case is field/trade-fair capture with no signal.
|
|
9
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
-
|
|
11
|
-
/** Lifecycle of a queued mutation. */
|
|
12
|
-
export enum OutboxEntryStatus {
|
|
13
|
-
/** Waiting to be sent (offline, or not yet attempted). */
|
|
14
|
-
Pending = 'pending',
|
|
15
|
-
/** Currently being sent. */
|
|
16
|
-
Inflight = 'inflight',
|
|
17
|
-
/** Failed transiently; will be retried (attempts < maxAttempts). */
|
|
18
|
-
Retrying = 'retrying',
|
|
19
|
-
/** Exhausted retries — needs manual retry or discard (dead-letter). */
|
|
20
|
-
Failed = 'failed',
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** One queued mutation. `payload` is opaque to the outbox; the executor for
|
|
24
|
-
* `kind` interprets it. */
|
|
25
|
-
export interface OutboxEntry<TPayload = unknown> {
|
|
26
|
-
/** Stable entry id (also the default idempotency key). */
|
|
27
|
-
id: string;
|
|
28
|
-
/** Registered executor key (e.g. "app-forge:create-record"). */
|
|
29
|
-
kind: string;
|
|
30
|
-
payload: TPayload;
|
|
31
|
-
/** Sent to the backend so a redelivered mutation is not double-applied. */
|
|
32
|
-
idempotencyKey: string;
|
|
33
|
-
status: OutboxEntryStatus;
|
|
34
|
-
attempts: number;
|
|
35
|
-
createdAt: number;
|
|
36
|
-
updatedAt: number;
|
|
37
|
-
/** Last error message (for the dead-letter UI). */
|
|
38
|
-
lastError?: string;
|
|
39
|
-
/** Optional human label for the pending-items UI. */
|
|
40
|
-
label?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Persistence backend for the outbox. IndexedDB in the browser; in-memory for
|
|
44
|
-
* SSR / tests. */
|
|
45
|
-
export interface OutboxStorage {
|
|
46
|
-
getAll(): Promise<OutboxEntry[]>;
|
|
47
|
-
put(entry: OutboxEntry): Promise<void>;
|
|
48
|
-
delete(id: string): Promise<void>;
|
|
49
|
-
clear(): Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Context passed to an executor when a queued mutation is sent. */
|
|
53
|
-
export interface OutboxExecuteContext {
|
|
54
|
-
/** Pass to the backend (e.g. as an `Idempotency-Key` header). */
|
|
55
|
-
idempotencyKey: string;
|
|
56
|
-
/** How many times this entry has now been attempted (1-based). */
|
|
57
|
-
attempt: number;
|
|
58
|
-
entryId: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Sends one queued mutation. Throwing schedules a retry (see
|
|
62
|
-
* {@link OutboxOptions.isRetriable}). */
|
|
63
|
-
export type OutboxExecutor<TPayload = unknown> = (
|
|
64
|
-
payload: TPayload,
|
|
65
|
-
ctx: OutboxExecuteContext,
|
|
66
|
-
) => Promise<void>;
|
|
67
|
-
|
|
68
|
-
/** Snapshot of outbox state for UI (offline badge, pending count, dead-letter). */
|
|
69
|
-
export interface OutboxSnapshot {
|
|
70
|
-
entries: OutboxEntry[];
|
|
71
|
-
pendingCount: number;
|
|
72
|
-
failedCount: number;
|
|
73
|
-
/** True while a drain is in progress. */
|
|
74
|
-
draining: boolean;
|
|
75
|
-
/** Current connectivity (from the outbox's online controller). */
|
|
76
|
-
online: boolean;
|
|
77
|
-
}
|