@memberjunction/react-runtime 5.40.2 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @fileoverview Helpers for the per-user interactive-component settings contract
3
+ * (`savedUserSettings` in / `onSaveUserSettings` out).
4
+ *
5
+ * These helpers are intentionally **pure and framework-agnostic** so the Angular
6
+ * host bridge and the Node test harness share one implementation and the scoping /
7
+ * serialization logic can be unit tested in isolation. The host wires these to a
8
+ * durable per-user store (e.g. `UserInfoEngine`); the runtime itself never persists.
9
+ *
10
+ * @module @memberjunction/react-runtime
11
+ */
12
+
13
+ /**
14
+ * Prefix applied to every interactive-component user-settings storage key. The
15
+ * full key is `InteractiveComponents_UserState_Root/<scope>`, where `<scope>` is
16
+ * resolved by {@link resolveUserStateScope}. It is deliberately long and unique
17
+ * to avoid collisions with other namespaced keys in the shared
18
+ * `MJ: User Settings` keyspace. Keeping the prefix in one place also avoids
19
+ * stringly-typed drift between the seed (read) and persist (write) paths.
20
+ */
21
+ export const USER_STATE_KEY_PREFIX = 'InteractiveComponents_UserState_Root/';
22
+
23
+ /**
24
+ * Resolve the stable per-component scope used to namespace a component's
25
+ * persisted user settings.
26
+ *
27
+ * An explicit, host-supplied scope always wins (used when a host needs to
28
+ * differentiate two instances of the same component spec — e.g. scope a form's
29
+ * settings by the entity it edits). Otherwise the scope is derived from the
30
+ * component spec's `namespace` + `name`.
31
+ *
32
+ * The result is lowercased to avoid case-variant duplicate rows in the settings
33
+ * store (per MJ's user-settings key convention).
34
+ *
35
+ * @returns the resolved scope, or `null` when no stable scope can be derived
36
+ * (e.g. an unnamed component) — signaling the caller to skip persistence.
37
+ */
38
+ export function resolveUserStateScope(
39
+ explicitScope: string | undefined | null,
40
+ namespace: string | undefined | null,
41
+ name: string | undefined | null
42
+ ): string | null {
43
+ const explicit = explicitScope?.trim();
44
+ if (explicit) {
45
+ return explicit.toLowerCase();
46
+ }
47
+ const cleanName = name?.trim();
48
+ if (!cleanName) {
49
+ return null;
50
+ }
51
+ const cleanNamespace = namespace?.trim();
52
+ const scope = cleanNamespace ? `${cleanNamespace}/${cleanName}` : cleanName;
53
+ return scope.toLowerCase();
54
+ }
55
+
56
+ /**
57
+ * Build the full storage key for a resolved scope, or `null` when the scope is
58
+ * `null` (persistence should be skipped).
59
+ */
60
+ export function userStateStorageKey(scope: string | null): string | null {
61
+ return scope ? `${USER_STATE_KEY_PREFIX}${scope}` : null;
62
+ }
63
+
64
+ /**
65
+ * Safely parse a stored settings blob into a plain object. Returns `{}` for
66
+ * null/empty input, invalid JSON, or any non-object JSON (arrays, primitives) —
67
+ * persisted user settings are always a flat key/value object.
68
+ */
69
+ export function parseStoredUserSettings(raw: string | undefined | null): Record<string, unknown> {
70
+ if (!raw) {
71
+ return {};
72
+ }
73
+ try {
74
+ const parsed = JSON.parse(raw);
75
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
76
+ return parsed as Record<string, unknown>;
77
+ }
78
+ return {};
79
+ } catch {
80
+ return {};
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Merge host-provided defaults with the stored per-user settings. Stored values
86
+ * win, so a returning user sees their saved preferences while a host can still
87
+ * seed sensible first-run defaults that fill any gaps.
88
+ */
89
+ export function mergeUserSettings(
90
+ hostDefaults: Record<string, unknown> | undefined | null,
91
+ stored: Record<string, unknown> | undefined | null
92
+ ): Record<string, unknown> {
93
+ return { ...(hostDefaults ?? {}), ...(stored ?? {}) };
94
+ }
95
+
96
+ /**
97
+ * Apply an `onSaveUserSettings` payload to the host's current settings snapshot.
98
+ *
99
+ * **Merge, not replace.** The contract asks components to pass the complete
100
+ * settings object, but the host must be resilient to a component (especially an
101
+ * AI-generated one) passing only the changed keys — and to the stale-prop case
102
+ * where a component spreads the mount-time `savedUserSettings` prop, which the
103
+ * host deliberately never refreshes mid-session (no re-render on save). Under
104
+ * full-replace semantics either slip silently wipes every other saved
105
+ * preference; under merge the worst case is a no-op.
106
+ *
107
+ * **Removing a key requires explicit intent**: set its value to `null` and the
108
+ * key is deleted from the snapshot (reads fall back to defaults via the
109
+ * documented `savedUserSettings?.key ?? fallback` pattern). `undefined` values
110
+ * are treated the same way, since `JSON.stringify` would drop them from the
111
+ * persisted blob anyway and the in-memory snapshot must stay consistent with
112
+ * what is stored.
113
+ *
114
+ * @returns a new object — neither input is mutated.
115
+ */
116
+ export function applyUserSettingsUpdate(
117
+ current: Record<string, unknown> | undefined | null,
118
+ incoming: Record<string, unknown> | undefined | null
119
+ ): Record<string, unknown> {
120
+ const next: Record<string, unknown> = { ...(current ?? {}) };
121
+ for (const [key, value] of Object.entries(incoming ?? {})) {
122
+ if (value === null || value === undefined) {
123
+ delete next[key];
124
+ } else {
125
+ next[key] = value;
126
+ }
127
+ }
128
+ return next;
129
+ }