@weaverse/hydrogen 5.20.1 → 5.20.3
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/dist/index.js +106 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -55
- package/dist/index.mjs.map +1 -1
- package/dist/types/utils/is-same-loader-payload.d.ts +39 -0
- package/dist/types/utils/is-same-loader-payload.d.ts.map +1 -0
- package/dist/types/utils/sync-reused-instance.d.ts +5 -3
- package/dist/types/utils/sync-reused-instance.d.ts.map +1 -1
- package/dist/types/utils/use-studio.d.ts +0 -1
- package/dist/types/utils/use-studio.d.ts.map +1 -1
- package/dist/types/weaverse-client.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/utils/is-same-loader-payload.ts +187 -0
- package/src/utils/sync-reused-instance.ts +11 -8
- package/src/utils/use-studio.ts +0 -37
- package/src/weaverse-client.ts +0 -1
- package/dist/types/utils/pixel.d.ts +0 -16
- package/dist/types/utils/pixel.d.ts.map +0 -1
- package/src/utils/pixel.ts +0 -71
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural comparison used to detect whether a reused Weaverse instance
|
|
3
|
+
* must adopt a fresh loader payload.
|
|
4
|
+
*
|
|
5
|
+
* `JSON.stringify` cannot do this job on the client:
|
|
6
|
+
*
|
|
7
|
+
* - Route loader data reaches `dataContext` unserialized, so deferred values
|
|
8
|
+
* are still `Promise`s. React 19 development builds hang an enumerable,
|
|
9
|
+
* self-referencing `_debugInfo` off those promises, and stringifying one
|
|
10
|
+
* throws `TypeError: Converting circular structure to JSON` during render
|
|
11
|
+
* (issue #511).
|
|
12
|
+
* - In production every promise stringifies to `{}`, so a *fresh* deferred
|
|
13
|
+
* value looks unchanged and the reused instance keeps serving the previous
|
|
14
|
+
* render's promise. Tokenizing promises to a constant (`'[[promise]]'`) has
|
|
15
|
+
* the same defect — it is stable by construction, therefore blind to
|
|
16
|
+
* identity.
|
|
17
|
+
*
|
|
18
|
+
* Semantics, chosen deliberately:
|
|
19
|
+
*
|
|
20
|
+
* - **Promises/thenables are atomic**: compared by identity, never traversed.
|
|
21
|
+
* A fresh promise is a change; the same promise is not. Debug metadata is
|
|
22
|
+
* invisible to the comparison.
|
|
23
|
+
* - **Opaque objects are atomic**: anything that is not a plain object, an
|
|
24
|
+
* array, or `toJSON`-serializable (`Map`, `Set`, `URL`, `RegExp`, streams,
|
|
25
|
+
* async iterators, cross-realm promises, class instances) is compared by
|
|
26
|
+
* identity. `JSON.stringify` flattens all of them to `{}`, which would
|
|
27
|
+
* silently collapse two distinct async values into "unchanged".
|
|
28
|
+
* - **Cyclic graphs are compared coinductively**: a pair already being
|
|
29
|
+
* compared is assumed equal, so equal self/mutual cycles are equal while a
|
|
30
|
+
* differing leaf anywhere still reports a change. Nothing throws.
|
|
31
|
+
* - **JSON-visible fields only**: `undefined`/function/symbol properties are
|
|
32
|
+
* omitted from objects and read as `null` in arrays, and NaN/±Infinity read
|
|
33
|
+
* as `null` — matching the previous `JSON.stringify` behavior for plain wire
|
|
34
|
+
* payloads. Key order is irrelevant, which removes the old "false mismatch
|
|
35
|
+
* costs a re-render" caveat.
|
|
36
|
+
*/
|
|
37
|
+
/** Compares two loader payloads for JSON-visible structural equality. */
|
|
38
|
+
export declare function isSameLoaderPayload(left: unknown, right: unknown): boolean;
|
|
39
|
+
//# sourceMappingURL=is-same-loader-payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-same-loader-payload.d.ts","sourceRoot":"","sources":["../../../src/utils/is-same-loader-payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAqCH,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAiH1E"}
|
|
@@ -13,9 +13,11 @@ import type { WeaverseHydrogen } from '../WeaverseHydrogenRoot';
|
|
|
13
13
|
* (unsaved drafts must not be clobbered) and applies updates via
|
|
14
14
|
* `refreshStudio` instead.
|
|
15
15
|
*
|
|
16
|
-
* Change detection uses
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Change detection uses {@link isSameLoaderPayload}: a structural comparison
|
|
17
|
+
* that treats promises/thenables and other opaque objects as atomic values
|
|
18
|
+
* compared by identity, and tolerates cyclic graphs. `dataContext` carries
|
|
19
|
+
* live route loader data — including unresolved deferred promises whose React
|
|
20
|
+
* development metadata is cyclic — so it cannot be JSON-serialized here.
|
|
19
21
|
*/
|
|
20
22
|
export declare function syncReusedInstance(weaverse: WeaverseHydrogen, params: WeaverseHydrogenParams): void;
|
|
21
23
|
//# sourceMappingURL=sync-reused-instance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-reused-instance.d.ts","sourceRoot":"","sources":["../../../src/utils/sync-reused-instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"sync-reused-instance.d.ts","sourceRoot":"","sources":["../../../src/utils/sync-reused-instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAG/D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,sBAAsB,QAkC/B"}
|
|
@@ -29,5 +29,4 @@ import type { WeaverseHydrogen } from '../index';
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function useStudioConnect(): void;
|
|
31
31
|
export declare function useStudio(weaverse: WeaverseHydrogen): void;
|
|
32
|
-
export declare function usePixel(context: WeaverseHydrogen): void;
|
|
33
32
|
//# sourceMappingURL=use-studio.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-studio.d.ts","sourceRoot":"","sources":["../../../src/utils/use-studio.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"use-studio.d.ts","sourceRoot":"","sources":["../../../src/utils/use-studio.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AA+BhD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,SAW/B;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,gBAAgB,QAiDnD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"weaverse-client.d.ts","sourceRoot":"","sources":["../../src/weaverse-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"weaverse-client.d.ts","sourceRoot":"","sources":["../../src/weaverse-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@weaverse/hydrogen",
|
|
3
3
|
"author": "Weaverse Team",
|
|
4
4
|
"description": "Components, hooks, and utilities for building Shopify Hydrogen websites with Weaverse",
|
|
5
|
-
"version": "5.20.
|
|
5
|
+
"version": "5.20.3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"typings": "dist/types/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@shopify/hydrogen": ">=2025.5",
|
|
48
48
|
"@shopify/remix-oxygen": "3",
|
|
49
|
-
"@weaverse/react": "5.20.
|
|
49
|
+
"@weaverse/react": "5.20.3",
|
|
50
50
|
"@weaverse/schema": "0.14.0",
|
|
51
51
|
"react": ">=19",
|
|
52
52
|
"react-dom": ">=19",
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural comparison used to detect whether a reused Weaverse instance
|
|
3
|
+
* must adopt a fresh loader payload.
|
|
4
|
+
*
|
|
5
|
+
* `JSON.stringify` cannot do this job on the client:
|
|
6
|
+
*
|
|
7
|
+
* - Route loader data reaches `dataContext` unserialized, so deferred values
|
|
8
|
+
* are still `Promise`s. React 19 development builds hang an enumerable,
|
|
9
|
+
* self-referencing `_debugInfo` off those promises, and stringifying one
|
|
10
|
+
* throws `TypeError: Converting circular structure to JSON` during render
|
|
11
|
+
* (issue #511).
|
|
12
|
+
* - In production every promise stringifies to `{}`, so a *fresh* deferred
|
|
13
|
+
* value looks unchanged and the reused instance keeps serving the previous
|
|
14
|
+
* render's promise. Tokenizing promises to a constant (`'[[promise]]'`) has
|
|
15
|
+
* the same defect — it is stable by construction, therefore blind to
|
|
16
|
+
* identity.
|
|
17
|
+
*
|
|
18
|
+
* Semantics, chosen deliberately:
|
|
19
|
+
*
|
|
20
|
+
* - **Promises/thenables are atomic**: compared by identity, never traversed.
|
|
21
|
+
* A fresh promise is a change; the same promise is not. Debug metadata is
|
|
22
|
+
* invisible to the comparison.
|
|
23
|
+
* - **Opaque objects are atomic**: anything that is not a plain object, an
|
|
24
|
+
* array, or `toJSON`-serializable (`Map`, `Set`, `URL`, `RegExp`, streams,
|
|
25
|
+
* async iterators, cross-realm promises, class instances) is compared by
|
|
26
|
+
* identity. `JSON.stringify` flattens all of them to `{}`, which would
|
|
27
|
+
* silently collapse two distinct async values into "unchanged".
|
|
28
|
+
* - **Cyclic graphs are compared coinductively**: a pair already being
|
|
29
|
+
* compared is assumed equal, so equal self/mutual cycles are equal while a
|
|
30
|
+
* differing leaf anywhere still reports a change. Nothing throws.
|
|
31
|
+
* - **JSON-visible fields only**: `undefined`/function/symbol properties are
|
|
32
|
+
* omitted from objects and read as `null` in arrays, and NaN/±Infinity read
|
|
33
|
+
* as `null` — matching the previous `JSON.stringify` behavior for plain wire
|
|
34
|
+
* payloads. Key order is irrelevant, which removes the old "false mismatch
|
|
35
|
+
* costs a re-render" caveat.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** `JSON.stringify` drops these from objects and nulls them inside arrays. */
|
|
39
|
+
function isJsonOmitted(value: unknown): boolean {
|
|
40
|
+
return (
|
|
41
|
+
value === undefined ||
|
|
42
|
+
typeof value === 'function' ||
|
|
43
|
+
typeof value === 'symbol'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return value !== null && typeof value === 'object'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Duck-typed so cross-realm promises and hand-rolled thenables also match. */
|
|
52
|
+
function isThenable(value: Record<string, unknown>): boolean {
|
|
53
|
+
return typeof value.then === 'function'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPlainObject(value: Record<string, unknown>): boolean {
|
|
57
|
+
const proto = Object.getPrototypeOf(value)
|
|
58
|
+
return proto === Object.prototype || proto === null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns the `toJSON()` projection, or the value itself when there is none. */
|
|
62
|
+
function toJsonValue(value: Record<string, unknown>): unknown {
|
|
63
|
+
return typeof value.toJSON === 'function'
|
|
64
|
+
? (value.toJSON as () => unknown)()
|
|
65
|
+
: value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Maps NaN/±Infinity to `null`, matching how `JSON.stringify` renders them. */
|
|
69
|
+
function toFiniteOrNull(value: unknown): unknown {
|
|
70
|
+
return typeof value === 'number' && !Number.isFinite(value) ? null : value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Compares two loader payloads for JSON-visible structural equality. */
|
|
74
|
+
export function isSameLoaderPayload(left: unknown, right: unknown): boolean {
|
|
75
|
+
// Pairs currently being compared, flattened as [a0, b0, a1, b1, ...].
|
|
76
|
+
// Re-entering a pair means a cycle closed on both sides; assuming equality
|
|
77
|
+
// there is what makes equal cyclic graphs compare equal (bisimulation)
|
|
78
|
+
// while a differing leaf anywhere still reports a change.
|
|
79
|
+
//
|
|
80
|
+
// Scoped to the active path rather than every visited pair, so memory stays
|
|
81
|
+
// proportional to graph depth (single digits for loader payloads) instead of
|
|
82
|
+
// node count, and the common all-equal walk allocates one array.
|
|
83
|
+
//
|
|
84
|
+
// Tradeoff: a graph that shares one subtree through many nested parents
|
|
85
|
+
// re-walks it per path. Wire-serialized loader payloads are trees, so this
|
|
86
|
+
// does not arise; memoizing equal pairs instead measured ~2x slower on a
|
|
87
|
+
// realistic 400-item page, which is the case that actually runs per render.
|
|
88
|
+
const activePairs: object[] = []
|
|
89
|
+
|
|
90
|
+
function isSame(rawA: unknown, rawB: unknown): boolean {
|
|
91
|
+
if (Object.is(rawA, rawB)) {
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
// `JSON.stringify` renders NaN/±Infinity as `null`; normalizing here keeps
|
|
95
|
+
// a non-finite number equal to the `null` a previous wire payload carried.
|
|
96
|
+
const a = toFiniteOrNull(rawA)
|
|
97
|
+
const b = toFiniteOrNull(rawB)
|
|
98
|
+
if (a === b) {
|
|
99
|
+
// Also accepts 0/-0, which `JSON.stringify` renders identically.
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
if (!(isObjectLike(a) && isObjectLike(b))) {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
if (isThenable(a) || isThenable(b)) {
|
|
106
|
+
// Identity comparison above already failed: distinct async values.
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
for (let index = 0; index < activePairs.length; index += 2) {
|
|
110
|
+
if (activePairs[index] === a && activePairs[index + 1] === b) {
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
activePairs.push(a, b)
|
|
116
|
+
try {
|
|
117
|
+
return isSameOpenPair(a, b)
|
|
118
|
+
} finally {
|
|
119
|
+
activePairs.length -= 2
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSameOpenPair(
|
|
124
|
+
a: Record<string, unknown>,
|
|
125
|
+
b: Record<string, unknown>
|
|
126
|
+
): boolean {
|
|
127
|
+
const aJson = toJsonValue(a)
|
|
128
|
+
const bJson = toJsonValue(b)
|
|
129
|
+
if (aJson !== a || bJson !== b) {
|
|
130
|
+
return isSame(aJson, bJson)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
137
|
+
return isSameArray(a, b)
|
|
138
|
+
}
|
|
139
|
+
if (!(isPlainObject(a) && isPlainObject(b))) {
|
|
140
|
+
// Opaque object with no JSON projection — identity is the only honest
|
|
141
|
+
// signal, and it already failed.
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
return isSameObject(a, b)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isSameArray(a: unknown[], b: unknown[]): boolean {
|
|
148
|
+
if (a.length !== b.length) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
for (let index = 0; index < a.length; index++) {
|
|
152
|
+
const aItem = isJsonOmitted(a[index]) ? null : a[index]
|
|
153
|
+
const bItem = isJsonOmitted(b[index]) ? null : b[index]
|
|
154
|
+
if (!isSame(aItem, bItem)) {
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isSameObject(
|
|
162
|
+
a: Record<string, unknown>,
|
|
163
|
+
b: Record<string, unknown>
|
|
164
|
+
): boolean {
|
|
165
|
+
let aKeyCount = 0
|
|
166
|
+
for (const key of Object.keys(a)) {
|
|
167
|
+
if (isJsonOmitted(a[key])) {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
aKeyCount++
|
|
171
|
+
// A JSON-visible value never compares equal to an omitted one, so a
|
|
172
|
+
// missing/undefined counterpart falls out of `isSame` as a change.
|
|
173
|
+
if (!isSame(a[key], b[key])) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let bKeyCount = 0
|
|
178
|
+
for (const key of Object.keys(b)) {
|
|
179
|
+
if (!isJsonOmitted(b[key])) {
|
|
180
|
+
bKeyCount++
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return aKeyCount === bKeyCount
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return isSame(left, right)
|
|
187
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WeaverseHydrogenParams } from '../types'
|
|
2
2
|
import type { WeaverseHydrogen } from '../WeaverseHydrogenRoot'
|
|
3
|
+
import { isSameLoaderPayload } from './is-same-loader-payload'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Apply a fresh loader payload to a reused Weaverse instance.
|
|
@@ -14,9 +15,11 @@ import type { WeaverseHydrogen } from '../WeaverseHydrogenRoot'
|
|
|
14
15
|
* (unsaved drafts must not be clobbered) and applies updates via
|
|
15
16
|
* `refreshStudio` instead.
|
|
16
17
|
*
|
|
17
|
-
* Change detection uses
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Change detection uses {@link isSameLoaderPayload}: a structural comparison
|
|
19
|
+
* that treats promises/thenables and other opaque objects as atomic values
|
|
20
|
+
* compared by identity, and tolerates cyclic graphs. `dataContext` carries
|
|
21
|
+
* live route loader data — including unresolved deferred promises whose React
|
|
22
|
+
* development metadata is cyclic — so it cannot be JSON-serialized here.
|
|
20
23
|
*/
|
|
21
24
|
export function syncReusedInstance(
|
|
22
25
|
weaverse: WeaverseHydrogen,
|
|
@@ -25,11 +28,11 @@ export function syncReusedInstance(
|
|
|
25
28
|
weaverse.requestInfo = params.requestInfo
|
|
26
29
|
weaverse.internal = params.internal
|
|
27
30
|
|
|
28
|
-
let dataChanged =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let dataChanged = !isSameLoaderPayload(weaverse.data, params.data)
|
|
32
|
+
let contextChanged = !isSameLoaderPayload(
|
|
33
|
+
weaverse.dataContext ?? null,
|
|
34
|
+
params.dataContext ?? null
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
if (contextChanged) {
|
|
35
38
|
// Assign before notifying any item store: components resolve data
|
package/src/utils/use-studio.ts
CHANGED
|
@@ -9,11 +9,6 @@ import {
|
|
|
9
9
|
import { useTranslation } from '../hooks/translation-context'
|
|
10
10
|
import type { WeaverseHydrogen } from '../index'
|
|
11
11
|
import { hasWeaverseStudio } from '../types'
|
|
12
|
-
import {
|
|
13
|
-
observeNavigation,
|
|
14
|
-
registerPixelInstance,
|
|
15
|
-
shouldFirePixel,
|
|
16
|
-
} from './pixel'
|
|
17
12
|
import { getStudioScriptSrc, resolveStudioScriptSrc } from './studio-script-src'
|
|
18
13
|
import { useThemeSettingsStore } from './use-theme-settings-store'
|
|
19
14
|
|
|
@@ -133,36 +128,4 @@ export function useStudio(weaverse: WeaverseHydrogen) {
|
|
|
133
128
|
.catch(console.error)
|
|
134
129
|
}
|
|
135
130
|
}, [pathname, search, navigation.state])
|
|
136
|
-
usePixel(weaverse)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function usePixel(context: WeaverseHydrogen) {
|
|
140
|
-
let { projectId, pageId, isDesignMode, weaverseHost } = context
|
|
141
|
-
let { key: navigationKey } = useLocation()
|
|
142
|
-
// Observe every navigation so a persistent layout-level instance forgets
|
|
143
|
-
// the previous navigation's fired pages on a real key change — but NOT on
|
|
144
|
-
// an in-place remount that keeps the same history entry (e.g. the
|
|
145
|
-
// `data={null}` suppress/restore path), which must not double-count.
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (!(projectId && pageId) || isDesignMode) {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
observeNavigation(navigationKey)
|
|
151
|
-
}, [navigationKey, projectId, pageId, isDesignMode])
|
|
152
|
-
|
|
153
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: fire once on mount
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (!(projectId && pageId) || isDesignMode) {
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
// Register BEFORE deciding to fire: the navigation state must live at
|
|
159
|
-
// least as long as some Weaverse instance is mounted (see pixel.ts).
|
|
160
|
-
let unregister = registerPixelInstance()
|
|
161
|
-
if (shouldFirePixel(navigationKey, pageId)) {
|
|
162
|
-
let img = new Image()
|
|
163
|
-
img.onload = () => img.remove()
|
|
164
|
-
img.src = `${weaverseHost}/api/public/px?projectId=${projectId}&pageId=${pageId}`
|
|
165
|
-
}
|
|
166
|
-
return unregister
|
|
167
|
-
}, [])
|
|
168
131
|
}
|
package/src/weaverse-client.ts
CHANGED
|
@@ -1125,7 +1125,6 @@ export class WeaverseClient {
|
|
|
1125
1125
|
} catch (error) {
|
|
1126
1126
|
const errorMessage = '❌ Page load failed'
|
|
1127
1127
|
const errorContext: Record<string, unknown> = {
|
|
1128
|
-
url: this.request.url,
|
|
1129
1128
|
projectId: params.projectId || this.configs.projectId,
|
|
1130
1129
|
}
|
|
1131
1130
|
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Record the navigation key seen by a mounted instance. A change of key is a
|
|
3
|
-
* real navigation and forgets the previous navigation's fired pages; an
|
|
4
|
-
* unchanged key (an in-place remount / suppress-restore on the same history
|
|
5
|
-
* entry) leaves them intact, so the page is not double-counted.
|
|
6
|
-
*/
|
|
7
|
-
export declare function observeNavigation(navigationKey: string): void;
|
|
8
|
-
export declare function shouldFirePixel(navigationKey: string, pageId: string): boolean;
|
|
9
|
-
/**
|
|
10
|
-
* Track a mounted Weaverse instance; returns the unmount cleanup. When the
|
|
11
|
-
* last instance unmounts, the navigation state is forgotten so a Back that
|
|
12
|
-
* restores the same history entry counts again even if no instance survived
|
|
13
|
-
* to observe the detour.
|
|
14
|
-
*/
|
|
15
|
-
export declare function registerPixelInstance(): () => void;
|
|
16
|
-
//# sourceMappingURL=pixel.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pixel.d.ts","sourceRoot":"","sources":["../../../src/utils/pixel.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAK7D;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GACb,OAAO,CAOT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,IAAI,CASlD"}
|
package/src/utils/pixel.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// One pixel per page per navigation. A few facts shape the dedup:
|
|
2
|
-
//
|
|
3
|
-
// 1. Co-located instances: nested compositions mount multiple Weaverse
|
|
4
|
-
// instances on one URL; each page is counted at most once.
|
|
5
|
-
// 2. The pixel FIRE effect runs once per instance mount (see `usePixel`), so
|
|
6
|
-
// an instance never re-fires on its own — a navigation is a fresh mount.
|
|
7
|
-
// 3. `location.key` is stable per history entry. A back/forward POP restores
|
|
8
|
-
// the entry's ORIGINAL key, and an in-place remount on the same entry
|
|
9
|
-
// (e.g. the `data={null}` suppress/restore path in WeaverseHydrogenRoot)
|
|
10
|
-
// keeps that same key. A REAL navigation is therefore a key CHANGE.
|
|
11
|
-
//
|
|
12
|
-
// `firedPageIds` is the set of pages already counted for the current
|
|
13
|
-
// navigation. It is forgotten in two ways:
|
|
14
|
-
// - On a key TRANSITION reported by ANY mounted instance via
|
|
15
|
-
// `observeNavigation` (driven by a per-navigation effect). A persistent
|
|
16
|
-
// layout-level instance keeps observing across a detour through a
|
|
17
|
-
// non-Weaverse route (cart, search, account), so a Back to the original
|
|
18
|
-
// entry is recognised as a new navigation and the page re-counts — while
|
|
19
|
-
// an in-place suppress/restore (no key change) does NOT re-count.
|
|
20
|
-
// - When the LAST instance unmounts (`registerPixelInstance`), covering the
|
|
21
|
-
// detour case where no instance survives to observe the key round-trip
|
|
22
|
-
// (a page with no persistent layout going to /cart and back).
|
|
23
|
-
//
|
|
24
|
-
// Known tradeoff: React StrictMode's dev-only mount→cleanup→mount cycle
|
|
25
|
-
// resets state and double-fires in development — parity with the pre-dedupe
|
|
26
|
-
// behavior; production semantics are unaffected.
|
|
27
|
-
let currentNavigationKey: string | null = null
|
|
28
|
-
let firedPageIds = new Set<string>()
|
|
29
|
-
let mountedInstances = 0
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Record the navigation key seen by a mounted instance. A change of key is a
|
|
33
|
-
* real navigation and forgets the previous navigation's fired pages; an
|
|
34
|
-
* unchanged key (an in-place remount / suppress-restore on the same history
|
|
35
|
-
* entry) leaves them intact, so the page is not double-counted.
|
|
36
|
-
*/
|
|
37
|
-
export function observeNavigation(navigationKey: string): void {
|
|
38
|
-
if (navigationKey !== currentNavigationKey) {
|
|
39
|
-
currentNavigationKey = navigationKey
|
|
40
|
-
firedPageIds.clear()
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function shouldFirePixel(
|
|
45
|
-
navigationKey: string,
|
|
46
|
-
pageId: string
|
|
47
|
-
): boolean {
|
|
48
|
-
observeNavigation(navigationKey)
|
|
49
|
-
if (firedPageIds.has(pageId)) {
|
|
50
|
-
return false
|
|
51
|
-
}
|
|
52
|
-
firedPageIds.add(pageId)
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Track a mounted Weaverse instance; returns the unmount cleanup. When the
|
|
58
|
-
* last instance unmounts, the navigation state is forgotten so a Back that
|
|
59
|
-
* restores the same history entry counts again even if no instance survived
|
|
60
|
-
* to observe the detour.
|
|
61
|
-
*/
|
|
62
|
-
export function registerPixelInstance(): () => void {
|
|
63
|
-
mountedInstances += 1
|
|
64
|
-
return () => {
|
|
65
|
-
mountedInstances -= 1
|
|
66
|
-
if (mountedInstances === 0) {
|
|
67
|
-
currentNavigationKey = null
|
|
68
|
-
firedPageIds.clear()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|