@eifi1/ui-kit 0.46.0 → 0.46.1

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 (75) hide show
  1. package/README.md +10 -9
  2. package/dist/admin/user-roster.d.ts +6 -2
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/components/data-table-row-actions.d.ts +22 -5
  6. package/dist/components/data-table-row-actions.js +14 -5
  7. package/dist/components/data-table-row-actions.js.map +1 -1
  8. package/dist/components/data-table.d.ts +16 -1
  9. package/dist/components/data-table.js +2 -0
  10. package/dist/components/data-table.js.map +1 -1
  11. package/dist/components/row-actions.d.ts +12 -5
  12. package/dist/components/row-actions.js +11 -4
  13. package/dist/components/row-actions.js.map +1 -1
  14. package/dist/components/translation-review.js +3 -1
  15. package/dist/components/translation-review.js.map +1 -1
  16. package/dist/data-table.d.ts +1 -0
  17. package/dist/data-table.js +2 -0
  18. package/dist/data-table.js.map +1 -1
  19. package/dist/eslint/index.js +1 -1
  20. package/dist/i18n/locales/en.js.map +2 -2
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +4 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/notifications/device-zone-report.d.ts +7 -0
  25. package/dist/notifications/device-zone-report.js +8 -1
  26. package/dist/notifications/device-zone-report.js.map +1 -1
  27. package/dist/notifications/feedback-notification-types.d.ts +14 -1
  28. package/dist/notifications/feedback-notification-types.js +5 -1
  29. package/dist/notifications/feedback-notification-types.js.map +1 -1
  30. package/dist/notifications/notification-bell.d.ts +1 -1
  31. package/dist/notifications/notification-bell.js.map +1 -1
  32. package/dist/notifications/notification-inbox.d.ts +12 -3
  33. package/dist/notifications/notification-inbox.js +2 -2
  34. package/dist/notifications/notification-inbox.js.map +1 -1
  35. package/dist/notifications/notifications-labels.d.ts +3 -1
  36. package/dist/notifications/notifications-labels.js.map +1 -1
  37. package/dist/notifications/time-zone-select.d.ts +5 -4
  38. package/dist/notifications/time-zone-select.js +2 -7
  39. package/dist/notifications/time-zone-select.js.map +1 -1
  40. package/dist/offline/local-first.d.ts +9 -0
  41. package/dist/offline/local-first.js +14 -0
  42. package/dist/offline/local-first.js.map +1 -1
  43. package/dist/offline.d.ts +1 -1
  44. package/dist/offline.js +2 -1
  45. package/dist/offline.js.map +1 -1
  46. package/dist/sync/db.d.ts +17 -0
  47. package/dist/sync/db.js +10 -1
  48. package/dist/sync/db.js.map +1 -1
  49. package/dist/sync/engine.d.ts +11 -9
  50. package/dist/sync/engine.js.map +1 -1
  51. package/dist/sync/hooks.d.ts +5 -0
  52. package/dist/sync/hooks.js.map +1 -1
  53. package/dist/sync.d.ts +2 -2
  54. package/dist/sync.js +5 -2
  55. package/dist/sync.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/admin/user-roster.tsx +12 -3
  58. package/src/components/data-table-row-actions.tsx +33 -9
  59. package/src/components/data-table.tsx +17 -0
  60. package/src/components/row-actions.tsx +33 -9
  61. package/src/components/translation-review.tsx +3 -1
  62. package/src/data-table.ts +3 -0
  63. package/src/index.ts +10 -3
  64. package/src/notifications/device-zone-report.ts +14 -1
  65. package/src/notifications/feedback-notification-types.ts +19 -1
  66. package/src/notifications/notification-bell.tsx +1 -1
  67. package/src/notifications/notification-inbox.tsx +15 -5
  68. package/src/notifications/notifications-labels.ts +3 -1
  69. package/src/notifications/time-zone-select.tsx +6 -10
  70. package/src/offline/local-first.ts +24 -0
  71. package/src/offline.ts +1 -1
  72. package/src/sync/db.ts +22 -0
  73. package/src/sync/engine.ts +11 -9
  74. package/src/sync/hooks.ts +5 -0
  75. package/src/sync.ts +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { httpStatusOf } from \"../auth/auth-errors\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. */\nexport interface SyncStep {\n name: string;\n run(db: SyncDb, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: SyncDb): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. `dropped`: `forget()` ran during it, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions {\n db: SyncDb;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(scope: string, entity: string, id: string, op: \"upsert\" | \"delete\", data?: Record<string, unknown>): Promise<void>;\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round (after the one under way). */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n | { code?: unknown; detail?: unknown }\n | undefined;\n const inner = body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine(options: SyncEngineOptions): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n\n let generation = 0;\n let state: SyncEngineState = { phase: \"idle\", online: browserOnline(), counts: ZERO, lastSyncedAt: null, nextRetryAt: null };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. */\n function takeQueue(scope: string, withBlocked: boolean): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, async () => {\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed: each change back to the status it had. */\n function restore(sent: OutboxChange[]) {\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. */\n async function apply(scope: string, answer: SyncResponseBody, sent: OutboxChange[], gen: number, lastPage: boolean) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n if (gen !== generation) throw new Dropped();\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused) await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({ ...row, entity: change.entity, id: change.id, pk: row?.pk ?? null, data: { ...row?.data, ...change.data } });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(scope: string, gen: number, signal: AbortSignal, withBlocked: boolean): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await restore(sent);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n if (httpStatusOf(err) === 403) return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n throw err;\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await restore(sent);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n const own = new AbortController();\n controller = own;\n set({ phase: \"syncing\" });\n try {\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, own.signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal: own.signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n if (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]));\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<string, unknown>;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop() {\n if (!started) return;\n started = false;\n // The round in flight writes nothing more: its request is aborted and its answer\n // dropped, the queue laid back; the next engine's round waits for it to settle.\n generation += 1;\n controller?.abort();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n },\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter((c) => !scope || c.scope === scope);\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n generation += 1;\n controller?.abort();\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n });\n stepCounts = {};\n set({ lastSyncedAt: null });\n await refreshCounts();\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor(options: SyncEngineOptions): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept"]}
1
+ {"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { httpStatusOf } from \"../auth/auth-errors\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type\n * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */\nexport interface SyncStep<D extends SyncDb = SyncDb> {\n name: string;\n run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: D): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. `dropped`: `forget()` ran during it, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions<D extends SyncDb = SyncDb> {\n /** The database; its type `D` is the one the `steps` get. */\n db: D;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep<D>[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(scope: string, entity: string, id: string, op: \"upsert\" | \"delete\", data?: Record<string, unknown>): Promise<void>;\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round (after the one under way). */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n | { code?: unknown; detail?: unknown }\n | undefined;\n const inner = body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n\n let generation = 0;\n let state: SyncEngineState = { phase: \"idle\", online: browserOnline(), counts: ZERO, lastSyncedAt: null, nextRetryAt: null };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. */\n function takeQueue(scope: string, withBlocked: boolean): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, async () => {\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed: each change back to the status it had. */\n function restore(sent: OutboxChange[]) {\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. */\n async function apply(scope: string, answer: SyncResponseBody, sent: OutboxChange[], gen: number, lastPage: boolean) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n if (gen !== generation) throw new Dropped();\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused) await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({ ...row, entity: change.entity, id: change.id, pk: row?.pk ?? null, data: { ...row?.data, ...change.data } });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(scope: string, gen: number, signal: AbortSignal, withBlocked: boolean): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await restore(sent);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n if (httpStatusOf(err) === 403) return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n throw err;\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await restore(sent);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n const own = new AbortController();\n controller = own;\n set({ phase: \"syncing\" });\n try {\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, own.signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal: own.signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n if (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]));\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<string, unknown>;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop() {\n if (!started) return;\n started = false;\n // The round in flight writes nothing more: its request is aborted and its answer\n // dropped, the queue laid back; the next engine's round waits for it to settle.\n generation += 1;\n controller?.abort();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n },\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter((c) => !scope || c.scope === scope);\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n generation += 1;\n controller?.abort();\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n });\n stepCounts = {};\n set({ lastSyncedAt: null });\n await refreshCounts();\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept"]}
@@ -13,6 +13,11 @@ export declare function useSyncWorker(engine: SyncEngine | null): void;
13
13
  * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`
14
14
  * + `rejected`, `lastSyncedAt` from `meta`. `onSync` is "Sync now". Without an engine, an
15
15
  * idle status that follows the browser's connection.
16
+ *
17
+ * With a `null` engine (signed out, or a computer marked shared) `status.online` follows
18
+ * `navigator.onLine` and its `online`/`offline` events, so an app may lean on
19
+ * `status.online` for its own offline lock (kastlan's offline write lock) whether or not an
20
+ * engine runs.
16
21
  */
17
22
  export declare function useSyncStatus(engine: SyncEngine | null): {
18
23
  status: SyncStatus;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sync/hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { liveQuery } from \"dexie\";\nimport type { SyncStatus } from \"../shell/sync-status-indicator\";\nimport type { SyncDb } from \"./db\";\nimport type { SyncEngine, SyncEngineState } from \"./engine\";\n\n/**\n * Keep `engine` running while it is mounted: started on mount, stopped on unmount or when\n * the engine changes (a company switch builds a new engine on its own database, D15).\n * `null` runs nothing — signed out, or a computer marked shared. Mount once, in the\n * signed-in layout (kastlan's `useHandoverSyncLoop`).\n */\nexport function useSyncWorker(engine: SyncEngine | null): void {\n useEffect(() => {\n if (!engine) return;\n engine.start();\n return () => engine.stop();\n }, [engine]);\n}\n\nconst IDLE: SyncEngineState = {\n phase: \"idle\",\n online: true,\n counts: { pending: 0, waiting: 0, blocked: 0, rejected: 0 },\n lastSyncedAt: null,\n nextRetryAt: null,\n};\nconst noEngine = () => () => undefined;\nconst idle = () => IDLE;\n\n/**\n * The engine's state as the kit's `SyncStatusIndicator` reads it (§2.3): `pending` = queued\n * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`\n * + `rejected`, `lastSyncedAt` from `meta`. `onSync` is \"Sync now\". Without an engine, an\n * idle status that follows the browser's connection.\n */\nexport function useSyncStatus(engine: SyncEngine | null): { status: SyncStatus; onSync: () => Promise<void> } {\n const state = useSyncExternalStore(\n engine ? engine.subscribe : noEngine,\n engine ? engine.getState : idle,\n engine ? engine.getState : idle,\n );\n const browserOnline = useSyncExternalStore(subscribeOnline, readOnline, () => true);\n const onSync = useCallback(async () => {\n await engine?.syncNow();\n }, [engine]);\n const status: SyncStatus = {\n phase: state.phase,\n online: engine ? state.online : browserOnline,\n pending: state.counts.pending,\n waitingForPlan: state.counts.waiting,\n blocked: state.counts.blocked + state.counts.rejected,\n lastSyncedAt: state.lastSyncedAt,\n };\n return { status, onSync };\n}\n\nfunction subscribeOnline(listener: () => void) {\n window.addEventListener(\"online\", listener);\n window.addEventListener(\"offline\", listener);\n return () => {\n window.removeEventListener(\"online\", listener);\n window.removeEventListener(\"offline\", listener);\n };\n}\nconst readOnline = () => navigator.onLine !== false;\n\n/**\n * A read of the copy that follows every change to it (Dexie's `liveQuery`; kastlan's\n * `useLive`). `deps` names what `query` reads; `query` itself is rebuilt every render.\n * `query` gets the app's own database type (`HandoverDb`). Without a database (not open\n * yet) it answers `isLoading: true`, so a page shows no \"empty\" before the copy opens.\n */\nexport function useSyncQuery<T, D extends SyncDb = SyncDb>(\n db: D | null,\n query: (db: D) => Promise<T>,\n deps: readonly unknown[],\n): { data: T | undefined; isLoading: boolean } {\n const [state, setState] = useState<{ data: T | undefined; isLoading: boolean }>({ data: undefined, isLoading: true });\n useEffect(() => {\n if (!db) return;\n const subscription = liveQuery(() => query(db)).subscribe({\n next: (data) => setState({ data, isLoading: false }),\n error: () => setState({ data: undefined, isLoading: false }),\n });\n return () => subscription.unsubscribe();\n // `query` is rebuilt every render; `deps` names what it reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, ...deps]);\n return db ? state : NO_COPY;\n}\n\nconst NO_COPY = { data: undefined, isLoading: true } as const;\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,UAAU,4BAA4B;AACvE,SAAS,iBAAiB;AAWnB,SAAS,cAAc,QAAiC;AAC7D,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,WAAO,MAAM;AACb,WAAO,MAAM,OAAO,KAAK;AAAA,EAC3B,GAAG,CAAC,MAAM,CAAC;AACb;AAEA,MAAM,OAAwB;AAAA,EAC5B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,EAAE;AAAA,EAC1D,cAAc;AAAA,EACd,aAAa;AACf;AACA,MAAM,WAAW,MAAM,MAAM;AAC7B,MAAM,OAAO,MAAM;AAQZ,SAAS,cAAc,QAAgF;AAC5G,QAAM,QAAQ;AAAA,IACZ,SAAS,OAAO,YAAY;AAAA,IAC5B,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,EAC7B;AACA,QAAM,gBAAgB,qBAAqB,iBAAiB,YAAY,MAAM,IAAI;AAClF,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,QAAQ,QAAQ;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,SAAqB;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,QAAQ,SAAS,MAAM,SAAS;AAAA,IAChC,SAAS,MAAM,OAAO;AAAA,IACtB,gBAAgB,MAAM,OAAO;AAAA,IAC7B,SAAS,MAAM,OAAO,UAAU,MAAM,OAAO;AAAA,IAC7C,cAAc,MAAM;AAAA,EACtB;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,SAAS,gBAAgB,UAAsB;AAC7C,SAAO,iBAAiB,UAAU,QAAQ;AAC1C,SAAO,iBAAiB,WAAW,QAAQ;AAC3C,SAAO,MAAM;AACX,WAAO,oBAAoB,UAAU,QAAQ;AAC7C,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AACF;AACA,MAAM,aAAa,MAAM,UAAU,WAAW;AAQvC,SAAS,aACd,IACA,OACA,MAC6C;AAC7C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsD,EAAE,MAAM,QAAW,WAAW,KAAK,CAAC;AACpH,YAAU,MAAM;AACd,QAAI,CAAC,GAAI;AACT,UAAM,eAAe,UAAU,MAAM,MAAM,EAAE,CAAC,EAAE,UAAU;AAAA,MACxD,MAAM,CAAC,SAAS,SAAS,EAAE,MAAM,WAAW,MAAM,CAAC;AAAA,MACnD,OAAO,MAAM,SAAS,EAAE,MAAM,QAAW,WAAW,MAAM,CAAC;AAAA,IAC7D,CAAC;AACD,WAAO,MAAM,aAAa,YAAY;AAAA,EAGxC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;AAChB,SAAO,KAAK,QAAQ;AACtB;AAEA,MAAM,UAAU,EAAE,MAAM,QAAW,WAAW,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../src/sync/hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { liveQuery } from \"dexie\";\nimport type { SyncStatus } from \"../shell/sync-status-indicator\";\nimport type { SyncDb } from \"./db\";\nimport type { SyncEngine, SyncEngineState } from \"./engine\";\n\n/**\n * Keep `engine` running while it is mounted: started on mount, stopped on unmount or when\n * the engine changes (a company switch builds a new engine on its own database, D15).\n * `null` runs nothing — signed out, or a computer marked shared. Mount once, in the\n * signed-in layout (kastlan's `useHandoverSyncLoop`).\n */\nexport function useSyncWorker(engine: SyncEngine | null): void {\n useEffect(() => {\n if (!engine) return;\n engine.start();\n return () => engine.stop();\n }, [engine]);\n}\n\nconst IDLE: SyncEngineState = {\n phase: \"idle\",\n online: true,\n counts: { pending: 0, waiting: 0, blocked: 0, rejected: 0 },\n lastSyncedAt: null,\n nextRetryAt: null,\n};\nconst noEngine = () => () => undefined;\nconst idle = () => IDLE;\n\n/**\n * The engine's state as the kit's `SyncStatusIndicator` reads it (§2.3): `pending` = queued\n * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`\n * + `rejected`, `lastSyncedAt` from `meta`. `onSync` is \"Sync now\". Without an engine, an\n * idle status that follows the browser's connection.\n *\n * With a `null` engine (signed out, or a computer marked shared) `status.online` follows\n * `navigator.onLine` and its `online`/`offline` events, so an app may lean on\n * `status.online` for its own offline lock (kastlan's offline write lock) whether or not an\n * engine runs.\n */\nexport function useSyncStatus(engine: SyncEngine | null): { status: SyncStatus; onSync: () => Promise<void> } {\n const state = useSyncExternalStore(\n engine ? engine.subscribe : noEngine,\n engine ? engine.getState : idle,\n engine ? engine.getState : idle,\n );\n const browserOnline = useSyncExternalStore(subscribeOnline, readOnline, () => true);\n const onSync = useCallback(async () => {\n await engine?.syncNow();\n }, [engine]);\n const status: SyncStatus = {\n phase: state.phase,\n online: engine ? state.online : browserOnline,\n pending: state.counts.pending,\n waitingForPlan: state.counts.waiting,\n blocked: state.counts.blocked + state.counts.rejected,\n lastSyncedAt: state.lastSyncedAt,\n };\n return { status, onSync };\n}\n\nfunction subscribeOnline(listener: () => void) {\n window.addEventListener(\"online\", listener);\n window.addEventListener(\"offline\", listener);\n return () => {\n window.removeEventListener(\"online\", listener);\n window.removeEventListener(\"offline\", listener);\n };\n}\nconst readOnline = () => navigator.onLine !== false;\n\n/**\n * A read of the copy that follows every change to it (Dexie's `liveQuery`; kastlan's\n * `useLive`). `deps` names what `query` reads; `query` itself is rebuilt every render.\n * `query` gets the app's own database type (`HandoverDb`). Without a database (not open\n * yet) it answers `isLoading: true`, so a page shows no \"empty\" before the copy opens.\n */\nexport function useSyncQuery<T, D extends SyncDb = SyncDb>(\n db: D | null,\n query: (db: D) => Promise<T>,\n deps: readonly unknown[],\n): { data: T | undefined; isLoading: boolean } {\n const [state, setState] = useState<{ data: T | undefined; isLoading: boolean }>({ data: undefined, isLoading: true });\n useEffect(() => {\n if (!db) return;\n const subscription = liveQuery(() => query(db)).subscribe({\n next: (data) => setState({ data, isLoading: false }),\n error: () => setState({ data: undefined, isLoading: false }),\n });\n return () => subscription.unsubscribe();\n // `query` is rebuilt every render; `deps` names what it reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, ...deps]);\n return db ? state : NO_COPY;\n}\n\nconst NO_COPY = { data: undefined, isLoading: true } as const;\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,UAAU,4BAA4B;AACvE,SAAS,iBAAiB;AAWnB,SAAS,cAAc,QAAiC;AAC7D,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,WAAO,MAAM;AACb,WAAO,MAAM,OAAO,KAAK;AAAA,EAC3B,GAAG,CAAC,MAAM,CAAC;AACb;AAEA,MAAM,OAAwB;AAAA,EAC5B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,EAAE;AAAA,EAC1D,cAAc;AAAA,EACd,aAAa;AACf;AACA,MAAM,WAAW,MAAM,MAAM;AAC7B,MAAM,OAAO,MAAM;AAaZ,SAAS,cAAc,QAAgF;AAC5G,QAAM,QAAQ;AAAA,IACZ,SAAS,OAAO,YAAY;AAAA,IAC5B,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,EAC7B;AACA,QAAM,gBAAgB,qBAAqB,iBAAiB,YAAY,MAAM,IAAI;AAClF,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,QAAQ,QAAQ;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,SAAqB;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,QAAQ,SAAS,MAAM,SAAS;AAAA,IAChC,SAAS,MAAM,OAAO;AAAA,IACtB,gBAAgB,MAAM,OAAO;AAAA,IAC7B,SAAS,MAAM,OAAO,UAAU,MAAM,OAAO;AAAA,IAC7C,cAAc,MAAM;AAAA,EACtB;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,SAAS,gBAAgB,UAAsB;AAC7C,SAAO,iBAAiB,UAAU,QAAQ;AAC1C,SAAO,iBAAiB,WAAW,QAAQ;AAC3C,SAAO,MAAM;AACX,WAAO,oBAAoB,UAAU,QAAQ;AAC7C,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AACF;AACA,MAAM,aAAa,MAAM,UAAU,WAAW;AAQvC,SAAS,aACd,IACA,OACA,MAC6C;AAC7C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsD,EAAE,MAAM,QAAW,WAAW,KAAK,CAAC;AACpH,YAAU,MAAM;AACd,QAAI,CAAC,GAAI;AACT,UAAM,eAAe,UAAU,MAAM,MAAM,EAAE,CAAC,EAAE,UAAU;AAAA,MACxD,MAAM,CAAC,SAAS,SAAS,EAAE,MAAM,WAAW,MAAM,CAAC;AAAA,MACnD,OAAO,MAAM,SAAS,EAAE,MAAM,QAAW,WAAW,MAAM,CAAC;AAAA,IAC7D,CAAC;AACD,WAAO,MAAM,aAAa,YAAY;AAAA,EAGxC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;AAChB,SAAO,KAAK,QAAQ;AACtB;AAEA,MAAM,UAAU,EAAE,MAAM,QAAW,WAAW,KAAK;","names":[]}
package/dist/sync.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { createSyncDb, SYNC_DB_STORES } from "./sync/db.js";
1
+ export { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from "./sync/db.js";
2
2
  export type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry } from "./sync/db.js";
3
3
  export { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor } from "./sync/engine.js";
4
4
  export type { SyncCounts, SyncEngine, SyncEngineOptions, SyncEngineState, SyncRoundResult, SyncStep, SyncStepContext, SyncStepResult, } from "./sync/engine.js";
5
5
  export { useSyncQuery, useSyncStatus, useSyncWorker } from "./sync/hooks.js";
6
6
  export type { SyncPageCursor, SyncRefusalBody, SyncRejectionBody, SyncRequestBody, SyncResponseBody, SyncWireChange, } from "./sync/wire.js";
7
- export { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
7
+ export { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from "./offline/local-first.js";
package/dist/sync.js CHANGED
@@ -1,22 +1,25 @@
1
1
  "use client";
2
- import { createSyncDb, SYNC_DB_STORES } from "./sync/db.js";
2
+ import { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from "./sync/db.js";
3
3
  import { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor } from "./sync/engine.js";
4
4
  import { useSyncQuery, useSyncStatus, useSyncWorker } from "./sync/hooks.js";
5
- import { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first.js";
5
+ import { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from "./offline/local-first.js";
6
6
  export {
7
7
  DEFAULT_HOLD_CODES,
8
8
  DEFAULT_SYNC_BACKOFF_MS,
9
+ SYNC_DB_PK_INDEX,
9
10
  SYNC_DB_STORES,
10
11
  createSyncDb,
11
12
  createSyncEngine,
12
13
  isOfflineError,
13
14
  readLocalFirst,
14
15
  releaseSyncEngine,
16
+ rowByPk,
15
17
  stampAfter,
16
18
  syncEngineFor,
17
19
  useSyncQuery,
18
20
  useSyncStatus,
19
21
  useSyncWorker,
22
+ uuid5,
20
23
  writeLocalFirst
21
24
  };
22
25
  //# sourceMappingURL=sync.js.map
package/dist/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sync.ts"],"sourcesContent":["// `@eifi1/ui-kit/sync` — local-first phase 3 (docs/local-first-harmonization.md §2.3, 0.46):\n// the sync engine on a Dexie copy, its worker and status hooks, and the live read of the copy.\n//\n// The only entry point that imports `dexie` — an optional peer for that reason. It never loads\n// `/offline`'s TanStack and idb-keyval peers: the leaf utilities both entries export live in\n// one module of their own (src/offline/local-first.ts). NOT re-exported from the main barrel.\nexport { createSyncDb, SYNC_DB_STORES } from \"./sync/db\";\nexport type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry } from \"./sync/db\";\nexport { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor } from \"./sync/engine\";\nexport type {\n SyncCounts,\n SyncEngine,\n SyncEngineOptions,\n SyncEngineState,\n SyncRoundResult,\n SyncStep,\n SyncStepContext,\n SyncStepResult,\n} from \"./sync/engine\";\nexport { useSyncQuery, useSyncStatus, useSyncWorker } from \"./sync/hooks\";\nexport type {\n SyncPageCursor,\n SyncRefusalBody,\n SyncRejectionBody,\n SyncRequestBody,\n SyncResponseBody,\n SyncWireChange,\n} from \"./sync/wire\";\nexport { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from \"./offline/local-first\";\n"],"mappings":";AAMA,SAAS,cAAc,sBAAsB;AAE7C,SAAS,kBAAkB,oBAAoB,yBAAyB,mBAAmB,qBAAqB;AAWhH,SAAS,cAAc,eAAe,qBAAqB;AAS3D,SAAS,gBAAgB,gBAAgB,YAAY,uBAAuB;","names":[]}
1
+ {"version":3,"sources":["../src/sync.ts"],"sourcesContent":["// `@eifi1/ui-kit/sync` — local-first phase 3 (docs/local-first-harmonization.md §2.3, 0.46):\n// the sync engine on a Dexie copy, its worker and status hooks, and the live read of the copy.\n//\n// The only entry point that imports `dexie` — an optional peer for that reason. It never loads\n// `/offline`'s TanStack and idb-keyval peers: the leaf utilities both entries export live in\n// one module of their own (src/offline/local-first.ts). NOT re-exported from the main barrel.\nexport { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from \"./sync/db\";\nexport type { CreateSyncDbOptions, OutboxChange, OutboxStatus, SyncDb, SyncDbUpgrade, SyncedRow, SyncMetaEntry } from \"./sync/db\";\nexport { createSyncEngine, DEFAULT_HOLD_CODES, DEFAULT_SYNC_BACKOFF_MS, releaseSyncEngine, syncEngineFor } from \"./sync/engine\";\nexport type {\n SyncCounts,\n SyncEngine,\n SyncEngineOptions,\n SyncEngineState,\n SyncRoundResult,\n SyncStep,\n SyncStepContext,\n SyncStepResult,\n} from \"./sync/engine\";\nexport { useSyncQuery, useSyncStatus, useSyncWorker } from \"./sync/hooks\";\nexport type {\n SyncPageCursor,\n SyncRefusalBody,\n SyncRejectionBody,\n SyncRequestBody,\n SyncResponseBody,\n SyncWireChange,\n} from \"./sync/wire\";\nexport { isOfflineError, readLocalFirst, stampAfter, uuid5, writeLocalFirst } from \"./offline/local-first\";\n"],"mappings":";AAMA,SAAS,cAAc,SAAS,kBAAkB,sBAAsB;AAExE,SAAS,kBAAkB,oBAAoB,yBAAyB,mBAAmB,qBAAqB;AAWhH,SAAS,cAAc,eAAe,qBAAqB;AAS3D,SAAS,gBAAgB,gBAAgB,YAAY,OAAO,uBAAuB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -266,6 +266,10 @@ export interface UserRosterColumnsOptions<T extends UserRosterRow, R extends str
266
266
  extra?: readonly DataTableColumn<T>[];
267
267
  /** The row's action menu (§4). Left out: no actions column. */
268
268
  actions?: (row: T) => UserRowActionList;
269
+ /** The actions column's heading (0.46.1, keksdose dev#598 rework — a VISIBLE "Actions"
270
+ * on every table with an actions column). Default: `labels.actions`, shown. A node
271
+ * replaces it; `false` keeps the pre-0.46.1 heading, a screen reader's name only. */
272
+ actionsHeader?: ReactNode | false;
269
273
  /**
270
274
  * Whether this row's role is a select rather than a chip, and with which reasons.
271
275
  * Left out, or `false`: a chip. kastlan's several roles are never a select here — they
@@ -344,6 +348,7 @@ export function userRosterColumns<T extends UserRosterRow, R extends string = st
344
348
  stateOf = adminUserStates,
345
349
  extra = [],
346
350
  actions,
351
+ actionsHeader,
347
352
  editableRole,
348
353
  onRoleChange,
349
354
  roleOf,
@@ -459,7 +464,9 @@ export function userRosterColumns<T extends UserRosterRow, R extends string = st
459
464
  if (actions) {
460
465
  columns.push({
461
466
  key: "actions",
462
- header: <span className="sr-only">{labels.actions}</span>,
467
+ // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).
468
+ header:
469
+ actionsHeader === false ? <span className="sr-only">{labels.actions}</span> : (actionsHeader ?? labels.actions),
463
470
  headerText: labels.actions,
464
471
  // The caller's own words only: the menu reads the provider's itself, and the
465
472
  // English defaults merged in above must not shadow them.
@@ -469,7 +476,7 @@ export function userRosterColumns<T extends UserRosterRow, R extends string = st
469
476
  </RowName>
470
477
  ),
471
478
  className: "w-px whitespace-nowrap text-end",
472
- headClassName: "w-px",
479
+ headClassName: "w-px text-end",
473
480
  // The menu button keeps its own click: a row link must not wrap it.
474
481
  noRowLink: true,
475
482
  });
@@ -487,6 +494,7 @@ export function useUserRosterColumns<T extends UserRosterRow, R extends string =
487
494
  stateOf,
488
495
  extra,
489
496
  actions,
497
+ actionsHeader,
490
498
  editableRole,
491
499
  onRoleChange,
492
500
  roleOf,
@@ -501,12 +509,13 @@ export function useUserRosterColumns<T extends UserRosterRow, R extends string =
501
509
  stateOf,
502
510
  extra,
503
511
  actions,
512
+ actionsHeader,
504
513
  editableRole,
505
514
  onRoleChange,
506
515
  roleOf,
507
516
  emailColumn,
508
517
  labels,
509
518
  }),
510
- [roles, stateOf, extra, actions, editableRole, onRoleChange, roleOf, emailColumn, labels],
519
+ [roles, stateOf, extra, actions, actionsHeader, editableRole, onRoleChange, roleOf, emailColumn, labels],
511
520
  );
512
521
  }
@@ -31,6 +31,16 @@ export function isTableOwnColumn(key: string): boolean {
31
31
  return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;
32
32
  }
33
33
 
34
+ /**
35
+ * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE
36
+ * "Actions" heading on every table with an actions column): the caller's node, else the
37
+ * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.
38
+ */
39
+ export function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode {
40
+ if (header === false) return <span className="sr-only">{word}</span>;
41
+ return header ?? word;
42
+ }
43
+
34
44
  /** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN
35
45
  * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays
36
46
  * on the curtain and reads muted. */
@@ -46,10 +56,19 @@ function swipeTone(action: SwipeAction): RowActionTone {
46
56
  }
47
57
 
48
58
  /**
49
- * A row's swipe plan as desktop row actions (dev#598): `end` then `start` — the physical
50
- * `right` / `left` only where the logical side is missing, as the card reads them — each
51
- * action once, told apart by its `id`, else by its label (two stages with one label would
52
- * be one button twice).
59
+ * A row's swipe plan as desktop row actions (dev#598) — what DataTable's
60
+ * `desktopSwipeActions` column draws, for an app that builds its own actions column (or
61
+ * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two
62
+ * cannot drift (public since 0.46.1, keksdose N3).
63
+ *
64
+ * `end` then `start` — the physical `right` / `left` only where the logical side is
65
+ * missing, as the card reads them — each action once, told apart by its `id`, else by its
66
+ * label (two stages with one label would be one button twice). Each button runs the
67
+ * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any
68
+ * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a
69
+ * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.
70
+ *
71
+ * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />
53
72
  */
54
73
  export function swipePlanRowActions<T>(
55
74
  plan: MobileSwipeActions | null | undefined,
@@ -139,6 +158,7 @@ function toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {
139
158
  export function useRowActionColumn<T>({
140
159
  columns: columnsProp,
141
160
  rowActions,
161
+ rowActionsHeader,
142
162
  rowName,
143
163
  labels,
144
164
  density,
@@ -147,6 +167,7 @@ export function useRowActionColumn<T>({
147
167
  }: {
148
168
  columns: DataTableColumn<T>[];
149
169
  rowActions: DataTableProps<T>["rowActions"];
170
+ rowActionsHeader?: DataTableProps<T>["rowActionsHeader"];
150
171
  rowName: DataTableProps<T>["rowName"];
151
172
  labels: DataTableLabels;
152
173
  density: DataTableDensity;
@@ -175,12 +196,13 @@ export function useRowActionColumn<T>({
175
196
  const swipeName = swipeOptions?.name;
176
197
  const swipeCollapse = swipeOptions?.collapse;
177
198
  const swipeCommit = swipeOptions?.commit;
199
+ const swipeHeader = swipeOptions?.header;
178
200
  const swipeColumn = useMemo<DataTableColumn<T> | null>(
179
201
  () =>
180
202
  swipeColumnOn
181
203
  ? {
182
204
  key: SWIPE_ACTIONS_KEY,
183
- header: <span className="sr-only">{labels.actions}</span>,
205
+ header: actionsHeading(swipeHeader, labels.actions),
184
206
  cell: (row) => (
185
207
  <RowActions
186
208
  actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}
@@ -194,7 +216,8 @@ export function useRowActionColumn<T>({
194
216
  />
195
217
  ),
196
218
  className: "w-px whitespace-nowrap text-end",
197
- headClassName: "w-px",
219
+ // The heading over the buttons, at the row's end (0.46.1).
220
+ headClassName: "w-px text-end",
198
221
  // On a phone the same actions are the card's swipes.
199
222
  mobileHidden: true,
200
223
  noRowLink: true,
@@ -208,6 +231,7 @@ export function useRowActionColumn<T>({
208
231
  swipeName,
209
232
  rowName,
210
233
  swipeCollapse,
234
+ swipeHeader,
211
235
  density,
212
236
  labels.actions,
213
237
  ],
@@ -219,7 +243,7 @@ export function useRowActionColumn<T>({
219
243
  ...columnsProp,
220
244
  {
221
245
  key: ROW_ACTIONS_KEY,
222
- header: <span className="sr-only">{labels.actions}</span>,
246
+ header: actionsHeading(rowActionsHeader, labels.actions),
223
247
  // Through RowActions (0.32, §10.8): the icons inline at Normal, one "⋯"
224
248
  // menu at Large once there are two — every label at once does not fit a
225
249
  // row. Each control stops its own click: the row's handler already skips
@@ -235,14 +259,14 @@ export function useRowActionColumn<T>({
235
259
  />
236
260
  ),
237
261
  className: "w-px whitespace-nowrap text-end",
238
- headClassName: "w-px",
262
+ headClassName: "w-px text-end",
239
263
  // On a phone the same actions are the card's swipes.
240
264
  mobileHidden: true,
241
265
  noRowLink: true,
242
266
  },
243
267
  ]
244
268
  : columnsProp,
245
- [columnsProp, rowActions, rowActionsFor, rowName, labels.actions, density],
269
+ [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density],
246
270
  );
247
271
  const columns = useMemo(
248
272
  () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),
@@ -609,6 +609,14 @@ export interface DataTableProps<T> {
609
609
  * `mobileSwipeActions` wins over the swipes derived from here.
610
610
  */
611
611
  rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
612
+ /**
613
+ * The `rowActions` column's heading (0.46.1, keksdose dev#598 rework — Marcel: a
614
+ * VISIBLE "Actions" heading on every table with an actions column). Default: the
615
+ * provider's `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A
616
+ * node replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader
617
+ * only.
618
+ */
619
+ rowActionsHeader?: ReactNode | false;
612
620
  /**
613
621
  * The bar above the table — a search field, filter chips, a create button. Inside
614
622
  * the table's frame, above the header, on desktop and phone alike (resource-list
@@ -703,6 +711,13 @@ export interface DataTableDesktopSwipeActions<T> {
703
711
  * no commit scope (the plan decides).
704
712
  */
705
713
  commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);
714
+ /**
715
+ * The column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE "Actions"
716
+ * heading on every table with an actions column). Default: the provider's
717
+ * `dataTable.actions` ("Actions"), shown, end-aligned over the buttons. A node
718
+ * replaces it; `false` keeps the 0.46.0 heading, named for a screen reader only.
719
+ */
720
+ header?: ReactNode | false;
706
721
  }
707
722
 
708
723
  /**
@@ -828,6 +843,7 @@ export function DataTable<T>({
828
843
  edgeFade = false,
829
844
  className,
830
845
  rowActions,
846
+ rowActionsHeader,
831
847
  toolbar,
832
848
  footerLabel,
833
849
  footer: footerProp,
@@ -860,6 +876,7 @@ export function DataTable<T>({
860
876
  const { columns, swipesFromActions } = useRowActionColumn({
861
877
  columns: columnsProp,
862
878
  rowActions,
879
+ rowActionsHeader,
863
880
  rowName,
864
881
  labels,
865
882
  density,