@pilotiq/pilotiq 0.13.0 → 0.13.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 (40) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +6 -0
  3. package/dist/pageData/helpers.d.ts +16 -0
  4. package/dist/pageData/helpers.d.ts.map +1 -1
  5. package/dist/pageData/helpers.js +61 -1
  6. package/dist/pageData/helpers.js.map +1 -1
  7. package/dist/pageData.d.ts +1 -1
  8. package/dist/pageData.d.ts.map +1 -1
  9. package/dist/pageData.js +1 -1
  10. package/dist/pageData.js.map +1 -1
  11. package/dist/react/FormCollabBindingRegistry.d.ts +16 -0
  12. package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
  13. package/dist/react/FormCollabBindingRegistry.js.map +1 -1
  14. package/dist/react/FormStateContext.d.ts.map +1 -1
  15. package/dist/react/FormStateContext.js +8 -1
  16. package/dist/react/FormStateContext.js.map +1 -1
  17. package/dist/react/fields/BuilderInput.d.ts.map +1 -1
  18. package/dist/react/fields/BuilderInput.js +64 -40
  19. package/dist/react/fields/BuilderInput.js.map +1 -1
  20. package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
  21. package/dist/react/fields/RepeaterInput.js +78 -43
  22. package/dist/react/fields/RepeaterInput.js.map +1 -1
  23. package/dist/react/fields/repeaterReconcile.d.ts +66 -0
  24. package/dist/react/fields/repeaterReconcile.d.ts.map +1 -0
  25. package/dist/react/fields/repeaterReconcile.js +96 -0
  26. package/dist/react/fields/repeaterReconcile.js.map +1 -0
  27. package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -1
  28. package/dist/react/schemaRenderer/form/FormRenderer.js +10 -0
  29. package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/pageData/helpers.ts +55 -1
  32. package/src/pageData.test.ts +67 -0
  33. package/src/pageData.ts +1 -0
  34. package/src/react/FormCollabBindingRegistry.ts +17 -0
  35. package/src/react/FormStateContext.tsx +8 -1
  36. package/src/react/fields/BuilderInput.tsx +53 -29
  37. package/src/react/fields/RepeaterInput.tsx +66 -32
  38. package/src/react/fields/repeaterReconcile.test.ts +114 -0
  39. package/src/react/fields/repeaterReconcile.ts +104 -0
  40. package/src/react/schemaRenderer/form/FormRenderer.tsx +10 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Phase A of the `Repeater.relationship` PK-switch reconciliation
3
+ * (see `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`).
4
+ *
5
+ * When a parent form submit creates new relationship-backed rows, the
6
+ * server assigns each child a DB primary key — but the row's `__id` in
7
+ * the row CRDT is still the renderer-minted UUID from the local session.
8
+ * After redirect, the submitting tab's pageData carries `__id = String(pk)`
9
+ * while CRDT still has the UUID, so the renderer ends up showing the
10
+ * same row twice (DB PK from initialRows + orphan UUID from CRDT).
11
+ *
12
+ * Phase A fix: the submitting tab marks itself for a one-shot CRDT
13
+ * reconcile on the next mount via a per-formId sessionStorage flag.
14
+ * `RepeaterInput` / `BuilderInput` read the flag on mount and, when set,
15
+ * snapshot the row CRDT after a short settle (waiting for WS sync) and
16
+ * reconcile against `initialRows` — removing orphan CRDT rows not in
17
+ * the form's authoritative data, and adding missing CRDT rows (rare,
18
+ * happens when the row was DB-seeded outside the collab session).
19
+ *
20
+ * The flag is scoped per-tab via sessionStorage, so other peers' tabs
21
+ * never run the reconciler — preserving their in-flight edits.
22
+ *
23
+ * Phase B (server-side rename via the @rudderjs/sync Y.Doc seam) will
24
+ * extend this to other peers without requiring them to reload.
25
+ */
26
+
27
+ const STORAGE_PREFIX = 'pilotiq.repeaterReconcile.'
28
+
29
+ function storageKey(formId: string): string {
30
+ return STORAGE_PREFIX + formId
31
+ }
32
+
33
+ /**
34
+ * Called by `FormRenderer` on submit success. Records that this tab
35
+ * has just persisted the form, so the next mount of any Repeater /
36
+ * Builder under the same form runs the PK-switch reconciler. No-op
37
+ * when `formId` is empty or `sessionStorage` is unavailable (SSR).
38
+ */
39
+ export function markSubmitForReconcile(formId: string): void {
40
+ if (!formId) return
41
+ if (typeof sessionStorage === 'undefined') return
42
+ try {
43
+ sessionStorage.setItem(storageKey(formId), '1')
44
+ } catch {
45
+ // Quota exceeded / disabled — silently skip. Reconciliation is
46
+ // an optimization, not a correctness requirement.
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Called by `RepeaterInput` / `BuilderInput` on mount. Returns `true`
52
+ * iff the form was just submitted in this tab AND clears the flag so
53
+ * subsequent mounts no-op. Idempotent across multiple Repeater/Builder
54
+ * fields on the same form — the FIRST reader clears the flag, so
55
+ * siblings see `false`. To avoid that, both fields call this helper at
56
+ * the same mount tick — for v1 we accept the limitation: only the first
57
+ * Repeater on the form runs the reconciler; siblings don't.
58
+ *
59
+ * If a future need surfaces (multiple relationship-backed Repeaters on
60
+ * the same form), switch to a per-field flag keyed by `formId.fieldName`
61
+ * or have the FormRenderer dispatch a custom event instead.
62
+ */
63
+ export function consumeReconcileFlag(formId: string): boolean {
64
+ if (!formId) return false
65
+ if (typeof sessionStorage === 'undefined') return false
66
+ try {
67
+ const v = sessionStorage.getItem(storageKey(formId))
68
+ if (v !== '1') return false
69
+ sessionStorage.removeItem(storageKey(formId))
70
+ return true
71
+ } catch {
72
+ return false
73
+ }
74
+ }
75
+
76
+ export interface ReconcileInputs {
77
+ /** Current CRDT row id order (post-WS-sync). */
78
+ current: readonly string[]
79
+ /** Authoritative row id list from server-rendered initialRows. */
80
+ authoritative: readonly string[]
81
+ }
82
+
83
+ export interface ReconcilePlan {
84
+ /** Row ids present in CRDT but not in initialRows — orphan UUIDs. */
85
+ toRemove: string[]
86
+ /** Row ids present in initialRows but not in CRDT — DB rows not yet
87
+ * in the room (rare; only happens when DB was seeded outside collab). */
88
+ toAdd: string[]
89
+ }
90
+
91
+ /**
92
+ * Pure helper: compute the symmetric difference. Exported separately so
93
+ * unit tests don't need a DOM / sessionStorage shim to verify the
94
+ * reconciliation arithmetic.
95
+ */
96
+ export function computeReconcilePlan({ current, authoritative }: ReconcileInputs): ReconcilePlan {
97
+ const currentSet = new Set(current)
98
+ const authSet = new Set(authoritative)
99
+ const toRemove: string[] = []
100
+ const toAdd: string[] = []
101
+ for (const id of current) if (!authSet.has(id)) toRemove.push(id)
102
+ for (const id of authoritative) if (!currentSet.has(id)) toAdd.push(id)
103
+ return { toRemove, toAdd }
104
+ }
@@ -6,6 +6,7 @@ import { useCollabRoom } from '../../CollabRoomContext.js'
6
6
  import { getFormCollabBinding } from '../../FormCollabBindingRegistry.js'
7
7
  import { useNavigate } from '../../navigate.js'
8
8
  import { useToast } from '../../Toaster.js'
9
+ import { markSubmitForReconcile } from '../../fields/repeaterReconcile.js'
9
10
  import { renderField } from './renderField.js'
10
11
 
11
12
  // ─── Form ───────────────────────────────────────────────────
@@ -111,6 +112,15 @@ export function FormRenderer({
111
112
  }
112
113
 
113
114
  // Success — drain notifications and SPA-navigate to the redirect.
115
+ //
116
+ // Before navigating, mark this tab for the relationship-backed
117
+ // Repeater/Builder PK-switch reconciler. The next mount of any
118
+ // child Repeater/Builder under this formId will run a one-shot
119
+ // CRDT reconcile to drop orphan UUIDs whose rows just persisted
120
+ // under a fresh DB PK. See
121
+ // `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`
122
+ // (Phase A).
123
+ markSubmitForReconcile(formId)
114
124
  const notifs = (data as { notifications?: NotificationMeta[] }).notifications
115
125
  if (notifs && notifs.length > 0) for (const n of notifs) notify(n)
116
126
  const redirect = String((data as { redirect?: string }).redirect ?? '')