@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.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +6 -0
- package/dist/pageData/helpers.d.ts +16 -0
- package/dist/pageData/helpers.d.ts.map +1 -1
- package/dist/pageData/helpers.js +61 -1
- package/dist/pageData/helpers.js.map +1 -1
- package/dist/pageData.d.ts +1 -1
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +1 -1
- package/dist/pageData.js.map +1 -1
- package/dist/react/FormCollabBindingRegistry.d.ts +16 -0
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -1
- package/dist/react/FormCollabBindingRegistry.js.map +1 -1
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +8 -1
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/fields/BuilderInput.d.ts.map +1 -1
- package/dist/react/fields/BuilderInput.js +64 -40
- package/dist/react/fields/BuilderInput.js.map +1 -1
- package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
- package/dist/react/fields/RepeaterInput.js +78 -43
- package/dist/react/fields/RepeaterInput.js.map +1 -1
- package/dist/react/fields/repeaterReconcile.d.ts +66 -0
- package/dist/react/fields/repeaterReconcile.d.ts.map +1 -0
- package/dist/react/fields/repeaterReconcile.js +96 -0
- package/dist/react/fields/repeaterReconcile.js.map +1 -0
- package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -1
- package/dist/react/schemaRenderer/form/FormRenderer.js +10 -0
- package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/pageData/helpers.ts +55 -1
- package/src/pageData.test.ts +67 -0
- package/src/pageData.ts +1 -0
- package/src/react/FormCollabBindingRegistry.ts +17 -0
- package/src/react/FormStateContext.tsx +8 -1
- package/src/react/fields/BuilderInput.tsx +53 -29
- package/src/react/fields/RepeaterInput.tsx +66 -32
- package/src/react/fields/repeaterReconcile.test.ts +114 -0
- package/src/react/fields/repeaterReconcile.ts +104 -0
- 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 ?? '')
|