@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.
Files changed (257) hide show
  1. package/dist/errors/http-error-status.d.ts +17 -0
  2. package/dist/errors/http-error-status.d.ts.map +1 -0
  3. package/dist/errors/http-error-status.js +59 -0
  4. package/dist/errors/http-error-status.js.map +1 -0
  5. package/dist/errors/index.d.ts +2 -0
  6. package/dist/errors/index.d.ts.map +1 -0
  7. package/dist/errors/index.js +2 -0
  8. package/dist/errors/index.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/biome-host/errors.d.ts.map +1 -1
  14. package/dist/lib/biome-host/errors.js +1 -19
  15. package/dist/lib/biome-host/errors.js.map +1 -1
  16. package/dist/offline/index.d.ts.map +1 -1
  17. package/dist/offline/index.js.map +1 -1
  18. package/dist/offline/online-controller.d.ts.map +1 -1
  19. package/dist/offline/online-controller.js +6 -1
  20. package/dist/offline/online-controller.js.map +1 -1
  21. package/dist/offline/outbox.d.ts.map +1 -1
  22. package/dist/offline/outbox.js +2 -24
  23. package/dist/offline/outbox.js.map +1 -1
  24. package/package.json +1 -2
  25. package/src/index.ts +0 -17
  26. package/src/lib/biome-host/agent-validation.ts +0 -239
  27. package/src/lib/biome-host/biome-builders.ts +0 -112
  28. package/src/lib/biome-host/biome-mode.ts +0 -6
  29. package/src/lib/biome-host/biome-navigation.ts +0 -37
  30. package/src/lib/biome-host/biome-registry.ts +0 -299
  31. package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
  32. package/src/lib/biome-host/biome-scope.tsx +0 -52
  33. package/src/lib/biome-host/biome-scoped-query.ts +0 -134
  34. package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
  35. package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
  36. package/src/lib/biome-host/define-web-biome.ts +0 -222
  37. package/src/lib/biome-host/errors.ts +0 -220
  38. package/src/lib/biome-host/frontend-biome.ts +0 -256
  39. package/src/lib/biome-host/host-bridge.ts +0 -281
  40. package/src/lib/biome-host/host-sources.ts +0 -54
  41. package/src/lib/biome-host/i18n-hooks.ts +0 -51
  42. package/src/lib/biome-host/i18n-port.test.ts +0 -50
  43. package/src/lib/biome-host/i18n-port.ts +0 -96
  44. package/src/lib/biome-host/index.ts +0 -35
  45. package/src/lib/biome-host/realtime-hooks.ts +0 -75
  46. package/src/lib/biome-host/realtime-port.ts +0 -115
  47. package/src/lib/biome-host/session-contributions.ts +0 -296
  48. package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
  49. package/src/lib/biome-host/use-page-state.ts +0 -248
  50. package/src/lib/capabilities/capability-context.ts +0 -25
  51. package/src/lib/capabilities/capability-provider.tsx +0 -73
  52. package/src/lib/capabilities/index.ts +0 -17
  53. package/src/lib/capabilities/types.ts +0 -69
  54. package/src/lib/capabilities/use-capability.ts +0 -79
  55. package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
  56. package/src/lib/contribution-registry/contribution-registry.ts +0 -204
  57. package/src/lib/contribution-registry/index.ts +0 -9
  58. package/src/lib/contribution-registry/renderable-registry.ts +0 -142
  59. package/src/lib/system-bus/capability-invoke.ts +0 -92
  60. package/src/lib/system-bus/deeplink.ts +0 -210
  61. package/src/lib/system-bus/enums.ts +0 -86
  62. package/src/lib/system-bus/host-ports.ts +0 -93
  63. package/src/lib/system-bus/index.ts +0 -16
  64. package/src/lib/system-bus/intent-registry.ts +0 -109
  65. package/src/lib/system-bus/intents.ts +0 -113
  66. package/src/lib/system-bus/palette.ts +0 -78
  67. package/src/lib/system-bus/system-bus-builder.ts +0 -159
  68. package/src/lib/system-bus/system-bus.ts +0 -37
  69. package/src/lib/system-bus/windows.ts +0 -51
  70. package/src/offline/id.ts +0 -16
  71. package/src/offline/index.ts +0 -54
  72. package/src/offline/mutation.ts +0 -75
  73. package/src/offline/offline-status-badge.tsx +0 -51
  74. package/src/offline/online-controller.ts +0 -105
  75. package/src/offline/outbox.test.ts +0 -172
  76. package/src/offline/outbox.ts +0 -348
  77. package/src/offline/react-hooks.ts +0 -40
  78. package/src/offline/react.tsx +0 -32
  79. package/src/offline/storage.ts +0 -112
  80. package/src/offline/types.ts +0 -77
  81. package/src/org-db/components/DbResultTable.tsx +0 -176
  82. package/src/org-db/index.ts +0 -1
  83. package/src/pdf/index.ts +0 -11
  84. package/src/registry/index.ts +0 -9
  85. package/src/registry/lib/agent-validation-host.ts +0 -36
  86. package/src/registry/lib/biome-slot.tsx +0 -50
  87. package/src/registry/lib/biomes-enabled-context.ts +0 -20
  88. package/src/registry/lib/extension-points.ts +0 -134
  89. package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
  90. package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
  91. package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
  92. package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
  93. package/src/registry/lib/primitives/index.ts +0 -18
  94. package/src/registry/lib/session-context-builder.ts +0 -84
  95. package/src/registry/lib/session-context-provider.tsx +0 -33
  96. package/src/registry/lib/session-context.ts +0 -21
  97. package/src/registry/lib/session-selectors.ts +0 -272
  98. package/src/session/comments/CommentRail.tsx +0 -164
  99. package/src/session/index.ts +0 -3
  100. package/src/session/lib/cn.ts +0 -11
  101. package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
  102. package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
  103. package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
  104. package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
  105. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
  106. package/src/session-kit/approvals/Obligation.tsx +0 -111
  107. package/src/session-kit/approvals/ScopedBody.tsx +0 -392
  108. package/src/session-kit/approvals/approval-icons.ts +0 -31
  109. package/src/session-kit/approvals/approval-model.ts +0 -204
  110. package/src/session-kit/approvals/index.ts +0 -22
  111. package/src/session-kit/approvals/obligation-display.ts +0 -100
  112. package/src/session-kit/approvals/risk-accent.ts +0 -47
  113. package/src/session-kit/approvals/scope-icons.ts +0 -19
  114. package/src/session-kit/combobox/Combobox.tsx +0 -358
  115. package/src/session-kit/combobox/use-click-outside.ts +0 -21
  116. package/src/session-kit/display/ContextHeader.tsx +0 -148
  117. package/src/session-kit/display/FileDiffCard.tsx +0 -140
  118. package/src/session-kit/display/MD.tsx +0 -162
  119. package/src/session-kit/display/MessageTurn.tsx +0 -157
  120. package/src/session-kit/display/ThinkingPanel.tsx +0 -81
  121. package/src/session-kit/display/TodoChecklist.tsx +0 -120
  122. package/src/session-kit/display/TokenMeter.tsx +0 -89
  123. package/src/session-kit/display/ToolStrip.tsx +0 -278
  124. package/src/session-kit/display/TypingDots.tsx +0 -24
  125. package/src/session-kit/index.ts +0 -44
  126. package/src/session-kit/lib/enums.ts +0 -66
  127. package/src/session-kit/lib/portal-accent.ts +0 -30
  128. package/src/session-kit/lib/status-dot.ts +0 -68
  129. package/src/session-kit/primitives/Avatar.tsx +0 -44
  130. package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
  131. package/src/session-kit/primitives/RiskPill.tsx +0 -95
  132. package/src/session-kit/primitives/ScopeDot.tsx +0 -47
  133. package/src/session-kit/primitives/Segmented.tsx +0 -71
  134. package/src/ui/branding/app-branding-context.ts +0 -20
  135. package/src/ui/branding/app-branding-provider.tsx +0 -123
  136. package/src/ui/branding/sanitize.ts +0 -89
  137. package/src/ui/breakpoints.ts +0 -14
  138. package/src/ui/capture/document-preview.tsx +0 -166
  139. package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
  140. package/src/ui/capture/field-spec-to-zod.ts +0 -174
  141. package/src/ui/capture/field-spec-validate.ts +0 -236
  142. package/src/ui/capture/field-spec.test.ts +0 -49
  143. package/src/ui/capture/field-spec.ts +0 -274
  144. package/src/ui/capture/index.ts +0 -51
  145. package/src/ui/capture/schema-field-input.tsx +0 -620
  146. package/src/ui/capture/schema-form.tsx +0 -149
  147. package/src/ui/capture/submit-values.test.ts +0 -70
  148. package/src/ui/capture/submit-values.ts +0 -63
  149. package/src/ui/capture/use-prefill.ts +0 -186
  150. package/src/ui/capture/use-schema-validation.ts +0 -145
  151. package/src/ui/capture/use-wizard.ts +0 -180
  152. package/src/ui/capture/value-format.ts +0 -32
  153. package/src/ui/capture/wizard.tsx +0 -105
  154. package/src/ui/charts/bar-chart.tsx +0 -139
  155. package/src/ui/charts/chart-frame.tsx +0 -125
  156. package/src/ui/charts/chart-types.ts +0 -52
  157. package/src/ui/charts/chart-utils.ts +0 -190
  158. package/src/ui/charts/charts.test.tsx +0 -204
  159. package/src/ui/charts/donut-chart.tsx +0 -129
  160. package/src/ui/charts/index.ts +0 -27
  161. package/src/ui/charts/line-chart.tsx +0 -202
  162. package/src/ui/charts/sparkline.tsx +0 -109
  163. package/src/ui/chrome/AsyncBoundary.tsx +0 -74
  164. package/src/ui/chrome/EmptyState.tsx +0 -257
  165. package/src/ui/chrome/ErrorCard.tsx +0 -63
  166. package/src/ui/chrome/LoadingState.tsx +0 -61
  167. package/src/ui/chrome/PageHeader.tsx +0 -137
  168. package/src/ui/chrome/RecordCardList.tsx +0 -98
  169. package/src/ui/chrome/StateCard.tsx +0 -150
  170. package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
  171. package/src/ui/chrome/page-shell.spec.tsx +0 -117
  172. package/src/ui/chrome/page-shell.tsx +0 -221
  173. package/src/ui/chrome/scope-badge.tsx +0 -40
  174. package/src/ui/chrome/status-badge.tsx +0 -79
  175. package/src/ui/cn.ts +0 -32
  176. package/src/ui/design-tokens.ts +0 -372
  177. package/src/ui/hooks/use-debounced-value.ts +0 -16
  178. package/src/ui/hooks/use-is-mobile.ts +0 -28
  179. package/src/ui/index.ts +0 -110
  180. package/src/ui/pdf/pdf-preview.tsx +0 -122
  181. package/src/ui/primitives/alert-dialog.tsx +0 -104
  182. package/src/ui/primitives/async-combobox.tsx +0 -182
  183. package/src/ui/primitives/badge-variants.ts +0 -21
  184. package/src/ui/primitives/badge.tsx +0 -25
  185. package/src/ui/primitives/button-variants.ts +0 -31
  186. package/src/ui/primitives/button.tsx +0 -29
  187. package/src/ui/primitives/card.tsx +0 -43
  188. package/src/ui/primitives/checkbox.tsx +0 -26
  189. package/src/ui/primitives/collapsible.tsx +0 -9
  190. package/src/ui/primitives/dialog.tsx +0 -103
  191. package/src/ui/primitives/dropdown-menu.tsx +0 -179
  192. package/src/ui/primitives/form-stepper.tsx +0 -94
  193. package/src/ui/primitives/form-stepper.types.ts +0 -15
  194. package/src/ui/primitives/input.tsx +0 -22
  195. package/src/ui/primitives/label.tsx +0 -17
  196. package/src/ui/primitives/overflow-tabs.tsx +0 -331
  197. package/src/ui/primitives/popover.tsx +0 -33
  198. package/src/ui/primitives/preview-device-frame.tsx +0 -213
  199. package/src/ui/primitives/radio-group.tsx +0 -36
  200. package/src/ui/primitives/resizable.tsx +0 -67
  201. package/src/ui/primitives/scroll-area.tsx +0 -38
  202. package/src/ui/primitives/select.tsx +0 -143
  203. package/src/ui/primitives/separator.tsx +0 -20
  204. package/src/ui/primitives/sheet.tsx +0 -107
  205. package/src/ui/primitives/skeleton.tsx +0 -99
  206. package/src/ui/primitives/switch.tsx +0 -27
  207. package/src/ui/primitives/table.tsx +0 -72
  208. package/src/ui/primitives/tabs.tsx +0 -53
  209. package/src/ui/primitives/tag-multi-select.tsx +0 -261
  210. package/src/ui/primitives/textarea.tsx +0 -21
  211. package/src/ui/primitives/tooltip.tsx +0 -30
  212. package/src/ui/primitives/url-preview-chrome.tsx +0 -103
  213. package/src/vista/GenericCatalogueSurface.tsx +0 -179
  214. package/src/vista/SurfaceFallbackCard.tsx +0 -109
  215. package/src/vista/VistaPanel.tsx +0 -163
  216. package/src/vista/form-payload.spec.ts +0 -139
  217. package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
  218. package/src/vista/html/CustomUiFrame.tsx +0 -206
  219. package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
  220. package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
  221. package/src/vista/html/custom-ui-frame-contract.ts +0 -88
  222. package/src/vista/html/custom-ui-intents.spec.ts +0 -135
  223. package/src/vista/html/custom-ui-intents.ts +0 -155
  224. package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
  225. package/src/vista/html/custom-ui-srcdoc.ts +0 -193
  226. package/src/vista/html/index.ts +0 -13
  227. package/src/vista/index.ts +0 -52
  228. package/src/vista/surface-catalogue.spec.ts +0 -140
  229. package/src/vista/surface-catalogue.ts +0 -90
  230. package/src/vista/surface-icons.ts +0 -57
  231. package/src/vista/surface-registry.ts +0 -56
  232. package/src/vista/surfaces/ChartSurface.tsx +0 -34
  233. package/src/vista/surfaces/FilesSurface.tsx +0 -202
  234. package/src/vista/surfaces/FormSurface.tsx +0 -380
  235. package/src/vista/surfaces/HtmlSurface.tsx +0 -43
  236. package/src/vista/surfaces/TableSurface.tsx +0 -100
  237. package/src/vista/surfaces/UrlSurface.tsx +0 -40
  238. package/src/vista/surfaces/blob-surfaces.tsx +0 -139
  239. package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
  240. package/src/vista/surfaces/elicitation-transport.ts +0 -367
  241. package/src/vista/surfaces/file-surface-utils.ts +0 -69
  242. package/src/vista/surfaces/index.ts +0 -24
  243. package/src/vista/surfaces/register-surfaces.tsx +0 -139
  244. package/src/vista/surfaces/text-surfaces.tsx +0 -132
  245. package/src/vista/use-vista-tabs.ts +0 -196
  246. package/src/vista/vista-code-renderer-context.ts +0 -16
  247. package/src/vista/vista-code-renderer.tsx +0 -35
  248. package/src/vista/vista-file-context.ts +0 -14
  249. package/src/vista/vista-file-source.tsx +0 -141
  250. package/src/vista/vista-form-context.ts +0 -20
  251. package/src/vista/vista-form-host.tsx +0 -48
  252. package/src/vista/vista-model.ts +0 -137
  253. package/src/vista/vista-open-context.tsx +0 -27
  254. package/src/vista/vista-open-state.ts +0 -11
  255. package/src/vista/vista-payloads.ts +0 -635
  256. package/src/vista/vista-tabs-manifest.ts +0 -138
  257. package/src/vista/vista-tools.ts +0 -232
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }