@getuserfeedback/protocol 3.0.10 → 3.0.12

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,9 +1,9 @@
1
1
  import type { WidgetLayoutSize, WidgetLayoutSizeUpdate, WidgetLayoutTimedSizeUpdate } from "./widget-layout-size-update.js";
2
2
  export type WidgetLayoutComparableSizeUpdate = WidgetLayoutSizeUpdate | WidgetLayoutTimedSizeUpdate;
3
- export type WidgetLayoutUnchangedTransitionPlan = {
3
+ export type WidgetLayoutUnchangedTransitionPlan<Update extends WidgetLayoutComparableSizeUpdate = WidgetLayoutComparableSizeUpdate> = {
4
4
  kind: "unchanged";
5
5
  size: WidgetLayoutSize;
6
- update: WidgetLayoutSizeUpdate;
6
+ update: Update;
7
7
  observedAtMs: number;
8
8
  };
9
9
  export type WidgetLayoutInstantTransitionPlan = {
@@ -13,11 +13,26 @@ export type WidgetLayoutInstantTransitionPlan = {
13
13
  startedAtMs: number;
14
14
  settledAtMs: number;
15
15
  };
16
- export type WidgetLayoutTransitionPlan = WidgetLayoutUnchangedTransitionPlan | WidgetLayoutInstantTransitionPlan;
16
+ export type WidgetLayoutTimedTransitionPlan = {
17
+ kind: "timed";
18
+ size: WidgetLayoutSize;
19
+ update: WidgetLayoutTimedSizeUpdate;
20
+ startedAtMs: number;
21
+ settledAtMs: number;
22
+ };
23
+ export type WidgetLayoutInstantOnlyTransitionPlan = WidgetLayoutUnchangedTransitionPlan<WidgetLayoutSizeUpdate> | WidgetLayoutInstantTransitionPlan;
24
+ export type WidgetLayoutTransitionPlan = WidgetLayoutInstantOnlyTransitionPlan;
25
+ export type WidgetLayoutTimedCapableTransitionPlan = WidgetLayoutUnchangedTransitionPlan | WidgetLayoutInstantTransitionPlan | WidgetLayoutTimedTransitionPlan;
26
+ export declare function getWidgetLayoutPlanNowMs(): number;
17
27
  export declare function areWidgetLayoutSizeUpdatesEqual(left: WidgetLayoutComparableSizeUpdate | undefined, right: WidgetLayoutComparableSizeUpdate): boolean;
18
28
  export declare function planWidgetLayoutTransition(input: {
19
29
  previous?: WidgetLayoutSizeUpdate;
20
30
  next: WidgetLayoutSizeUpdate;
21
31
  nowMs: number;
22
- }): WidgetLayoutTransitionPlan;
32
+ }): WidgetLayoutInstantOnlyTransitionPlan;
33
+ export declare function planWidgetLayoutTransition(input: {
34
+ previous?: WidgetLayoutComparableSizeUpdate;
35
+ next: WidgetLayoutComparableSizeUpdate;
36
+ nowMs: number;
37
+ }): WidgetLayoutTimedCapableTransitionPlan;
23
38
  //# sourceMappingURL=widget-layout-transition-plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widget-layout-transition-plan.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-transition-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,gBAAgB,EAChB,sBAAsB,EACtB,2BAA2B,EAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,MAAM,gCAAgC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAE/B,MAAM,MAAM,mCAAmC,GAAG;IACjD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACnC,mCAAmC,GACnC,iCAAiC,CAAC;AAErC,wBAAgB,+BAA+B,CAC9C,IAAI,EAAE,gCAAgC,GAAG,SAAS,EAClD,KAAK,EAAE,gCAAgC,GACrC,OAAO,CAiCT;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,0BAA0B,CAyB7B"}
1
+ {"version":3,"file":"widget-layout-transition-plan.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-transition-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,gBAAgB,EAChB,sBAAsB,EACtB,2BAA2B,EAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,MAAM,gCAAgC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAE/B,MAAM,MAAM,mCAAmC,CAC9C,MAAM,SACL,gCAAgC,GAAG,gCAAgC,IACjE;IACH,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,2BAA2B,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qCAAqC,GAC9C,mCAAmC,CAAC,sBAAsB,CAAC,GAC3D,iCAAiC,CAAC;AAErC,MAAM,MAAM,0BAA0B,GAAG,qCAAqC,CAAC;AAE/E,MAAM,MAAM,sCAAsC,GAC/C,mCAAmC,GACnC,iCAAiC,GACjC,+BAA+B,CAAC;AAEnC,wBAAgB,wBAAwB,IAAI,MAAM,CAQjD;AAED,wBAAgB,+BAA+B,CAC9C,IAAI,EAAE,gCAAgC,GAAG,SAAS,EAClD,KAAK,EAAE,gCAAgC,GACrC,OAAO,CAiCT;AAsCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,qCAAqC,CAAC;AAC1C,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,EAAE,gCAAgC,CAAC;IAC5C,IAAI,EAAE,gCAAgC,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,sCAAsC,CAAC"}
@@ -1,3 +1,11 @@
1
+ export function getWidgetLayoutPlanNowMs() {
2
+ const performanceNow = typeof globalThis.performance?.now === "function"
3
+ ? globalThis.performance.now()
4
+ : undefined;
5
+ return typeof performanceNow === "number" && Number.isFinite(performanceNow)
6
+ ? performanceNow
7
+ : Date.now();
8
+ }
1
9
  export function areWidgetLayoutSizeUpdatesEqual(left, right) {
2
10
  if (left?.size.width !== right.size.width ||
3
11
  left.size.height !== right.size.height ||
@@ -24,6 +32,27 @@ export function areWidgetLayoutSizeUpdatesEqual(left, right) {
24
32
  }
25
33
  }
26
34
  }
35
+ function cloneWidgetLayoutSizeUpdate(update) {
36
+ if (update.transition.kind === "timed") {
37
+ return {
38
+ size: { ...update.size },
39
+ transition: {
40
+ ...update.transition,
41
+ easing: {
42
+ ...update.transition.easing,
43
+ points: [...update.transition.easing.points],
44
+ },
45
+ },
46
+ };
47
+ }
48
+ return {
49
+ size: { ...update.size },
50
+ transition: { ...update.transition },
51
+ };
52
+ }
53
+ function isWidgetLayoutTimedSizeUpdate(update) {
54
+ return update.transition.kind === "timed";
55
+ }
27
56
  export function planWidgetLayoutTransition(input) {
28
57
  if (!Number.isFinite(input.nowMs)) {
29
58
  throw new RangeError("Widget layout transition plans require finite nowMs");
@@ -32,20 +61,23 @@ export function planWidgetLayoutTransition(input) {
32
61
  return {
33
62
  kind: "unchanged",
34
63
  size: { ...input.next.size },
35
- update: {
36
- size: { ...input.next.size },
37
- transition: { ...input.next.transition },
38
- },
64
+ update: cloneWidgetLayoutSizeUpdate(input.next),
39
65
  observedAtMs: input.nowMs,
40
66
  };
41
67
  }
68
+ if (isWidgetLayoutTimedSizeUpdate(input.next)) {
69
+ return {
70
+ kind: "timed",
71
+ size: { ...input.next.size },
72
+ update: cloneWidgetLayoutSizeUpdate(input.next),
73
+ startedAtMs: input.nowMs,
74
+ settledAtMs: input.nowMs + input.next.transition.durationMs,
75
+ };
76
+ }
42
77
  return {
43
78
  kind: "instant",
44
79
  size: { ...input.next.size },
45
- update: {
46
- size: { ...input.next.size },
47
- transition: { ...input.next.transition },
48
- },
80
+ update: cloneWidgetLayoutSizeUpdate(input.next),
49
81
  startedAtMs: input.nowMs,
50
82
  settledAtMs: input.nowMs,
51
83
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/protocol",
3
- "version": "3.0.10",
3
+ "version": "3.0.12",
4
4
  "description": "getuserfeedback widget protocol — host surface and (later) wire protocol",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -133,7 +133,7 @@
133
133
  },
134
134
  "scripts": {
135
135
  "lint": "ultracite check .",
136
- "build": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json",
136
+ "build": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json && bun scripts/verify-dist-js.ts",
137
137
  "types": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json --emitDeclarationOnly",
138
138
  "typecheck": "tsc -b tsconfig.json",
139
139
  "test": "bun test --dots",
@@ -5,13 +5,95 @@ import {
5
5
  } from "./widget-layout-size-update.js";
6
6
  import {
7
7
  areWidgetLayoutSizeUpdatesEqual,
8
+ getWidgetLayoutPlanNowMs,
8
9
  planWidgetLayoutTransition,
10
+ type WidgetLayoutInstantOnlyTransitionPlan,
11
+ type WidgetLayoutTransitionPlan,
9
12
  } from "./widget-layout-transition-plan.js";
10
13
 
11
14
  describe("widget layout transition plan", () => {
15
+ function expectInstantOnlyPlan(
16
+ _plan: WidgetLayoutInstantOnlyTransitionPlan,
17
+ ): void {}
18
+ function expectLegacyTransitionPlan(
19
+ _plan: WidgetLayoutTransitionPlan,
20
+ ): void {}
21
+
22
+ test("uses the browser monotonic clock for layout plan timestamps", () => {
23
+ const originalPerformanceDescriptor = Object.getOwnPropertyDescriptor(
24
+ globalThis,
25
+ "performance",
26
+ );
27
+
28
+ try {
29
+ Object.defineProperty(globalThis, "performance", {
30
+ configurable: true,
31
+ value: {
32
+ now: () => 123.45,
33
+ },
34
+ });
35
+
36
+ expect(getWidgetLayoutPlanNowMs()).toBe(123.45);
37
+ } finally {
38
+ if (originalPerformanceDescriptor) {
39
+ Object.defineProperty(
40
+ globalThis,
41
+ "performance",
42
+ originalPerformanceDescriptor,
43
+ );
44
+ } else {
45
+ Reflect.deleteProperty(globalThis, "performance");
46
+ }
47
+ }
48
+ });
49
+
50
+ test("falls back to Date.now when the monotonic clock is unavailable", () => {
51
+ const originalNow = Date.now;
52
+ const originalPerformanceDescriptor = Object.getOwnPropertyDescriptor(
53
+ globalThis,
54
+ "performance",
55
+ );
56
+
57
+ try {
58
+ Date.now = () => 1_700;
59
+ Object.defineProperty(globalThis, "performance", {
60
+ configurable: true,
61
+ value: {
62
+ now: () => Number.NaN,
63
+ },
64
+ });
65
+
66
+ expect(getWidgetLayoutPlanNowMs()).toBe(1_700);
67
+ } finally {
68
+ Date.now = originalNow;
69
+ if (originalPerformanceDescriptor) {
70
+ Object.defineProperty(
71
+ globalThis,
72
+ "performance",
73
+ originalPerformanceDescriptor,
74
+ );
75
+ } else {
76
+ Reflect.deleteProperty(globalThis, "performance");
77
+ }
78
+ }
79
+ });
80
+
12
81
  test("plans the first instant layout update at the requested time", () => {
13
82
  const next = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 });
14
83
 
84
+ expectInstantOnlyPlan(
85
+ planWidgetLayoutTransition({
86
+ next,
87
+ nowMs: 1_000,
88
+ }),
89
+ );
90
+ expectLegacyTransitionPlan(
91
+ planWidgetLayoutTransition({
92
+ next,
93
+ nowMs: 1_000,
94
+ }),
95
+ );
96
+
15
97
  expect(
16
98
  planWidgetLayoutTransition({
17
99
  next,
@@ -93,6 +175,37 @@ describe("widget layout transition plan", () => {
93
175
  });
94
176
  });
95
177
 
178
+ test("plans explicit timed updates with settled time from duration", () => {
179
+ const next = toTimedWidgetLayoutSizeUpdate({
180
+ size: { width: 320, height: 360 },
181
+ durationMs: 180,
182
+ easing: [0.2, 0, 0, 1],
183
+ });
184
+
185
+ expect(
186
+ planWidgetLayoutTransition({
187
+ next,
188
+ nowMs: 1_500,
189
+ }),
190
+ ).toEqual({
191
+ kind: "timed",
192
+ size: { width: 320, height: 360 },
193
+ update: {
194
+ size: { width: 320, height: 360 },
195
+ transition: {
196
+ kind: "timed",
197
+ durationMs: 180,
198
+ easing: {
199
+ kind: "cubic-bezier",
200
+ points: [0.2, 0, 0, 1],
201
+ },
202
+ },
203
+ },
204
+ startedAtMs: 1_500,
205
+ settledAtMs: 1_680,
206
+ });
207
+ });
208
+
96
209
  test("does not leak mutable references from the next update", () => {
97
210
  const next = {
98
211
  size: { width: 320, height: 240 },
@@ -111,6 +224,26 @@ describe("widget layout transition plan", () => {
111
224
  expect(plan.update.transition.durationMs).toBe(0);
112
225
  });
113
226
 
227
+ test("does not leak mutable timed transition points from the next update", () => {
228
+ const next = toTimedWidgetLayoutSizeUpdate({
229
+ size: { width: 320, height: 240 },
230
+ durationMs: 180,
231
+ easing: [0.2, 0, 0, 1],
232
+ });
233
+ const plan = planWidgetLayoutTransition({
234
+ next,
235
+ nowMs: 1_000,
236
+ });
237
+
238
+ (next.transition.easing.points as unknown as number[])[0] = 0.4;
239
+
240
+ expect(plan.update.transition.kind).toBe("timed");
241
+ if (plan.update.transition.kind !== "timed") {
242
+ throw new Error("expected timed transition");
243
+ }
244
+ expect(plan.update.transition.easing.points).toEqual([0.2, 0, 0, 1]);
245
+ });
246
+
114
247
  test("rejects non-finite plan time", () => {
115
248
  expect(() =>
116
249
  planWidgetLayoutTransition({
@@ -8,10 +8,13 @@ export type WidgetLayoutComparableSizeUpdate =
8
8
  | WidgetLayoutSizeUpdate
9
9
  | WidgetLayoutTimedSizeUpdate;
10
10
 
11
- export type WidgetLayoutUnchangedTransitionPlan = {
11
+ export type WidgetLayoutUnchangedTransitionPlan<
12
+ Update extends
13
+ WidgetLayoutComparableSizeUpdate = WidgetLayoutComparableSizeUpdate,
14
+ > = {
12
15
  kind: "unchanged";
13
16
  size: WidgetLayoutSize;
14
- update: WidgetLayoutSizeUpdate;
17
+ update: Update;
15
18
  observedAtMs: number;
16
19
  };
17
20
 
@@ -23,10 +26,35 @@ export type WidgetLayoutInstantTransitionPlan = {
23
26
  settledAtMs: number;
24
27
  };
25
28
 
26
- export type WidgetLayoutTransitionPlan =
27
- | WidgetLayoutUnchangedTransitionPlan
29
+ export type WidgetLayoutTimedTransitionPlan = {
30
+ kind: "timed";
31
+ size: WidgetLayoutSize;
32
+ update: WidgetLayoutTimedSizeUpdate;
33
+ startedAtMs: number;
34
+ settledAtMs: number;
35
+ };
36
+
37
+ export type WidgetLayoutInstantOnlyTransitionPlan =
38
+ | WidgetLayoutUnchangedTransitionPlan<WidgetLayoutSizeUpdate>
28
39
  | WidgetLayoutInstantTransitionPlan;
29
40
 
41
+ export type WidgetLayoutTransitionPlan = WidgetLayoutInstantOnlyTransitionPlan;
42
+
43
+ export type WidgetLayoutTimedCapableTransitionPlan =
44
+ | WidgetLayoutUnchangedTransitionPlan
45
+ | WidgetLayoutInstantTransitionPlan
46
+ | WidgetLayoutTimedTransitionPlan;
47
+
48
+ export function getWidgetLayoutPlanNowMs(): number {
49
+ const performanceNow =
50
+ typeof globalThis.performance?.now === "function"
51
+ ? globalThis.performance.now()
52
+ : undefined;
53
+ return typeof performanceNow === "number" && Number.isFinite(performanceNow)
54
+ ? performanceNow
55
+ : Date.now();
56
+ }
57
+
30
58
  export function areWidgetLayoutSizeUpdatesEqual(
31
59
  left: WidgetLayoutComparableSizeUpdate | undefined,
32
60
  right: WidgetLayoutComparableSizeUpdate,
@@ -65,11 +93,57 @@ export function areWidgetLayoutSizeUpdatesEqual(
65
93
  }
66
94
  }
67
95
 
96
+ function cloneWidgetLayoutSizeUpdate(
97
+ update: WidgetLayoutSizeUpdate,
98
+ ): WidgetLayoutSizeUpdate;
99
+ function cloneWidgetLayoutSizeUpdate(
100
+ update: WidgetLayoutTimedSizeUpdate,
101
+ ): WidgetLayoutTimedSizeUpdate;
102
+ function cloneWidgetLayoutSizeUpdate(
103
+ update: WidgetLayoutComparableSizeUpdate,
104
+ ): WidgetLayoutComparableSizeUpdate;
105
+ function cloneWidgetLayoutSizeUpdate(
106
+ update: WidgetLayoutComparableSizeUpdate,
107
+ ): WidgetLayoutComparableSizeUpdate {
108
+ if (update.transition.kind === "timed") {
109
+ return {
110
+ size: { ...update.size },
111
+ transition: {
112
+ ...update.transition,
113
+ easing: {
114
+ ...update.transition.easing,
115
+ points: [...update.transition.easing.points],
116
+ },
117
+ },
118
+ };
119
+ }
120
+ return {
121
+ size: { ...update.size },
122
+ transition: { ...update.transition },
123
+ };
124
+ }
125
+
126
+ function isWidgetLayoutTimedSizeUpdate(
127
+ update: WidgetLayoutComparableSizeUpdate,
128
+ ): update is WidgetLayoutTimedSizeUpdate {
129
+ return update.transition.kind === "timed";
130
+ }
131
+
68
132
  export function planWidgetLayoutTransition(input: {
69
133
  previous?: WidgetLayoutSizeUpdate;
70
134
  next: WidgetLayoutSizeUpdate;
71
135
  nowMs: number;
72
- }): WidgetLayoutTransitionPlan {
136
+ }): WidgetLayoutInstantOnlyTransitionPlan;
137
+ export function planWidgetLayoutTransition(input: {
138
+ previous?: WidgetLayoutComparableSizeUpdate;
139
+ next: WidgetLayoutComparableSizeUpdate;
140
+ nowMs: number;
141
+ }): WidgetLayoutTimedCapableTransitionPlan;
142
+ export function planWidgetLayoutTransition(input: {
143
+ previous?: WidgetLayoutComparableSizeUpdate;
144
+ next: WidgetLayoutComparableSizeUpdate;
145
+ nowMs: number;
146
+ }): WidgetLayoutTimedCapableTransitionPlan {
73
147
  if (!Number.isFinite(input.nowMs)) {
74
148
  throw new RangeError("Widget layout transition plans require finite nowMs");
75
149
  }
@@ -77,20 +151,23 @@ export function planWidgetLayoutTransition(input: {
77
151
  return {
78
152
  kind: "unchanged",
79
153
  size: { ...input.next.size },
80
- update: {
81
- size: { ...input.next.size },
82
- transition: { ...input.next.transition },
83
- },
154
+ update: cloneWidgetLayoutSizeUpdate(input.next),
84
155
  observedAtMs: input.nowMs,
85
156
  };
86
157
  }
158
+ if (isWidgetLayoutTimedSizeUpdate(input.next)) {
159
+ return {
160
+ kind: "timed",
161
+ size: { ...input.next.size },
162
+ update: cloneWidgetLayoutSizeUpdate(input.next),
163
+ startedAtMs: input.nowMs,
164
+ settledAtMs: input.nowMs + input.next.transition.durationMs,
165
+ };
166
+ }
87
167
  return {
88
168
  kind: "instant",
89
169
  size: { ...input.next.size },
90
- update: {
91
- size: { ...input.next.size },
92
- transition: { ...input.next.transition },
93
- },
170
+ update: cloneWidgetLayoutSizeUpdate(input.next),
94
171
  startedAtMs: input.nowMs,
95
172
  settledAtMs: input.nowMs,
96
173
  };