@getuserfeedback/react-native 3.0.37 → 3.0.39

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.
@@ -1,23 +1,29 @@
1
+ import { type EventViewSize } from "@getuserfeedback/protocol/internal/view-host-control-messages";
2
+ import type { CoreWebViewHostConnection } from "./core-webview-host-controller.js";
3
+ import type { ViewWebViewHostConnection } from "./view-webview-host-controller.js";
1
4
  type NativeViewActivationState = "idle" | "prerendering" | "activation-requested" | "prerendering-activation-requested" | "prerendered" | "active";
5
+ type NativeViewSizeReport = Omit<EventViewSize, "gen" | "t" | "viewId">;
2
6
  type NativeViewRecord = Readonly<{
3
7
  allocationId: number;
4
8
  activationState: NativeViewActivationState;
5
9
  generation: number;
6
10
  instanceId: string;
11
+ size: NativeViewSizeReport | null;
7
12
  srcdoc: string;
8
13
  viewId: string;
9
14
  }>;
10
15
  export type NativeViewRecordController = {
16
+ attachViewReadinessConnection(input: {
17
+ allocationId: number;
18
+ connection: ViewWebViewHostConnection;
19
+ }): (() => void) | null;
11
20
  dispose(): void;
12
21
  getSnapshot(): readonly NativeViewRecord[];
13
- handleCoreMessage(value: unknown): void;
14
- markPrepared(input: {
15
- allocationId: number;
16
- viewId: string;
17
- }): boolean;
18
22
  subscribe(listener: () => void): () => void;
19
23
  };
20
24
  export declare function createNativeViewRecordController(input: {
21
- generation: number;
25
+ coreConnection: CoreWebViewHostConnection;
26
+ onError: (error: Error) => void;
27
+ readinessRetryDelayMs?: number;
22
28
  }): NativeViewRecordController;
23
29
  export {};
@@ -1,6 +1,11 @@
1
1
  import { coreViewHostActionSchema, } from "@getuserfeedback/protocol/internal/core-view-host-actions";
2
- import { createViewActivationController, createViewPreparationRegistry, } from "./view-orchestration-runtime.js";
2
+ import { buildEventViewSize, eventViewSizeSchema, } from "@getuserfeedback/protocol/internal/view-host-control-messages";
3
+ import { buildEventViewPrerendered } from "@getuserfeedback/protocol/internal/view-host-lifecycle-events";
4
+ import { createViewActivationController, createViewPreparationReadinessRegistry, createViewPreparationRegistry, } from "./view-orchestration-runtime.js";
5
+ const createNativeViewReadinessRegistry = () => createViewPreparationReadinessRegistry();
6
+ const DEFAULT_READINESS_RETRY_DELAY_MS = 50;
3
7
  export function createNativeViewRecordController(input) {
8
+ var _a;
4
9
  const entries = new Map();
5
10
  const listeners = new Set();
6
11
  const pendingActivations = new Map();
@@ -8,6 +13,15 @@ export function createNativeViewRecordController(input) {
8
13
  let disposed = false;
9
14
  let nextAllocationId = 0;
10
15
  let snapshot = Object.freeze([]);
16
+ const readinessRetryDelayMs = (_a = input.readinessRetryDelayMs) !== null && _a !== void 0 ? _a : DEFAULT_READINESS_RETRY_DELAY_MS;
17
+ const reportError = (error, fallbackMessage) => {
18
+ try {
19
+ input.onError(error instanceof Error ? error : new Error(fallbackMessage));
20
+ }
21
+ catch (_a) {
22
+ // Error observers cannot change view lifecycle settlement.
23
+ }
24
+ };
11
25
  const notify = () => {
12
26
  snapshot = Object.freeze(Array.from(entries.values(), toRecord));
13
27
  for (const listener of listeners) {
@@ -24,11 +38,15 @@ export function createNativeViewRecordController(input) {
24
38
  return created;
25
39
  };
26
40
  const removeEntry = (entry) => {
41
+ var _a;
27
42
  if (entries.get(entry.viewId) !== entry) {
28
43
  return false;
29
44
  }
45
+ (_a = entry.viewReadinessDetach) === null || _a === void 0 ? void 0 : _a.call(entry);
30
46
  entries.delete(entry.viewId);
31
- notify();
47
+ if (!disposed) {
48
+ notify();
49
+ }
32
50
  return true;
33
51
  };
34
52
  const handlePrerender = (action) => {
@@ -50,7 +68,10 @@ export function createNativeViewRecordController(input) {
50
68
  generation: action.gen,
51
69
  instanceId: action.instanceId,
52
70
  preparation,
71
+ preparationState: "claimed",
72
+ size: null,
53
73
  srcdoc: action.srcdoc,
74
+ viewReadinessDetach: null,
54
75
  viewId: action.viewId,
55
76
  };
56
77
  entries.set(action.viewId, entry);
@@ -79,16 +100,214 @@ export function createNativeViewRecordController(input) {
79
100
  removeEntry(entry);
80
101
  }
81
102
  };
103
+ const handleCoreMessage = (value) => {
104
+ if (disposed) {
105
+ return;
106
+ }
107
+ const parsed = coreViewHostActionSchema.safeParse(value);
108
+ if (!parsed.success ||
109
+ parsed.data.gen !== input.coreConnection.generation) {
110
+ return;
111
+ }
112
+ switch (parsed.data.t) {
113
+ case "getuserfeedback:action:prerenderView":
114
+ handlePrerender(parsed.data);
115
+ break;
116
+ case "getuserfeedback:action:activateView":
117
+ handleActivate(parsed.data);
118
+ break;
119
+ case "getuserfeedback:action:closeView":
120
+ handleClose(parsed.data);
121
+ break;
122
+ }
123
+ };
124
+ const completeConnectionReadiness = (delivery, entry, size) => {
125
+ if (!size || disposed || entries.get(entry.viewId) !== entry) {
126
+ return false;
127
+ }
128
+ if (entry.preparationState === "claimed") {
129
+ if (!entry.preparation.commit()) {
130
+ return false;
131
+ }
132
+ entry.preparationState = "committed";
133
+ }
134
+ if (delivery.stage === "pending-size") {
135
+ input.coreConnection.transport.postMessage(buildEventViewSize(Object.assign({ gen: entry.generation, viewId: entry.viewId }, size)));
136
+ delivery.stage = "pending-prerendered";
137
+ }
138
+ if (delivery.stage === "pending-prerendered") {
139
+ input.coreConnection.transport.postMessage(buildEventViewPrerendered({
140
+ gen: entry.generation,
141
+ viewId: entry.viewId,
142
+ }));
143
+ delivery.stage = "delivered";
144
+ }
145
+ if (entry.preparationState !== "prepared") {
146
+ entry.preparationState = "prepared";
147
+ entry.activation.markPrerendered();
148
+ }
149
+ notify();
150
+ return true;
151
+ };
152
+ const stopCoreTransport = input.coreConnection.transport.subscribe(handleCoreMessage);
82
153
  return {
154
+ attachViewReadinessConnection({ allocationId, connection }) {
155
+ if (disposed) {
156
+ return null;
157
+ }
158
+ const { viewId } = connection;
159
+ const entry = entries.get(viewId);
160
+ if (!entry ||
161
+ entry.allocationId !== allocationId ||
162
+ connection.generation !== input.coreConnection.generation) {
163
+ return null;
164
+ }
165
+ const previousDetach = entry.viewReadinessDetach;
166
+ const connectionReadiness = createNativeViewReadinessRegistry().begin(viewId);
167
+ const readinessDelivery = {
168
+ stage: "awaiting-readiness",
169
+ };
170
+ let latestReadySize = null;
171
+ let retryState = "available";
172
+ let retryTimer = null;
173
+ let detached = false;
174
+ let subscribed = false;
175
+ let unsubscribe = () => { };
176
+ const bufferedMessages = [];
177
+ const detach = () => {
178
+ if (detached) {
179
+ return;
180
+ }
181
+ detached = true;
182
+ if (retryTimer !== null) {
183
+ clearTimeout(retryTimer);
184
+ retryTimer = null;
185
+ }
186
+ unsubscribe();
187
+ connectionReadiness.release();
188
+ if (entry.viewReadinessDetach === detach) {
189
+ entry.viewReadinessDetach = null;
190
+ }
191
+ };
192
+ const attemptReadinessDelivery = (isRetry = false) => {
193
+ if (!latestReadySize ||
194
+ readinessDelivery.stage === "awaiting-readiness" ||
195
+ readinessDelivery.stage === "delivered" ||
196
+ detached ||
197
+ disposed ||
198
+ entries.get(viewId) !== entry ||
199
+ entry.viewReadinessDetach !== detach) {
200
+ return "idle";
201
+ }
202
+ if ((isRetry && retryState !== "scheduled") ||
203
+ (!isRetry && retryState !== "available")) {
204
+ return "failed";
205
+ }
206
+ if (isRetry) {
207
+ retryState = "exhausted";
208
+ }
209
+ try {
210
+ if (completeConnectionReadiness(readinessDelivery, entry, latestReadySize)) {
211
+ if (retryTimer !== null) {
212
+ clearTimeout(retryTimer);
213
+ retryTimer = null;
214
+ }
215
+ return "delivered";
216
+ }
217
+ return "idle";
218
+ }
219
+ catch (error) {
220
+ if (!isRetry) {
221
+ retryState = "scheduled";
222
+ retryTimer = setTimeout(() => {
223
+ retryTimer = null;
224
+ attemptReadinessDelivery(true);
225
+ }, readinessRetryDelayMs);
226
+ }
227
+ else {
228
+ reportError(error, "Native view readiness delivery failed");
229
+ }
230
+ return "failed";
231
+ }
232
+ };
233
+ const handleMessage = (value) => {
234
+ if (detached ||
235
+ disposed ||
236
+ entries.get(viewId) !== entry ||
237
+ entry.viewReadinessDetach !== detach) {
238
+ return;
239
+ }
240
+ const parsed = eventViewSizeSchema.safeParse(value);
241
+ if (!parsed.success ||
242
+ parsed.data.gen !== input.coreConnection.generation ||
243
+ (parsed.data.viewId !== undefined && parsed.data.viewId !== viewId)) {
244
+ return;
245
+ }
246
+ const size = toViewSizeReport(parsed.data);
247
+ entry.size = size;
248
+ const readySize = connectionReadiness.recordSize(size);
249
+ if (readySize) {
250
+ latestReadySize = readySize;
251
+ readinessDelivery.stage = "pending-size";
252
+ }
253
+ else if (readinessDelivery.stage === "pending-size" ||
254
+ readinessDelivery.stage === "pending-prerendered") {
255
+ latestReadySize = size;
256
+ }
257
+ if (attemptReadinessDelivery() !== "delivered") {
258
+ notify();
259
+ }
260
+ };
261
+ let subscribedUnsubscribe;
262
+ try {
263
+ subscribedUnsubscribe = connection.transport.subscribe((value) => {
264
+ if (!subscribed) {
265
+ bufferedMessages.push(value);
266
+ return;
267
+ }
268
+ handleMessage(value);
269
+ });
270
+ }
271
+ catch (error) {
272
+ connectionReadiness.release();
273
+ reportError(error, "Native view readiness subscription failed");
274
+ return null;
275
+ }
276
+ unsubscribe = subscribedUnsubscribe;
277
+ if (disposed || entries.get(viewId) !== entry) {
278
+ subscribedUnsubscribe();
279
+ connectionReadiness.release();
280
+ return null;
281
+ }
282
+ entry.viewReadinessDetach = detach;
283
+ previousDetach === null || previousDetach === void 0 ? void 0 : previousDetach();
284
+ subscribed = true;
285
+ for (const message of bufferedMessages) {
286
+ handleMessage(message);
287
+ if (detached) {
288
+ return null;
289
+ }
290
+ }
291
+ const readySize = connectionReadiness.markHostPrepared();
292
+ if (readySize) {
293
+ latestReadySize = readySize;
294
+ readinessDelivery.stage = "pending-size";
295
+ }
296
+ attemptReadinessDelivery();
297
+ return detached ? null : detach;
298
+ },
83
299
  dispose() {
84
300
  if (disposed) {
85
301
  return;
86
302
  }
87
303
  disposed = true;
88
304
  const hadEntries = entries.size > 0;
89
- entries.clear();
90
- pendingActivations.clear();
305
+ stopCoreTransport();
91
306
  preparations.cancelAll();
307
+ for (const entry of Array.from(entries.values())) {
308
+ removeEntry(entry);
309
+ }
310
+ pendingActivations.clear();
92
311
  if (hadEntries) {
93
312
  snapshot = Object.freeze([]);
94
313
  for (const listener of listeners) {
@@ -98,44 +317,6 @@ export function createNativeViewRecordController(input) {
98
317
  listeners.clear();
99
318
  },
100
319
  getSnapshot: () => snapshot,
101
- handleCoreMessage(value) {
102
- if (disposed) {
103
- return;
104
- }
105
- const parsed = coreViewHostActionSchema.safeParse(value);
106
- if (!parsed.success || parsed.data.gen !== input.generation) {
107
- return;
108
- }
109
- switch (parsed.data.t) {
110
- case "getuserfeedback:action:prerenderView":
111
- handlePrerender(parsed.data);
112
- break;
113
- case "getuserfeedback:action:activateView":
114
- handleActivate(parsed.data);
115
- break;
116
- case "getuserfeedback:action:closeView":
117
- handleClose(parsed.data);
118
- break;
119
- }
120
- },
121
- markPrepared({ allocationId, viewId }) {
122
- if (disposed) {
123
- return false;
124
- }
125
- const entry = entries.get(viewId);
126
- if (!entry || entry.allocationId !== allocationId) {
127
- return false;
128
- }
129
- if (!entry.preparation.commit()) {
130
- return false;
131
- }
132
- const previousState = entry.activation.getState();
133
- entry.activation.markPrerendered();
134
- if (entry.activation.getState() !== previousState) {
135
- notify();
136
- }
137
- return true;
138
- },
139
320
  subscribe(listener) {
140
321
  if (disposed) {
141
322
  return () => { };
@@ -151,9 +332,21 @@ function toRecord(entry) {
151
332
  allocationId: entry.allocationId,
152
333
  generation: entry.generation,
153
334
  instanceId: entry.instanceId,
335
+ size: entry.size,
154
336
  srcdoc: entry.srcdoc,
155
337
  viewId: entry.viewId,
156
338
  });
157
339
  }
158
- // TODO(migration): Subscribe this controller to the native core transport, render its identity-safe records through ViewWebViewHost, and complete preparation only after protocol-owned view readiness is observed. Keep the provider runtime active during this proof.
159
- // [from=private-native-view-allocation-state] [to=private-native-view-readiness-routing] [scope=slice] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=30]
340
+ function toViewSizeReport(event) {
341
+ return {
342
+ width: event.width,
343
+ height: event.height,
344
+ intrinsicWidth: event.intrinsicWidth,
345
+ frameAppearance: event.frameAppearance,
346
+ placement: event.placement,
347
+ transitionPolicy: event.transitionPolicy,
348
+ layoutTransactionId: event.layoutTransactionId,
349
+ };
350
+ }
351
+ // TODO(migration): Render the controller's identity-safe records through ViewWebViewHost and attach each connected view transport. Keep the provider runtime active during this proof.
352
+ // [from=private-native-view-readiness-routing] [to=private-native-view-rendering-proof] [scope=slice] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=40]
package/dist/version.js CHANGED
@@ -3,4 +3,4 @@ const reactNativeSdkVersion = typeof __GX_REACT_NATIVE_SDK_VERSION__ === "string
3
3
  : "";
4
4
  // Build scripts patch this fallback to the package version for published artifacts.
5
5
  // Source-linked workspace usage keeps the local fallback.
6
- export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "3.0.37";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "3.0.39";
@@ -79,6 +79,54 @@ function readViewActivationState(value) {
79
79
  }
80
80
  return value;
81
81
  }
82
+ // ../view-orchestration/src/view-preparation-readiness-registry.ts
83
+ function createViewPreparationReadinessRegistry() {
84
+ const states = new Map;
85
+ const resolveReadySize = (state2) => {
86
+ if (!state2 || state2.status === "ready" || !state2.hostPrepared || state2.size === null) {
87
+ return null;
88
+ }
89
+ state2.status = "ready";
90
+ return state2.size;
91
+ };
92
+ return {
93
+ begin(viewId) {
94
+ const state2 = {
95
+ hostPrepared: false,
96
+ size: null,
97
+ status: "pending"
98
+ };
99
+ states.set(viewId, state2);
100
+ const getCurrentState = () => states.get(viewId) === state2 ? state2 : null;
101
+ return {
102
+ markHostPrepared() {
103
+ const current = getCurrentState();
104
+ if (!current || current.status === "ready") {
105
+ return null;
106
+ }
107
+ current.hostPrepared = true;
108
+ return resolveReadySize(current);
109
+ },
110
+ recordSize(size) {
111
+ const current = getCurrentState();
112
+ if (!current || current.status === "ready") {
113
+ return null;
114
+ }
115
+ current.size ??= size;
116
+ return resolveReadySize(current);
117
+ },
118
+ release() {
119
+ if (getCurrentState()) {
120
+ states.delete(viewId);
121
+ }
122
+ }
123
+ };
124
+ },
125
+ clearAll: () => {
126
+ states.clear();
127
+ }
128
+ };
129
+ }
82
130
  // ../view-orchestration/src/view-preparation-registry.ts
83
131
  class ViewPreparationCancelledError extends Error {
84
132
  viewId;
@@ -173,8 +221,12 @@ function createViewActivationController2() {
173
221
  function createViewPreparationRegistry2() {
174
222
  return createViewPreparationRegistry();
175
223
  }
224
+ function createViewPreparationReadinessRegistry2() {
225
+ return createViewPreparationReadinessRegistry();
226
+ }
176
227
  export {
177
228
  createViewPreparationRegistry2 as createViewPreparationRegistry,
229
+ createViewPreparationReadinessRegistry2 as createViewPreparationReadinessRegistry,
178
230
  createViewActivationController2 as createViewActivationController,
179
231
  createActiveViewSnapshotListController2 as createActiveViewSnapshotListController
180
232
  };
@@ -6,6 +6,7 @@ export type ViewWebViewHostConnection = {
6
6
  postMessage: (message: unknown) => void;
7
7
  subscribe: (listener: (message: unknown) => void) => () => void;
8
8
  };
9
+ viewId: string;
9
10
  };
10
11
  export type ViewWebViewHostController = WebViewHostController<ViewWebViewBridgeMessage>;
11
12
  export declare const createViewWebViewHostController: (input: {
@@ -21,6 +21,7 @@ export const createViewWebViewHostController = (input) => {
21
21
  }
22
22
  }),
23
23
  },
24
+ viewId: input.viewId,
24
25
  });
25
26
  },
26
27
  onDisconnected: input.onDisconnected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "3.0.37",
3
+ "version": "3.0.39",
4
4
  "description": "getuserfeedback React Native SDK",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -44,8 +44,8 @@
44
44
  "lint": "biome check ."
45
45
  },
46
46
  "dependencies": {
47
- "@getuserfeedback/protocol": "^3.10.0",
48
- "@getuserfeedback/sdk": "^0.10.6",
47
+ "@getuserfeedback/protocol": "^3.11.0",
48
+ "@getuserfeedback/sdk": "^0.10.8",
49
49
  "robot3": "^1.2.0"
50
50
  },
51
51
  "peerDependencies": {