@contentful/experiences-live-preview 0.1.8
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/CHANGELOG.md +68 -0
- package/README.md +138 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -0
- package/dist/experience-payload.d.ts +7 -0
- package/dist/experience-payload.js +20 -0
- package/dist/experience-payload.js.map +1 -0
- package/dist/fetch-preview-session.d.ts +28 -0
- package/dist/fetch-preview-session.js +55 -0
- package/dist/fetch-preview-session.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/live-preview-client.d.ts +19 -0
- package/dist/live-preview-client.js +73 -0
- package/dist/live-preview-client.js.map +1 -0
- package/dist/preview-session-url.d.ts +15 -0
- package/dist/preview-session-url.js +44 -0
- package/dist/preview-session-url.js.map +1 -0
- package/dist/preview-session.d.ts +27 -0
- package/dist/preview-session.js +74 -0
- package/dist/preview-session.js.map +1 -0
- package/dist/preview-status.d.ts +8 -0
- package/dist/preview-status.js +18 -0
- package/dist/preview-status.js.map +1 -0
- package/dist/websocket.d.ts +31 -0
- package/dist/websocket.js +128 -0
- package/dist/websocket.js.map +1 -0
- package/package.json +36 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
## 0.1.8 (2026-09-22)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated client to 0.4.8
|
|
6
|
+
|
|
7
|
+
## 0.1.7 (2026-09-22)
|
|
8
|
+
|
|
9
|
+
### 🧱 Updated Dependencies
|
|
10
|
+
|
|
11
|
+
- Updated client to 0.4.7
|
|
12
|
+
- Updated core to 0.8.6
|
|
13
|
+
|
|
14
|
+
## 0.1.6 (2026-09-22)
|
|
15
|
+
|
|
16
|
+
### 🩹 Fixes
|
|
17
|
+
|
|
18
|
+
- **live-preview:** accept payloads without viewports [SPA-5272] ([6db6fa7](https://github.com/contentful/experiences/commit/6db6fa7))
|
|
19
|
+
|
|
20
|
+
### 🧱 Updated Dependencies
|
|
21
|
+
|
|
22
|
+
- Updated client to 0.4.6
|
|
23
|
+
- Updated core to 0.8.5
|
|
24
|
+
|
|
25
|
+
## 0.1.5 (2026-09-22)
|
|
26
|
+
|
|
27
|
+
### 🚀 Features
|
|
28
|
+
|
|
29
|
+
- **live-preview:** expose connection errors [SPA-5151] ([#198](https://github.com/contentful/experiences/pull/198))
|
|
30
|
+
|
|
31
|
+
## 0.1.4 (2026-09-21)
|
|
32
|
+
|
|
33
|
+
### 🧱 Updated Dependencies
|
|
34
|
+
|
|
35
|
+
- Updated client to 0.4.5
|
|
36
|
+
- Updated core to 0.8.4
|
|
37
|
+
|
|
38
|
+
## 0.1.3 (2026-09-21)
|
|
39
|
+
|
|
40
|
+
### 🧱 Updated Dependencies
|
|
41
|
+
|
|
42
|
+
- Updated client to 0.4.4
|
|
43
|
+
|
|
44
|
+
## 0.1.2 (2026-09-18)
|
|
45
|
+
|
|
46
|
+
### 🚀 Features
|
|
47
|
+
|
|
48
|
+
- **live-preview:** support resource resolution [SPA-5369] ([#193](https://github.com/contentful/experiences/pull/193))
|
|
49
|
+
|
|
50
|
+
## 0.1.1 (2026-09-11)
|
|
51
|
+
|
|
52
|
+
### 🚀 Features
|
|
53
|
+
|
|
54
|
+
- **live-preview:** expose preview status helper [SPA-5289] ([#170](https://github.com/contentful/experiences/pull/170))
|
|
55
|
+
|
|
56
|
+
### 🧱 Updated Dependencies
|
|
57
|
+
|
|
58
|
+
- Updated core to 0.8.3
|
|
59
|
+
|
|
60
|
+
## 0.1.0 (2026-09-10)
|
|
61
|
+
|
|
62
|
+
### 🚀 Features
|
|
63
|
+
|
|
64
|
+
- **live-preview:** add live preview client [SPA-5151] ([#157](https://github.com/contentful/experiences/pull/157))
|
|
65
|
+
|
|
66
|
+
### 🧱 Updated Dependencies
|
|
67
|
+
|
|
68
|
+
- Updated core to 0.8.2
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @contentful/experiences-live-preview
|
|
2
|
+
|
|
3
|
+
Optional live preview for Contentful Experiences.
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
type PreviewSessionOptions = {
|
|
9
|
+
spaceId: string;
|
|
10
|
+
environmentId: string;
|
|
11
|
+
previewToken?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
resourceResolution?: string;
|
|
14
|
+
sessionHost?: string;
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type PreviewSessionExperienceOptions = {
|
|
19
|
+
spaceId: string;
|
|
20
|
+
environmentId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
resourceResolution?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type PreviewSessionClientOptions = {
|
|
26
|
+
previewToken: string;
|
|
27
|
+
host?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type PreviewSessionResolveOptions = {
|
|
31
|
+
config: ResolverConfig;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
initialViewportId?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
fetchPreviewSession(
|
|
38
|
+
previewSessionOptions: PreviewSessionExperienceOptions,
|
|
39
|
+
clientOptions: PreviewSessionClientOptions,
|
|
40
|
+
resolveOptions: PreviewSessionResolveOptions,
|
|
41
|
+
): Promise<PortableRenderPlan>;
|
|
42
|
+
|
|
43
|
+
PreviewSessionFetchError;
|
|
44
|
+
LivePreviewConnectionError;
|
|
45
|
+
NotFoundError;
|
|
46
|
+
PREVIEW_WEBSOCKET_HOST;
|
|
47
|
+
|
|
48
|
+
type LivePreviewResult = {
|
|
49
|
+
data: ExperiencePayload | undefined;
|
|
50
|
+
error: Error | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type LivePreviewClient = {
|
|
54
|
+
getResult(): LivePreviewResult;
|
|
55
|
+
subscribe(listener: () => void): () => void;
|
|
56
|
+
subscribeStatus(listener: (status: LivePreviewStatus) => void): () => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
createLivePreviewClient(
|
|
60
|
+
previewSessionOptions: PreviewSessionOptions,
|
|
61
|
+
initialPayload?: ExperiencePayload,
|
|
62
|
+
): LivePreviewClient;
|
|
63
|
+
|
|
64
|
+
sendPreviewStatus(status: LivePreviewStatus): void;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use `fetchPreviewSession` for the initial render when the Contentful app
|
|
68
|
+
provides a `preview_session_id`. It fetches the current Preview Session
|
|
69
|
+
snapshot and resolves it into a `PortableRenderPlan`. Use `createLivePreviewClient`
|
|
70
|
+
to receive subsequent WebSocket updates.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { fetchPreviewSession } from '@contentful/experiences-live-preview';
|
|
74
|
+
|
|
75
|
+
const plan = await fetchPreviewSession(
|
|
76
|
+
{ spaceId, environmentId, sessionId },
|
|
77
|
+
{ previewToken },
|
|
78
|
+
{ config: experienceConfig }
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The request sends `previewToken` as a Bearer token in the `Authorization`
|
|
83
|
+
header. `host` selects a compatible Preview Session API host and defaults to
|
|
84
|
+
the production host. Pass the same encoded `resourceResolution` value in the
|
|
85
|
+
`previewSessionOptions` passed to `fetchPreviewSession` and
|
|
86
|
+
`createLivePreviewClient` when the session references resources from other
|
|
87
|
+
spaces. A missing session throws `NotFoundError`. Other request and response
|
|
88
|
+
failures throw `PreviewSessionFetchError`.
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
`createLivePreviewClient` returns a data source for a Preview Session. It opens
|
|
93
|
+
the socket when the first listener subscribes and publishes each valid `next`
|
|
94
|
+
payload as received. If you pass `initialPayload`, `getResult().data` returns
|
|
95
|
+
it until a valid update arrives. Without `initialPayload`, the data starts as
|
|
96
|
+
`undefined`.
|
|
97
|
+
|
|
98
|
+
`getResult().error` is set when the live-preview connection fails. The last
|
|
99
|
+
valid data remains available in `getResult().data`.
|
|
100
|
+
|
|
101
|
+
A valid payload may omit the deprecated `viewports` field while the APIs migrate
|
|
102
|
+
to flat design-property values.
|
|
103
|
+
|
|
104
|
+
`sessionId` and `previewToken` are optional. The package opens a socket only when
|
|
105
|
+
both values are provided. The caller supplies the session ID through
|
|
106
|
+
`PreviewSessionOptions`.
|
|
107
|
+
|
|
108
|
+
`sessionHost` is an optional WebSocket URL for the Preview Session service. It
|
|
109
|
+
defaults to the production Contentful Session service. The SDK uses the URL as
|
|
110
|
+
supplied, appends the subscription route, and sends `previewToken` as the
|
|
111
|
+
encoded `access_token` query parameter. When provided, `resourceResolution` is
|
|
112
|
+
sent as the encoded `resource_resolution` query parameter.
|
|
113
|
+
|
|
114
|
+
When you use the package directly in a Contentful preview, connect the client's
|
|
115
|
+
status subscription to `sendPreviewStatus`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createLivePreviewClient, sendPreviewStatus } from '@contentful/experiences-live-preview';
|
|
119
|
+
|
|
120
|
+
const client = createLivePreviewClient(previewSessionOptions);
|
|
121
|
+
const unsubscribeStatus = client.subscribeStatus(sendPreviewStatus);
|
|
122
|
+
const unsubscribe = client.subscribe(() => {
|
|
123
|
+
const experience = client.getResult().data;
|
|
124
|
+
if (experience) updatePreview(experience);
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This tells the Contentful app how to coordinate updates with the SDK. The
|
|
129
|
+
subscription is required for this integration. Framework adapters set it up for
|
|
130
|
+
you. Keep both unsubscribe functions and call them when the preview no longer
|
|
131
|
+
uses the client.
|
|
132
|
+
|
|
133
|
+
A valid `next` message replaces the current data atomically. The client keeps
|
|
134
|
+
the last valid data when it receives malformed messages, server errors, or
|
|
135
|
+
transport interruptions. Unknown message types are ignored. The `next` payload
|
|
136
|
+
contains an `Experience`. Adapters can build on this source and reuse its socket
|
|
137
|
+
and message handling. Other frameworks can use the same framework-neutral
|
|
138
|
+
client.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by `fetchPreviewSession` for a Preview Session fetch failure other
|
|
3
|
+
* than a missing session.
|
|
4
|
+
*/
|
|
5
|
+
declare class PreviewSessionFetchError extends Error {
|
|
6
|
+
readonly spaceId: string;
|
|
7
|
+
readonly environmentId: string;
|
|
8
|
+
readonly sessionId: string;
|
|
9
|
+
constructor(message: string, options: {
|
|
10
|
+
spaceId: string;
|
|
11
|
+
environmentId: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
declare class LivePreviewConnectionError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { LivePreviewConnectionError, PreviewSessionFetchError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class PreviewSessionFetchError extends Error {
|
|
2
|
+
spaceId;
|
|
3
|
+
environmentId;
|
|
4
|
+
sessionId;
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
7
|
+
this.name = "PreviewSessionFetchError";
|
|
8
|
+
this.spaceId = options.spaceId;
|
|
9
|
+
this.environmentId = options.environmentId;
|
|
10
|
+
this.sessionId = options.sessionId;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
class LivePreviewConnectionError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Live Preview connection failed.");
|
|
16
|
+
this.name = "LivePreviewConnectionError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
LivePreviewConnectionError,
|
|
21
|
+
PreviewSessionFetchError
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Thrown by `fetchPreviewSession` for a Preview Session fetch failure other\n * than a missing session.\n */\nexport class PreviewSessionFetchError extends Error {\n readonly spaceId: string;\n readonly environmentId: string;\n readonly sessionId: string;\n\n constructor(\n message: string,\n options: {\n spaceId: string;\n environmentId: string;\n sessionId: string;\n cause?: unknown;\n }\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'PreviewSessionFetchError';\n this.spaceId = options.spaceId;\n this.environmentId = options.environmentId;\n this.sessionId = options.sessionId;\n }\n}\n\nexport class LivePreviewConnectionError extends Error {\n constructor() {\n super('Live Preview connection failed.');\n this.name = 'LivePreviewConnectionError';\n }\n}\n"],"mappings":"AAIO,MAAM,iCAAiC,MAAM;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YACE,SACA,SAMA;AACA,UAAM,SAAS,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,MAAS;AACjF,SAAK,OAAO;AACZ,SAAK,UAAU,QAAQ;AACvB,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,YAAY,QAAQ;AAAA,EAC3B;AACF;AAEO,MAAM,mCAAmC,MAAM;AAAA,EACpD,cAAc;AACZ,UAAM,iCAAiC;AACvC,SAAK,OAAO;AAAA,EACd;AACF;","names":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExperiencePayload } from '@contentful/experiences-sdk-core';
|
|
2
|
+
|
|
3
|
+
declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
4
|
+
declare function isExperiencePayload(value: unknown): value is ExperiencePayload;
|
|
5
|
+
declare function isErrorPayload(value: unknown): boolean;
|
|
6
|
+
|
|
7
|
+
export { isErrorPayload, isExperiencePayload, isRecord };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null;
|
|
3
|
+
}
|
|
4
|
+
function isExperiencePayload(value) {
|
|
5
|
+
if (!isRecord(value) || !Array.isArray(value.nodes) || value.viewports !== void 0 && !Array.isArray(value.viewports)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const sys = value.sys;
|
|
9
|
+
return isRecord(sys) && !Array.isArray(sys) && sys.type === "Experience";
|
|
10
|
+
}
|
|
11
|
+
function isErrorPayload(value) {
|
|
12
|
+
if (!isRecord(value) || !isRecord(value.sys) || Array.isArray(value.sys)) return false;
|
|
13
|
+
return value.sys.type === "Error" && typeof value.sys.id === "string";
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
isErrorPayload,
|
|
17
|
+
isExperiencePayload,
|
|
18
|
+
isRecord
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=experience-payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/experience-payload.ts"],"sourcesContent":["import type { ExperiencePayload } from '@contentful/experiences-sdk-core';\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport function isExperiencePayload(value: unknown): value is ExperiencePayload {\n if (\n !isRecord(value) ||\n !Array.isArray(value.nodes) ||\n (value.viewports !== undefined && !Array.isArray(value.viewports))\n ) {\n return false;\n }\n\n const sys = value.sys;\n return isRecord(sys) && !Array.isArray(sys) && sys.type === 'Experience';\n}\n\nexport function isErrorPayload(value: unknown): boolean {\n if (!isRecord(value) || !isRecord(value.sys) || Array.isArray(value.sys)) return false;\n return value.sys.type === 'Error' && typeof value.sys.id === 'string';\n}\n"],"mappings":"AAEO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEO,SAAS,oBAAoB,OAA4C;AAC9E,MACE,CAAC,SAAS,KAAK,KACf,CAAC,MAAM,QAAQ,MAAM,KAAK,KACzB,MAAM,cAAc,UAAa,CAAC,MAAM,QAAQ,MAAM,SAAS,GAChE;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,MAAM;AAClB,SAAO,SAAS,GAAG,KAAK,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,SAAS;AAC9D;AAEO,SAAS,eAAe,OAAyB;AACtD,MAAI,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,MAAM,GAAG,KAAK,MAAM,QAAQ,MAAM,GAAG,EAAG,QAAO;AACjF,SAAO,MAAM,IAAI,SAAS,WAAW,OAAO,MAAM,IAAI,OAAO;AAC/D;","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ResolverConfig, PortableRenderPlan } from '@contentful/experiences-sdk-core';
|
|
2
|
+
|
|
3
|
+
type PreviewSessionExperienceOptions = {
|
|
4
|
+
spaceId: string;
|
|
5
|
+
environmentId: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Opaque encoded resource-resolution value for referenced spaces.
|
|
9
|
+
*/
|
|
10
|
+
resourceResolution?: string;
|
|
11
|
+
};
|
|
12
|
+
type PreviewSessionClientOptions = {
|
|
13
|
+
previewToken: string;
|
|
14
|
+
host?: string;
|
|
15
|
+
};
|
|
16
|
+
type PreviewSessionResolveOptions = {
|
|
17
|
+
config: ResolverConfig;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
initialViewportId?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Fetches the current hydrated Experience for a Preview Session and resolves
|
|
24
|
+
* it into the renderer's PortableRenderPlan.
|
|
25
|
+
*/
|
|
26
|
+
declare function fetchPreviewSession(previewSessionOptions: PreviewSessionExperienceOptions, clientOptions: PreviewSessionClientOptions, resolveOptions: PreviewSessionResolveOptions): Promise<PortableRenderPlan>;
|
|
27
|
+
|
|
28
|
+
export { type PreviewSessionClientOptions, type PreviewSessionExperienceOptions, type PreviewSessionResolveOptions, fetchPreviewSession };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createClient, NotFoundError, PREVIEW_HOST } from "@contentful/experiences-client";
|
|
2
|
+
import {
|
|
3
|
+
createDebugLogger,
|
|
4
|
+
resolveExperience
|
|
5
|
+
} from "@contentful/experiences-sdk-core";
|
|
6
|
+
import { PreviewSessionFetchError } from "./errors.js";
|
|
7
|
+
import { isExperiencePayload } from "./experience-payload.js";
|
|
8
|
+
import { previewSessionGetExperienceUrl } from "./preview-session-url.js";
|
|
9
|
+
async function fetchPreviewSession(previewSessionOptions, clientOptions, resolveOptions) {
|
|
10
|
+
const { spaceId, environmentId, sessionId, resourceResolution } = previewSessionOptions;
|
|
11
|
+
const { previewToken, host } = clientOptions;
|
|
12
|
+
const { config, metadata, debug, initialViewportId } = resolveOptions;
|
|
13
|
+
const log = createDebugLogger(debug, "live-preview");
|
|
14
|
+
const client = createClient({
|
|
15
|
+
accessToken: previewToken,
|
|
16
|
+
host: host ?? PREVIEW_HOST
|
|
17
|
+
});
|
|
18
|
+
log.log("fetching preview session experience", { spaceId, environmentId, sessionId });
|
|
19
|
+
let payload;
|
|
20
|
+
try {
|
|
21
|
+
const response = await client.fetch(
|
|
22
|
+
previewSessionGetExperienceUrl({ ...previewSessionOptions, resourceResolution })
|
|
23
|
+
);
|
|
24
|
+
if (response.status === 404) {
|
|
25
|
+
throw new NotFoundError();
|
|
26
|
+
}
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
const responsePayload = await response.json();
|
|
31
|
+
if (!isExperiencePayload(responsePayload)) {
|
|
32
|
+
throw new Error("Response was not a valid Experience payload");
|
|
33
|
+
}
|
|
34
|
+
payload = responsePayload;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof NotFoundError) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
40
|
+
throw new PreviewSessionFetchError(
|
|
41
|
+
`Failed to fetch Preview Session "${sessionId}" (space "${spaceId}", environment "${environmentId}"): ${reason}`,
|
|
42
|
+
{ spaceId, environmentId, sessionId, cause: error }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
log.lazy("received raw payload", () => payload);
|
|
46
|
+
return resolveExperience(payload, config, {
|
|
47
|
+
metadata,
|
|
48
|
+
debug,
|
|
49
|
+
initialViewportId
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
fetchPreviewSession
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=fetch-preview-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fetch-preview-session.ts"],"sourcesContent":["import { createClient, NotFoundError, PREVIEW_HOST } from '@contentful/experiences-client';\nimport {\n createDebugLogger,\n resolveExperience,\n type ExperiencePayload,\n type PortableRenderPlan,\n type ResolverConfig,\n} from '@contentful/experiences-sdk-core';\nimport { PreviewSessionFetchError } from './errors.js';\nimport { isExperiencePayload } from './experience-payload.js';\nimport { previewSessionGetExperienceUrl } from './preview-session-url.js';\n\nexport type PreviewSessionExperienceOptions = {\n spaceId: string;\n environmentId: string;\n sessionId: string;\n /**\n * Opaque encoded resource-resolution value for referenced spaces.\n */\n resourceResolution?: string;\n};\n\nexport type PreviewSessionClientOptions = {\n previewToken: string;\n host?: string;\n};\n\nexport type PreviewSessionResolveOptions = {\n config: ResolverConfig;\n metadata?: Record<string, unknown>;\n debug?: boolean;\n initialViewportId?: string;\n};\n\n/**\n * Fetches the current hydrated Experience for a Preview Session and resolves\n * it into the renderer's PortableRenderPlan.\n */\nexport async function fetchPreviewSession(\n previewSessionOptions: PreviewSessionExperienceOptions,\n clientOptions: PreviewSessionClientOptions,\n resolveOptions: PreviewSessionResolveOptions\n): Promise<PortableRenderPlan> {\n const { spaceId, environmentId, sessionId, resourceResolution } = previewSessionOptions;\n const { previewToken, host } = clientOptions;\n const { config, metadata, debug, initialViewportId } = resolveOptions;\n const log = createDebugLogger(debug, 'live-preview');\n const client = createClient({\n accessToken: previewToken,\n host: host ?? PREVIEW_HOST,\n });\n\n log.log('fetching preview session experience', { spaceId, environmentId, sessionId });\n\n let payload: ExperiencePayload;\n try {\n const response = await client.fetch(\n previewSessionGetExperienceUrl({ ...previewSessionOptions, resourceResolution })\n );\n if (response.status === 404) {\n throw new NotFoundError();\n }\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const responsePayload: unknown = await response.json();\n if (!isExperiencePayload(responsePayload)) {\n throw new Error('Response was not a valid Experience payload');\n }\n payload = responsePayload;\n } catch (error) {\n if (error instanceof NotFoundError) {\n throw error;\n }\n const reason = error instanceof Error ? error.message : String(error);\n throw new PreviewSessionFetchError(\n `Failed to fetch Preview Session \"${sessionId}\" (space \"${spaceId}\", environment ` +\n `\"${environmentId}\"): ${reason}`,\n { spaceId, environmentId, sessionId, cause: error }\n );\n }\n\n log.lazy('received raw payload', () => payload);\n\n return resolveExperience(payload, config, {\n metadata,\n debug,\n initialViewportId,\n });\n}\n"],"mappings":"AAAA,SAAS,cAAc,eAAe,oBAAoB;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AACP,SAAS,gCAAgC;AACzC,SAAS,2BAA2B;AACpC,SAAS,sCAAsC;AA4B/C,eAAsB,oBACpB,uBACA,eACA,gBAC6B;AAC7B,QAAM,EAAE,SAAS,eAAe,WAAW,mBAAmB,IAAI;AAClE,QAAM,EAAE,cAAc,KAAK,IAAI;AAC/B,QAAM,EAAE,QAAQ,UAAU,OAAO,kBAAkB,IAAI;AACvD,QAAM,MAAM,kBAAkB,OAAO,cAAc;AACnD,QAAM,SAAS,aAAa;AAAA,IAC1B,aAAa;AAAA,IACb,MAAM,QAAQ;AAAA,EAChB,CAAC;AAED,MAAI,IAAI,uCAAuC,EAAE,SAAS,eAAe,UAAU,CAAC;AAEpF,MAAI;AACJ,MAAI;AACF,UAAM,WAAW,MAAM,OAAO;AAAA,MAC5B,+BAA+B,EAAE,GAAG,uBAAuB,mBAAmB,CAAC;AAAA,IACjF;AACA,QAAI,SAAS,WAAW,KAAK;AAC3B,YAAM,IAAI,cAAc;AAAA,IAC1B;AACA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,QAAQ,SAAS,MAAM,EAAE;AAAA,IAC3C;AAEA,UAAM,kBAA2B,MAAM,SAAS,KAAK;AACrD,QAAI,CAAC,oBAAoB,eAAe,GAAG;AACzC,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC/D;AACA,cAAU;AAAA,EACZ,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,YAAM;AAAA,IACR;AACA,UAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,UAAM,IAAI;AAAA,MACR,oCAAoC,SAAS,aAAa,OAAO,mBAC3D,aAAa,OAAO,MAAM;AAAA,MAChC,EAAE,SAAS,eAAe,WAAW,OAAO,MAAM;AAAA,IACpD;AAAA,EACF;AAEA,MAAI,KAAK,wBAAwB,MAAM,OAAO;AAE9C,SAAO,kBAAkB,SAAS,QAAQ;AAAA,IACxC;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LivePreviewClient, LivePreviewResult, LivePreviewStatus, createLivePreviewClient } from './live-preview-client.js';
|
|
2
|
+
export { sendPreviewStatus } from './preview-status.js';
|
|
3
|
+
export { PreviewSessionClientOptions, PreviewSessionExperienceOptions, PreviewSessionResolveOptions, fetchPreviewSession } from './fetch-preview-session.js';
|
|
4
|
+
export { LivePreviewConnectionError, PreviewSessionFetchError } from './errors.js';
|
|
5
|
+
export { NotFoundError, PREVIEW_WEBSOCKET_HOST } from '@contentful/experiences-client';
|
|
6
|
+
export { PreviewSessionOptions } from './preview-session.js';
|
|
7
|
+
import '@contentful/experiences-sdk-core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createLivePreviewClient } from "./live-preview-client.js";
|
|
2
|
+
import { sendPreviewStatus } from "./preview-status.js";
|
|
3
|
+
import { fetchPreviewSession } from "./fetch-preview-session.js";
|
|
4
|
+
import { LivePreviewConnectionError, PreviewSessionFetchError } from "./errors.js";
|
|
5
|
+
import { PREVIEW_WEBSOCKET_HOST } from "@contentful/experiences-client";
|
|
6
|
+
import { NotFoundError } from "@contentful/experiences-client";
|
|
7
|
+
export {
|
|
8
|
+
LivePreviewConnectionError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
PREVIEW_WEBSOCKET_HOST,
|
|
11
|
+
PreviewSessionFetchError,
|
|
12
|
+
createLivePreviewClient,
|
|
13
|
+
fetchPreviewSession,
|
|
14
|
+
sendPreviewStatus
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { createLivePreviewClient } from './live-preview-client.js';\nexport { sendPreviewStatus } from './preview-status.js';\nexport { fetchPreviewSession } from './fetch-preview-session.js';\nexport { LivePreviewConnectionError, PreviewSessionFetchError } from './errors.js';\nexport { PREVIEW_WEBSOCKET_HOST } from '@contentful/experiences-client';\nexport type {\n LivePreviewClient,\n LivePreviewResult,\n LivePreviewStatus,\n} from './live-preview-client.js';\nexport type { PreviewSessionOptions } from './preview-session.js';\nexport type {\n PreviewSessionClientOptions,\n PreviewSessionExperienceOptions,\n PreviewSessionResolveOptions,\n} from './fetch-preview-session.js';\nexport { NotFoundError } from '@contentful/experiences-client';\n"],"mappings":"AAAA,SAAS,+BAA+B;AACxC,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B,gCAAgC;AACrE,SAAS,8BAA8B;AAYvC,SAAS,qBAAqB;","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExperiencePayload } from '@contentful/experiences-sdk-core';
|
|
2
|
+
import { PreviewSessionOptions } from './preview-session.js';
|
|
3
|
+
import './errors.js';
|
|
4
|
+
|
|
5
|
+
type LivePreviewStatus = 'live' | 'static';
|
|
6
|
+
type LivePreviewResult = {
|
|
7
|
+
readonly data: ExperiencePayload | undefined;
|
|
8
|
+
readonly error: Error | undefined;
|
|
9
|
+
};
|
|
10
|
+
type LivePreviewClient = {
|
|
11
|
+
getResult(): LivePreviewResult;
|
|
12
|
+
/** @deprecated Use getResult().data instead. */
|
|
13
|
+
getSnapshot(): ExperiencePayload | undefined;
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
subscribeStatus(listener: (status: LivePreviewStatus) => void): () => void;
|
|
16
|
+
};
|
|
17
|
+
declare function createLivePreviewClient(previewSessionOptions: PreviewSessionOptions, initialPayload?: ExperiencePayload): LivePreviewClient;
|
|
18
|
+
|
|
19
|
+
export { type LivePreviewClient, type LivePreviewResult, type LivePreviewStatus, createLivePreviewClient };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { subscribeToPreviewSession } from "./preview-session.js";
|
|
2
|
+
function createLivePreviewClient(previewSessionOptions, initialPayload) {
|
|
3
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
const statusListeners = /* @__PURE__ */ new Set();
|
|
5
|
+
const notifyListeners = () => {
|
|
6
|
+
for (const { handler } of [...listeners]) handler();
|
|
7
|
+
};
|
|
8
|
+
const hasLivePreviewOptions = previewSessionOptions.sessionId !== void 0 && previewSessionOptions.previewToken !== void 0;
|
|
9
|
+
let currentStatus = hasLivePreviewOptions ? void 0 : "static";
|
|
10
|
+
let currentResult = { data: initialPayload, error: void 0 };
|
|
11
|
+
let unsubscribeFromSession;
|
|
12
|
+
const updateData = (data) => {
|
|
13
|
+
currentResult = { ...currentResult, data };
|
|
14
|
+
notifyListeners();
|
|
15
|
+
};
|
|
16
|
+
const updateError = (error) => {
|
|
17
|
+
currentResult = { ...currentResult, error };
|
|
18
|
+
updateStatus("static");
|
|
19
|
+
notifyListeners();
|
|
20
|
+
};
|
|
21
|
+
const updateStatus = (status) => {
|
|
22
|
+
if (status === "live" && currentResult.error !== void 0) {
|
|
23
|
+
currentResult = { ...currentResult, error: void 0 };
|
|
24
|
+
notifyListeners();
|
|
25
|
+
}
|
|
26
|
+
if (currentStatus === status) return;
|
|
27
|
+
currentStatus = status;
|
|
28
|
+
for (const { handler } of [...statusListeners]) handler(status);
|
|
29
|
+
};
|
|
30
|
+
const closeSession = () => {
|
|
31
|
+
unsubscribeFromSession?.();
|
|
32
|
+
unsubscribeFromSession = void 0;
|
|
33
|
+
if (hasLivePreviewOptions) currentStatus = void 0;
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
getResult: () => currentResult,
|
|
37
|
+
getSnapshot: () => currentResult.data,
|
|
38
|
+
subscribe(listener) {
|
|
39
|
+
const subscription = { handler: listener };
|
|
40
|
+
const isFirstSubscriber = listeners.size === 0;
|
|
41
|
+
listeners.add(subscription);
|
|
42
|
+
if (isFirstSubscriber) {
|
|
43
|
+
try {
|
|
44
|
+
unsubscribeFromSession = subscribeToPreviewSession(previewSessionOptions, {
|
|
45
|
+
onOpen: () => updateStatus("live"),
|
|
46
|
+
onError: updateError,
|
|
47
|
+
onUpdate: updateData
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
listeners.delete(subscription);
|
|
51
|
+
closeSession();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return () => {
|
|
56
|
+
if (!listeners.delete(subscription)) return;
|
|
57
|
+
if (listeners.size === 0) closeSession();
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
subscribeStatus(listener) {
|
|
61
|
+
const subscription = { handler: listener };
|
|
62
|
+
statusListeners.add(subscription);
|
|
63
|
+
if (currentStatus !== void 0) listener(currentStatus);
|
|
64
|
+
return () => {
|
|
65
|
+
statusListeners.delete(subscription);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
createLivePreviewClient
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=live-preview-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/live-preview-client.ts"],"sourcesContent":["import type { ExperiencePayload } from '@contentful/experiences-sdk-core';\nimport { subscribeToPreviewSession, type PreviewSessionOptions } from './preview-session.js';\n\nexport type LivePreviewStatus = 'live' | 'static';\n\nexport type LivePreviewResult = {\n readonly data: ExperiencePayload | undefined;\n readonly error: Error | undefined;\n};\n\nexport type LivePreviewClient = {\n getResult(): LivePreviewResult;\n /** @deprecated Use getResult().data instead. */\n getSnapshot(): ExperiencePayload | undefined;\n subscribe(listener: () => void): () => void;\n subscribeStatus(listener: (status: LivePreviewStatus) => void): () => void;\n};\n\nexport function createLivePreviewClient(\n previewSessionOptions: PreviewSessionOptions,\n initialPayload?: ExperiencePayload\n): LivePreviewClient {\n const listeners = new Set<{ handler: () => void }>();\n const statusListeners = new Set<{ handler: (status: LivePreviewStatus) => void }>();\n const notifyListeners = (): void => {\n for (const { handler } of [...listeners]) handler();\n };\n\n const hasLivePreviewOptions =\n previewSessionOptions.sessionId !== undefined &&\n previewSessionOptions.previewToken !== undefined;\n let currentStatus: LivePreviewStatus | undefined = hasLivePreviewOptions ? undefined : 'static';\n let currentResult: LivePreviewResult = { data: initialPayload, error: undefined };\n let unsubscribeFromSession: (() => void) | undefined;\n\n const updateData = (data: ExperiencePayload): void => {\n currentResult = { ...currentResult, data };\n notifyListeners();\n };\n\n const updateError = (error: Error): void => {\n currentResult = { ...currentResult, error };\n updateStatus('static');\n notifyListeners();\n };\n\n const updateStatus = (status: LivePreviewStatus): void => {\n if (status === 'live' && currentResult.error !== undefined) {\n currentResult = { ...currentResult, error: undefined };\n notifyListeners();\n }\n if (currentStatus === status) return;\n currentStatus = status;\n for (const { handler } of [...statusListeners]) handler(status);\n };\n\n const closeSession = (): void => {\n unsubscribeFromSession?.();\n unsubscribeFromSession = undefined;\n if (hasLivePreviewOptions) currentStatus = undefined;\n };\n\n return {\n getResult: () => currentResult,\n getSnapshot: () => currentResult.data,\n subscribe(listener) {\n const subscription = { handler: listener };\n const isFirstSubscriber = listeners.size === 0;\n listeners.add(subscription);\n if (isFirstSubscriber) {\n try {\n unsubscribeFromSession = subscribeToPreviewSession(previewSessionOptions, {\n onOpen: () => updateStatus('live'),\n onError: updateError,\n onUpdate: updateData,\n });\n } catch (error: unknown) {\n listeners.delete(subscription);\n closeSession();\n throw error;\n }\n }\n\n return () => {\n if (!listeners.delete(subscription)) return;\n if (listeners.size === 0) closeSession();\n };\n },\n subscribeStatus(listener) {\n const subscription = { handler: listener };\n statusListeners.add(subscription);\n if (currentStatus !== undefined) listener(currentStatus);\n\n return () => {\n statusListeners.delete(subscription);\n };\n },\n };\n}\n"],"mappings":"AACA,SAAS,iCAA6D;AAiB/D,SAAS,wBACd,uBACA,gBACmB;AACnB,QAAM,YAAY,oBAAI,IAA6B;AACnD,QAAM,kBAAkB,oBAAI,IAAsD;AAClF,QAAM,kBAAkB,MAAY;AAClC,eAAW,EAAE,QAAQ,KAAK,CAAC,GAAG,SAAS,EAAG,SAAQ;AAAA,EACpD;AAEA,QAAM,wBACJ,sBAAsB,cAAc,UACpC,sBAAsB,iBAAiB;AACzC,MAAI,gBAA+C,wBAAwB,SAAY;AACvF,MAAI,gBAAmC,EAAE,MAAM,gBAAgB,OAAO,OAAU;AAChF,MAAI;AAEJ,QAAM,aAAa,CAAC,SAAkC;AACpD,oBAAgB,EAAE,GAAG,eAAe,KAAK;AACzC,oBAAgB;AAAA,EAClB;AAEA,QAAM,cAAc,CAAC,UAAuB;AAC1C,oBAAgB,EAAE,GAAG,eAAe,MAAM;AAC1C,iBAAa,QAAQ;AACrB,oBAAgB;AAAA,EAClB;AAEA,QAAM,eAAe,CAAC,WAAoC;AACxD,QAAI,WAAW,UAAU,cAAc,UAAU,QAAW;AAC1D,sBAAgB,EAAE,GAAG,eAAe,OAAO,OAAU;AACrD,sBAAgB;AAAA,IAClB;AACA,QAAI,kBAAkB,OAAQ;AAC9B,oBAAgB;AAChB,eAAW,EAAE,QAAQ,KAAK,CAAC,GAAG,eAAe,EAAG,SAAQ,MAAM;AAAA,EAChE;AAEA,QAAM,eAAe,MAAY;AAC/B,6BAAyB;AACzB,6BAAyB;AACzB,QAAI,sBAAuB,iBAAgB;AAAA,EAC7C;AAEA,SAAO;AAAA,IACL,WAAW,MAAM;AAAA,IACjB,aAAa,MAAM,cAAc;AAAA,IACjC,UAAU,UAAU;AAClB,YAAM,eAAe,EAAE,SAAS,SAAS;AACzC,YAAM,oBAAoB,UAAU,SAAS;AAC7C,gBAAU,IAAI,YAAY;AAC1B,UAAI,mBAAmB;AACrB,YAAI;AACF,mCAAyB,0BAA0B,uBAAuB;AAAA,YACxE,QAAQ,MAAM,aAAa,MAAM;AAAA,YACjC,SAAS;AAAA,YACT,UAAU;AAAA,UACZ,CAAC;AAAA,QACH,SAAS,OAAgB;AACvB,oBAAU,OAAO,YAAY;AAC7B,uBAAa;AACb,gBAAM;AAAA,QACR;AAAA,MACF;AAEA,aAAO,MAAM;AACX,YAAI,CAAC,UAAU,OAAO,YAAY,EAAG;AACrC,YAAI,UAAU,SAAS,EAAG,cAAa;AAAA,MACzC;AAAA,IACF;AAAA,IACA,gBAAgB,UAAU;AACxB,YAAM,eAAe,EAAE,SAAS,SAAS;AACzC,sBAAgB,IAAI,YAAY;AAChC,UAAI,kBAAkB,OAAW,UAAS,aAAa;AAEvD,aAAO,MAAM;AACX,wBAAgB,OAAO,YAAY;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type PreviewSessionRouteOptions = {
|
|
2
|
+
spaceId: string;
|
|
3
|
+
environmentId: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
};
|
|
6
|
+
declare function previewSessionGetExperienceUrl({ resourceResolution, ...options }: PreviewSessionRouteOptions & {
|
|
7
|
+
resourceResolution?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
declare function previewSessionSubscribeUrl({ spaceId, environmentId, sessionId, sessionHost, previewToken, resourceResolution, }: PreviewSessionRouteOptions & {
|
|
10
|
+
sessionHost?: string;
|
|
11
|
+
previewToken: string;
|
|
12
|
+
resourceResolution?: string;
|
|
13
|
+
}): string;
|
|
14
|
+
|
|
15
|
+
export { previewSessionGetExperienceUrl, previewSessionSubscribeUrl };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PREVIEW_WEBSOCKET_HOST } from "@contentful/experiences-client";
|
|
2
|
+
function previewSessionPath(options) {
|
|
3
|
+
return [
|
|
4
|
+
"spaces",
|
|
5
|
+
encodeURIComponent(options.spaceId),
|
|
6
|
+
"environments",
|
|
7
|
+
encodeURIComponent(options.environmentId),
|
|
8
|
+
"preview_sessions",
|
|
9
|
+
encodeURIComponent(options.sessionId)
|
|
10
|
+
].join("/");
|
|
11
|
+
}
|
|
12
|
+
function previewSessionGetExperienceUrl({
|
|
13
|
+
resourceResolution,
|
|
14
|
+
...options
|
|
15
|
+
}) {
|
|
16
|
+
const path = `/${previewSessionPath(options)}/experience`;
|
|
17
|
+
if (resourceResolution === void 0) return path;
|
|
18
|
+
const searchParams = new globalThis.URLSearchParams({
|
|
19
|
+
resource_resolution: resourceResolution
|
|
20
|
+
});
|
|
21
|
+
return `${path}?${searchParams.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
function previewSessionSubscribeUrl({
|
|
24
|
+
spaceId,
|
|
25
|
+
environmentId,
|
|
26
|
+
sessionId,
|
|
27
|
+
sessionHost,
|
|
28
|
+
previewToken,
|
|
29
|
+
resourceResolution
|
|
30
|
+
}) {
|
|
31
|
+
const url = new globalThis.URL(sessionHost ?? PREVIEW_WEBSOCKET_HOST);
|
|
32
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
33
|
+
url.pathname = `${basePath}/${previewSessionPath({ spaceId, environmentId, sessionId })}/subscribe`;
|
|
34
|
+
url.searchParams.set("access_token", previewToken);
|
|
35
|
+
if (resourceResolution !== void 0) {
|
|
36
|
+
url.searchParams.set("resource_resolution", resourceResolution);
|
|
37
|
+
}
|
|
38
|
+
return url.toString();
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
previewSessionGetExperienceUrl,
|
|
42
|
+
previewSessionSubscribeUrl
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=preview-session-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preview-session-url.ts"],"sourcesContent":["import { PREVIEW_WEBSOCKET_HOST } from '@contentful/experiences-client';\n\ntype PreviewSessionRouteOptions = {\n spaceId: string;\n environmentId: string;\n sessionId: string;\n};\n\nfunction previewSessionPath(options: PreviewSessionRouteOptions): string {\n return [\n 'spaces',\n encodeURIComponent(options.spaceId),\n 'environments',\n encodeURIComponent(options.environmentId),\n 'preview_sessions',\n encodeURIComponent(options.sessionId),\n ].join('/');\n}\n\nexport function previewSessionGetExperienceUrl({\n resourceResolution,\n ...options\n}: PreviewSessionRouteOptions & { resourceResolution?: string }): string {\n const path = `/${previewSessionPath(options)}/experience`;\n if (resourceResolution === undefined) return path;\n\n const searchParams = new globalThis.URLSearchParams({\n resource_resolution: resourceResolution,\n });\n return `${path}?${searchParams.toString()}`;\n}\n\nexport function previewSessionSubscribeUrl({\n spaceId,\n environmentId,\n sessionId,\n sessionHost,\n previewToken,\n resourceResolution,\n}: PreviewSessionRouteOptions & {\n sessionHost?: string;\n previewToken: string;\n resourceResolution?: string;\n}): string {\n const url = new globalThis.URL(sessionHost ?? PREVIEW_WEBSOCKET_HOST);\n const basePath = url.pathname.replace(/\\/+$/, '');\n url.pathname = `${basePath}/${previewSessionPath({ spaceId, environmentId, sessionId })}/subscribe`;\n url.searchParams.set('access_token', previewToken);\n if (resourceResolution !== undefined) {\n url.searchParams.set('resource_resolution', resourceResolution);\n }\n\n return url.toString();\n}\n"],"mappings":"AAAA,SAAS,8BAA8B;AAQvC,SAAS,mBAAmB,SAA6C;AACvE,SAAO;AAAA,IACL;AAAA,IACA,mBAAmB,QAAQ,OAAO;AAAA,IAClC;AAAA,IACA,mBAAmB,QAAQ,aAAa;AAAA,IACxC;AAAA,IACA,mBAAmB,QAAQ,SAAS;AAAA,EACtC,EAAE,KAAK,GAAG;AACZ;AAEO,SAAS,+BAA+B;AAAA,EAC7C;AAAA,EACA,GAAG;AACL,GAAyE;AACvE,QAAM,OAAO,IAAI,mBAAmB,OAAO,CAAC;AAC5C,MAAI,uBAAuB,OAAW,QAAO;AAE7C,QAAM,eAAe,IAAI,WAAW,gBAAgB;AAAA,IAClD,qBAAqB;AAAA,EACvB,CAAC;AACD,SAAO,GAAG,IAAI,IAAI,aAAa,SAAS,CAAC;AAC3C;AAEO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,MAAM,IAAI,WAAW,IAAI,eAAe,sBAAsB;AACpE,QAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ,EAAE;AAChD,MAAI,WAAW,GAAG,QAAQ,IAAI,mBAAmB,EAAE,SAAS,eAAe,UAAU,CAAC,CAAC;AACvF,MAAI,aAAa,IAAI,gBAAgB,YAAY;AACjD,MAAI,uBAAuB,QAAW;AACpC,QAAI,aAAa,IAAI,uBAAuB,kBAAkB;AAAA,EAChE;AAEA,SAAO,IAAI,SAAS;AACtB;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ExperiencePayload } from '@contentful/experiences-sdk-core';
|
|
2
|
+
import { LivePreviewConnectionError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
type PreviewSessionOptions = {
|
|
5
|
+
spaceId: string;
|
|
6
|
+
environmentId: string;
|
|
7
|
+
previewToken?: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Opaque encoded resource-resolution value for referenced spaces.
|
|
11
|
+
*/
|
|
12
|
+
resourceResolution?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Custom WebSocket URL for Preview Session subscriptions (staging, proxy, per-region).
|
|
15
|
+
* Omit to use the production Preview Session WebSocket host.
|
|
16
|
+
*/
|
|
17
|
+
sessionHost?: string;
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
};
|
|
20
|
+
type PreviewSessionHandlers = {
|
|
21
|
+
onUpdate: (experience: ExperiencePayload) => void;
|
|
22
|
+
onOpen?: () => void;
|
|
23
|
+
onError?: (error: LivePreviewConnectionError) => void;
|
|
24
|
+
};
|
|
25
|
+
declare function subscribeToPreviewSession(options: PreviewSessionOptions, handlers: PreviewSessionHandlers): () => void;
|
|
26
|
+
|
|
27
|
+
export { type PreviewSessionOptions, subscribeToPreviewSession };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createDebugLogger } from "@contentful/experiences-sdk-core";
|
|
2
|
+
import { isErrorPayload, isExperiencePayload, isRecord } from "./experience-payload.js";
|
|
3
|
+
import { createWebSocketConnection } from "./websocket.js";
|
|
4
|
+
import { previewSessionSubscribeUrl } from "./preview-session-url.js";
|
|
5
|
+
import { LivePreviewConnectionError } from "./errors.js";
|
|
6
|
+
const RETRY_DELAYS_MS = [100, 500, 1e3];
|
|
7
|
+
function parseMessage(data) {
|
|
8
|
+
let message = data;
|
|
9
|
+
if (typeof data === "string") {
|
|
10
|
+
try {
|
|
11
|
+
message = JSON.parse(data);
|
|
12
|
+
} catch {
|
|
13
|
+
return { kind: "invalid" };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (!isRecord(message) || typeof message.type !== "string") {
|
|
17
|
+
return { kind: "invalid" };
|
|
18
|
+
}
|
|
19
|
+
if (message.type === "next") {
|
|
20
|
+
return isExperiencePayload(message.data) ? { kind: "next", payload: message.data } : { kind: "invalid" };
|
|
21
|
+
}
|
|
22
|
+
if (message.type === "error") {
|
|
23
|
+
return isErrorPayload(message.data) ? { kind: "error" } : { kind: "invalid" };
|
|
24
|
+
}
|
|
25
|
+
return { kind: "unknown" };
|
|
26
|
+
}
|
|
27
|
+
function isSessionEnded(event) {
|
|
28
|
+
if (event.code !== 1e3) return false;
|
|
29
|
+
const reason = event.reason.trim().toLowerCase();
|
|
30
|
+
return reason === "deleted" || reason === "expired";
|
|
31
|
+
}
|
|
32
|
+
function subscribeToPreviewSession(options, handlers) {
|
|
33
|
+
const log = createDebugLogger(options.debug, "live-preview");
|
|
34
|
+
const { sessionId, previewToken } = options;
|
|
35
|
+
const { onError, onOpen, onUpdate } = handlers;
|
|
36
|
+
if (sessionId === void 0 || previewToken === void 0) return () => void 0;
|
|
37
|
+
const connection = createWebSocketConnection({
|
|
38
|
+
url: previewSessionSubscribeUrl({ ...options, sessionId, previewToken }),
|
|
39
|
+
retry: (failureCount, event) => {
|
|
40
|
+
const sessionEnded = isSessionEnded(event);
|
|
41
|
+
const hasExhaustedRetries = failureCount >= RETRY_DELAYS_MS.length;
|
|
42
|
+
const shouldRetry = !hasExhaustedRetries && !sessionEnded;
|
|
43
|
+
if (hasExhaustedRetries || sessionEnded) {
|
|
44
|
+
onError?.(new LivePreviewConnectionError());
|
|
45
|
+
}
|
|
46
|
+
return shouldRetry;
|
|
47
|
+
},
|
|
48
|
+
retryDelay: (retryAttempt) => RETRY_DELAYS_MS[retryAttempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1] ?? 0,
|
|
49
|
+
onFailure: () => onError?.(new LivePreviewConnectionError())
|
|
50
|
+
});
|
|
51
|
+
const unsubscribeFromOpen = onOpen ? connection.onopen(() => onOpen()) : void 0;
|
|
52
|
+
const unsubscribe = connection.onmessage((event) => {
|
|
53
|
+
const message = parseMessage(event.data);
|
|
54
|
+
if (message.kind === "unknown") return;
|
|
55
|
+
if (message.kind === "error") {
|
|
56
|
+
log.log("Preview Session returned an error");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (message.kind === "invalid") {
|
|
60
|
+
log.log("ignored invalid Preview Session message");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
onUpdate(message.payload);
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
unsubscribeFromOpen?.();
|
|
67
|
+
unsubscribe();
|
|
68
|
+
connection.close();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
subscribeToPreviewSession
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=preview-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preview-session.ts"],"sourcesContent":["import { createDebugLogger, type ExperiencePayload } from '@contentful/experiences-sdk-core';\nimport { isErrorPayload, isExperiencePayload, isRecord } from './experience-payload.js';\nimport { createWebSocketConnection, type WebSocketCloseEvent } from './websocket.js';\nimport { previewSessionSubscribeUrl } from './preview-session-url.js';\nimport { LivePreviewConnectionError } from './errors.js';\n\nconst RETRY_DELAYS_MS: readonly number[] = [100, 500, 1000];\n\nexport type PreviewSessionOptions = {\n spaceId: string;\n environmentId: string;\n previewToken?: string;\n sessionId?: string;\n /**\n * Opaque encoded resource-resolution value for referenced spaces.\n */\n resourceResolution?: string;\n /**\n * Custom WebSocket URL for Preview Session subscriptions (staging, proxy, per-region).\n * Omit to use the production Preview Session WebSocket host.\n */\n sessionHost?: string;\n debug?: boolean;\n};\n\ntype SessionMessage =\n | { kind: 'next'; payload: ExperiencePayload }\n | { kind: 'error' }\n | { kind: 'unknown' }\n | { kind: 'invalid' };\n\ntype PreviewSessionHandlers = {\n onUpdate: (experience: ExperiencePayload) => void;\n onOpen?: () => void;\n onError?: (error: LivePreviewConnectionError) => void;\n};\n\nfunction parseMessage(data: unknown): SessionMessage {\n let message: unknown = data;\n\n if (typeof data === 'string') {\n try {\n message = JSON.parse(data);\n } catch {\n return { kind: 'invalid' };\n }\n }\n\n if (!isRecord(message) || typeof message.type !== 'string') {\n return { kind: 'invalid' };\n }\n\n if (message.type === 'next') {\n return isExperiencePayload(message.data)\n ? { kind: 'next', payload: message.data }\n : { kind: 'invalid' };\n }\n\n if (message.type === 'error') {\n return isErrorPayload(message.data) ? { kind: 'error' } : { kind: 'invalid' };\n }\n\n return { kind: 'unknown' };\n}\n\nfunction isSessionEnded(event: WebSocketCloseEvent): boolean {\n if (event.code !== 1000) return false;\n const reason = event.reason.trim().toLowerCase();\n return reason === 'deleted' || reason === 'expired';\n}\n\nexport function subscribeToPreviewSession(\n options: PreviewSessionOptions,\n handlers: PreviewSessionHandlers\n): () => void {\n const log = createDebugLogger(options.debug, 'live-preview');\n const { sessionId, previewToken } = options;\n const { onError, onOpen, onUpdate } = handlers;\n if (sessionId === undefined || previewToken === undefined) return () => undefined;\n\n const connection = createWebSocketConnection({\n url: previewSessionSubscribeUrl({ ...options, sessionId, previewToken }),\n retry: (failureCount, event) => {\n const sessionEnded = isSessionEnded(event);\n const hasExhaustedRetries = failureCount >= RETRY_DELAYS_MS.length;\n const shouldRetry = !hasExhaustedRetries && !sessionEnded;\n if (hasExhaustedRetries || sessionEnded) {\n onError?.(new LivePreviewConnectionError());\n }\n return shouldRetry;\n },\n retryDelay: (retryAttempt) =>\n RETRY_DELAYS_MS[retryAttempt] ?? RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1] ?? 0,\n onFailure: () => onError?.(new LivePreviewConnectionError()),\n });\n\n const unsubscribeFromOpen = onOpen ? connection.onopen(() => onOpen()) : undefined;\n const unsubscribe = connection.onmessage((event) => {\n const message = parseMessage(event.data);\n\n if (message.kind === 'unknown') return;\n if (message.kind === 'error') {\n log.log('Preview Session returned an error');\n return;\n }\n if (message.kind === 'invalid') {\n log.log('ignored invalid Preview Session message');\n return;\n }\n\n onUpdate(message.payload);\n });\n\n return () => {\n unsubscribeFromOpen?.();\n unsubscribe();\n connection.close();\n };\n}\n"],"mappings":"AAAA,SAAS,yBAAiD;AAC1D,SAAS,gBAAgB,qBAAqB,gBAAgB;AAC9D,SAAS,iCAA2D;AACpE,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAE3C,MAAM,kBAAqC,CAAC,KAAK,KAAK,GAAI;AA+B1D,SAAS,aAAa,MAA+B;AACnD,MAAI,UAAmB;AAEvB,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN,aAAO,EAAE,MAAM,UAAU;AAAA,IAC3B;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,OAAO,KAAK,OAAO,QAAQ,SAAS,UAAU;AAC1D,WAAO,EAAE,MAAM,UAAU;AAAA,EAC3B;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,WAAO,oBAAoB,QAAQ,IAAI,IACnC,EAAE,MAAM,QAAQ,SAAS,QAAQ,KAAK,IACtC,EAAE,MAAM,UAAU;AAAA,EACxB;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC5B,WAAO,eAAe,QAAQ,IAAI,IAAI,EAAE,MAAM,QAAQ,IAAI,EAAE,MAAM,UAAU;AAAA,EAC9E;AAEA,SAAO,EAAE,MAAM,UAAU;AAC3B;AAEA,SAAS,eAAe,OAAqC;AAC3D,MAAI,MAAM,SAAS,IAAM,QAAO;AAChC,QAAM,SAAS,MAAM,OAAO,KAAK,EAAE,YAAY;AAC/C,SAAO,WAAW,aAAa,WAAW;AAC5C;AAEO,SAAS,0BACd,SACA,UACY;AACZ,QAAM,MAAM,kBAAkB,QAAQ,OAAO,cAAc;AAC3D,QAAM,EAAE,WAAW,aAAa,IAAI;AACpC,QAAM,EAAE,SAAS,QAAQ,SAAS,IAAI;AACtC,MAAI,cAAc,UAAa,iBAAiB,OAAW,QAAO,MAAM;AAExE,QAAM,aAAa,0BAA0B;AAAA,IAC3C,KAAK,2BAA2B,EAAE,GAAG,SAAS,WAAW,aAAa,CAAC;AAAA,IACvE,OAAO,CAAC,cAAc,UAAU;AAC9B,YAAM,eAAe,eAAe,KAAK;AACzC,YAAM,sBAAsB,gBAAgB,gBAAgB;AAC5D,YAAM,cAAc,CAAC,uBAAuB,CAAC;AAC7C,UAAI,uBAAuB,cAAc;AACvC,kBAAU,IAAI,2BAA2B,CAAC;AAAA,MAC5C;AACA,aAAO;AAAA,IACT;AAAA,IACA,YAAY,CAAC,iBACX,gBAAgB,YAAY,KAAK,gBAAgB,gBAAgB,SAAS,CAAC,KAAK;AAAA,IAClF,WAAW,MAAM,UAAU,IAAI,2BAA2B,CAAC;AAAA,EAC7D,CAAC;AAED,QAAM,sBAAsB,SAAS,WAAW,OAAO,MAAM,OAAO,CAAC,IAAI;AACzE,QAAM,cAAc,WAAW,UAAU,CAAC,UAAU;AAClD,UAAM,UAAU,aAAa,MAAM,IAAI;AAEvC,QAAI,QAAQ,SAAS,UAAW;AAChC,QAAI,QAAQ,SAAS,SAAS;AAC5B,UAAI,IAAI,mCAAmC;AAC3C;AAAA,IACF;AACA,QAAI,QAAQ,SAAS,WAAW;AAC9B,UAAI,IAAI,yCAAyC;AACjD;AAAA,IACF;AAEA,aAAS,QAAQ,OAAO;AAAA,EAC1B,CAAC;AAED,SAAO,MAAM;AACX,0BAAsB;AACtB,gBAAY;AACZ,eAAW,MAAM;AAAA,EACnB;AACF;","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LivePreviewStatus } from './live-preview-client.js';
|
|
2
|
+
import '@contentful/experiences-sdk-core';
|
|
3
|
+
import './preview-session.js';
|
|
4
|
+
import './errors.js';
|
|
5
|
+
|
|
6
|
+
declare function sendPreviewStatus(status: LivePreviewStatus): void;
|
|
7
|
+
|
|
8
|
+
export { sendPreviewStatus };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const PREVIEW_STATUS_MESSAGE = {
|
|
2
|
+
source: "experiences/live-preview",
|
|
3
|
+
type: "status"
|
|
4
|
+
};
|
|
5
|
+
function sendPreviewStatus(status) {
|
|
6
|
+
if (typeof window === "undefined") return;
|
|
7
|
+
window.parent?.postMessage(
|
|
8
|
+
{
|
|
9
|
+
...PREVIEW_STATUS_MESSAGE,
|
|
10
|
+
status
|
|
11
|
+
},
|
|
12
|
+
"*"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
sendPreviewStatus
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=preview-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preview-status.ts"],"sourcesContent":["import type { LivePreviewStatus } from './live-preview-client.js';\n\nconst PREVIEW_STATUS_MESSAGE = {\n source: 'experiences/live-preview',\n type: 'status',\n} as const;\n\nexport function sendPreviewStatus(status: LivePreviewStatus): void {\n if (typeof window === 'undefined') return;\n\n window.parent?.postMessage(\n {\n ...PREVIEW_STATUS_MESSAGE,\n status,\n },\n '*'\n );\n}\n"],"mappings":"AAEA,MAAM,yBAAyB;AAAA,EAC7B,QAAQ;AAAA,EACR,MAAM;AACR;AAEO,SAAS,kBAAkB,QAAiC;AACjE,MAAI,OAAO,WAAW,YAAa;AAEnC,SAAO,QAAQ;AAAA,IACb;AAAA,MACE,GAAG;AAAA,MACH;AAAA,IACF;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type WebSocketCloseEvent = {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
readonly reason: string;
|
|
4
|
+
};
|
|
5
|
+
type WebSocketOpenEvent = {
|
|
6
|
+
readonly type: string;
|
|
7
|
+
};
|
|
8
|
+
type WebSocketMessageEvent = {
|
|
9
|
+
readonly data: unknown;
|
|
10
|
+
};
|
|
11
|
+
type WebSocket = {
|
|
12
|
+
onopen: ((event: WebSocketOpenEvent) => void) | null;
|
|
13
|
+
onclose: ((event: WebSocketCloseEvent) => void) | null;
|
|
14
|
+
onmessage: ((event: WebSocketMessageEvent) => void) | null;
|
|
15
|
+
close(): void;
|
|
16
|
+
};
|
|
17
|
+
type WebSocketConstructor = new (url: string) => WebSocket;
|
|
18
|
+
type WebSocketConnection = {
|
|
19
|
+
close(): void;
|
|
20
|
+
onopen(handler: (event: WebSocketOpenEvent) => void): () => void;
|
|
21
|
+
onmessage(handler: (event: WebSocketMessageEvent) => void): () => void;
|
|
22
|
+
};
|
|
23
|
+
declare function createWebSocketConnection(options: {
|
|
24
|
+
url: string;
|
|
25
|
+
retry?: number | ((failureCount: number, event: WebSocketCloseEvent) => boolean);
|
|
26
|
+
retryDelay?: number | ((retryAttempt: number, event: WebSocketCloseEvent) => number);
|
|
27
|
+
WebSocket?: WebSocketConstructor;
|
|
28
|
+
onFailure?: (error: unknown) => void;
|
|
29
|
+
}): WebSocketConnection;
|
|
30
|
+
|
|
31
|
+
export { type WebSocketCloseEvent, type WebSocketMessageEvent, type WebSocketOpenEvent, createWebSocketConnection };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
function createWebSocketConnection(options) {
|
|
2
|
+
const WS = options.WebSocket ?? globalThis.WebSocket;
|
|
3
|
+
if (!WS) throw new Error("WebSocket constructor is not available in this runtime.");
|
|
4
|
+
let websocket;
|
|
5
|
+
let retryTimer;
|
|
6
|
+
let retryCount = 0;
|
|
7
|
+
let closed = false;
|
|
8
|
+
const openHandlers = /* @__PURE__ */ new Set();
|
|
9
|
+
const messageHandlers = /* @__PURE__ */ new Set();
|
|
10
|
+
const clearRetryTimer = () => {
|
|
11
|
+
if (retryTimer === void 0) return;
|
|
12
|
+
clearTimeout(retryTimer);
|
|
13
|
+
retryTimer = void 0;
|
|
14
|
+
};
|
|
15
|
+
const clearHandlers = () => {
|
|
16
|
+
openHandlers.clear();
|
|
17
|
+
messageHandlers.clear();
|
|
18
|
+
};
|
|
19
|
+
const detachSocketHandlers = (currentSocket) => {
|
|
20
|
+
currentSocket.onopen = null;
|
|
21
|
+
currentSocket.onmessage = null;
|
|
22
|
+
currentSocket.onclose = null;
|
|
23
|
+
};
|
|
24
|
+
const shouldRetry = (event) => {
|
|
25
|
+
if (closed) return false;
|
|
26
|
+
if (typeof options.retry === "number") return retryCount < options.retry;
|
|
27
|
+
if (typeof options.retry === "function") return options.retry(retryCount, event);
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
const getRetryDelay = (event) => {
|
|
31
|
+
if (typeof options.retryDelay === "function") {
|
|
32
|
+
return options.retryDelay(retryCount, event);
|
|
33
|
+
}
|
|
34
|
+
return options.retryDelay ?? 0;
|
|
35
|
+
};
|
|
36
|
+
const handleClose = (currentSocket, event) => {
|
|
37
|
+
if (websocket !== currentSocket) return;
|
|
38
|
+
websocket = void 0;
|
|
39
|
+
detachSocketHandlers(currentSocket);
|
|
40
|
+
if (closed || !shouldRetry(event)) {
|
|
41
|
+
closed = true;
|
|
42
|
+
clearHandlers();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const delay = getRetryDelay(event);
|
|
46
|
+
retryCount += 1;
|
|
47
|
+
retryTimer = setTimeout(() => {
|
|
48
|
+
retryTimer = void 0;
|
|
49
|
+
if (closed) return;
|
|
50
|
+
try {
|
|
51
|
+
connect();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
closed = true;
|
|
54
|
+
clearHandlers();
|
|
55
|
+
options.onFailure?.(error);
|
|
56
|
+
}
|
|
57
|
+
}, delay);
|
|
58
|
+
};
|
|
59
|
+
const connect = () => {
|
|
60
|
+
if (closed || websocket !== void 0 || retryTimer !== void 0) return;
|
|
61
|
+
const ws = new WS(options.url);
|
|
62
|
+
websocket = ws;
|
|
63
|
+
ws.onclose = (event) => handleClose(ws, event);
|
|
64
|
+
ws.onopen = (event) => {
|
|
65
|
+
if (websocket !== ws || closed) return;
|
|
66
|
+
let handlerError;
|
|
67
|
+
let hasHandlerError = false;
|
|
68
|
+
for (const handler of [...openHandlers]) {
|
|
69
|
+
try {
|
|
70
|
+
handler(event);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!hasHandlerError) handlerError = error;
|
|
73
|
+
hasHandlerError = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (hasHandlerError) throw handlerError;
|
|
77
|
+
};
|
|
78
|
+
ws.onmessage = (event) => {
|
|
79
|
+
if (websocket !== ws || closed) return;
|
|
80
|
+
let handlerError;
|
|
81
|
+
let hasHandlerError = false;
|
|
82
|
+
for (const handler of [...messageHandlers]) {
|
|
83
|
+
try {
|
|
84
|
+
handler(event);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!hasHandlerError) handlerError = error;
|
|
87
|
+
hasHandlerError = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (hasHandlerError) throw handlerError;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const connection = {
|
|
94
|
+
close() {
|
|
95
|
+
if (closed) return;
|
|
96
|
+
closed = true;
|
|
97
|
+
clearRetryTimer();
|
|
98
|
+
const currentSocket = websocket;
|
|
99
|
+
if (!currentSocket) {
|
|
100
|
+
clearHandlers();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
detachSocketHandlers(currentSocket);
|
|
104
|
+
clearHandlers();
|
|
105
|
+
currentSocket.close();
|
|
106
|
+
},
|
|
107
|
+
onopen(handler) {
|
|
108
|
+
if (closed) return () => void 0;
|
|
109
|
+
openHandlers.add(handler);
|
|
110
|
+
return () => {
|
|
111
|
+
openHandlers.delete(handler);
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
onmessage(handler) {
|
|
115
|
+
if (closed) return () => void 0;
|
|
116
|
+
messageHandlers.add(handler);
|
|
117
|
+
return () => {
|
|
118
|
+
messageHandlers.delete(handler);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
connect();
|
|
123
|
+
return connection;
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
createWebSocketConnection
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/websocket.ts"],"sourcesContent":["export type WebSocketCloseEvent = {\n readonly code: number;\n readonly reason: string;\n};\n\nexport type WebSocketOpenEvent = {\n readonly type: string;\n};\n\nexport type WebSocketMessageEvent = {\n readonly data: unknown;\n};\n\ntype WebSocket = {\n onopen: ((event: WebSocketOpenEvent) => void) | null;\n onclose: ((event: WebSocketCloseEvent) => void) | null;\n onmessage: ((event: WebSocketMessageEvent) => void) | null;\n close(): void;\n};\n\ntype WebSocketConstructor = new (url: string) => WebSocket;\n\ntype WebSocketConnection = {\n close(): void;\n onopen(handler: (event: WebSocketOpenEvent) => void): () => void;\n onmessage(handler: (event: WebSocketMessageEvent) => void): () => void;\n};\n\nexport function createWebSocketConnection(options: {\n url: string;\n retry?: number | ((failureCount: number, event: WebSocketCloseEvent) => boolean);\n retryDelay?: number | ((retryAttempt: number, event: WebSocketCloseEvent) => number);\n WebSocket?: WebSocketConstructor;\n onFailure?: (error: unknown) => void;\n}): WebSocketConnection {\n const WS = options.WebSocket ?? (globalThis.WebSocket as WebSocketConstructor | undefined);\n if (!WS) throw new Error('WebSocket constructor is not available in this runtime.');\n\n let websocket: WebSocket | undefined;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n let retryCount = 0;\n let closed = false;\n\n const openHandlers = new Set<(event: WebSocketOpenEvent) => void>();\n const messageHandlers = new Set<(event: WebSocketMessageEvent) => void>();\n\n const clearRetryTimer = () => {\n if (retryTimer === undefined) return;\n clearTimeout(retryTimer);\n retryTimer = undefined;\n };\n\n const clearHandlers = () => {\n openHandlers.clear();\n messageHandlers.clear();\n };\n\n const detachSocketHandlers = (currentSocket: WebSocket) => {\n currentSocket.onopen = null;\n currentSocket.onmessage = null;\n currentSocket.onclose = null;\n };\n\n const shouldRetry = (event: WebSocketCloseEvent) => {\n if (closed) return false;\n if (typeof options.retry === 'number') return retryCount < options.retry;\n if (typeof options.retry === 'function') return options.retry(retryCount, event);\n return false;\n };\n\n const getRetryDelay = (event: WebSocketCloseEvent) => {\n if (typeof options.retryDelay === 'function') {\n return options.retryDelay(retryCount, event);\n }\n return options.retryDelay ?? 0;\n };\n\n const handleClose = (currentSocket: WebSocket, event: WebSocketCloseEvent) => {\n if (websocket !== currentSocket) return;\n\n websocket = undefined;\n detachSocketHandlers(currentSocket);\n\n if (closed || !shouldRetry(event)) {\n closed = true;\n clearHandlers();\n return;\n }\n\n const delay = getRetryDelay(event);\n retryCount += 1;\n retryTimer = setTimeout(() => {\n retryTimer = undefined;\n if (closed) return;\n try {\n connect();\n } catch (error: unknown) {\n closed = true;\n clearHandlers();\n options.onFailure?.(error);\n }\n }, delay);\n };\n\n const connect = () => {\n if (closed || websocket !== undefined || retryTimer !== undefined) return;\n\n const ws = new WS(options.url);\n websocket = ws;\n ws.onclose = (event) => handleClose(ws, event);\n ws.onopen = (event) => {\n if (websocket !== ws || closed) return;\n\n let handlerError: unknown;\n let hasHandlerError = false;\n\n for (const handler of [...openHandlers]) {\n try {\n handler(event);\n } catch (error: unknown) {\n if (!hasHandlerError) handlerError = error;\n hasHandlerError = true;\n }\n }\n\n if (hasHandlerError) throw handlerError;\n };\n ws.onmessage = (event) => {\n if (websocket !== ws || closed) return;\n\n let handlerError: unknown;\n let hasHandlerError = false;\n\n for (const handler of [...messageHandlers]) {\n try {\n handler(event);\n } catch (error: unknown) {\n if (!hasHandlerError) handlerError = error;\n hasHandlerError = true;\n }\n }\n\n if (hasHandlerError) throw handlerError;\n };\n };\n\n const connection: WebSocketConnection = {\n close() {\n if (closed) return;\n closed = true;\n clearRetryTimer();\n\n const currentSocket = websocket;\n if (!currentSocket) {\n clearHandlers();\n return;\n }\n\n detachSocketHandlers(currentSocket);\n clearHandlers();\n currentSocket.close();\n },\n onopen(handler) {\n if (closed) return () => undefined;\n\n openHandlers.add(handler);\n return () => {\n openHandlers.delete(handler);\n };\n },\n onmessage(handler) {\n if (closed) return () => undefined;\n\n messageHandlers.add(handler);\n return () => {\n messageHandlers.delete(handler);\n };\n },\n };\n\n connect();\n return connection;\n}\n"],"mappings":"AA4BO,SAAS,0BAA0B,SAMlB;AACtB,QAAM,KAAK,QAAQ,aAAc,WAAW;AAC5C,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,yDAAyD;AAElF,MAAI;AACJ,MAAI;AACJ,MAAI,aAAa;AACjB,MAAI,SAAS;AAEb,QAAM,eAAe,oBAAI,IAAyC;AAClE,QAAM,kBAAkB,oBAAI,IAA4C;AAExE,QAAM,kBAAkB,MAAM;AAC5B,QAAI,eAAe,OAAW;AAC9B,iBAAa,UAAU;AACvB,iBAAa;AAAA,EACf;AAEA,QAAM,gBAAgB,MAAM;AAC1B,iBAAa,MAAM;AACnB,oBAAgB,MAAM;AAAA,EACxB;AAEA,QAAM,uBAAuB,CAAC,kBAA6B;AACzD,kBAAc,SAAS;AACvB,kBAAc,YAAY;AAC1B,kBAAc,UAAU;AAAA,EAC1B;AAEA,QAAM,cAAc,CAAC,UAA+B;AAClD,QAAI,OAAQ,QAAO;AACnB,QAAI,OAAO,QAAQ,UAAU,SAAU,QAAO,aAAa,QAAQ;AACnE,QAAI,OAAO,QAAQ,UAAU,WAAY,QAAO,QAAQ,MAAM,YAAY,KAAK;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,CAAC,UAA+B;AACpD,QAAI,OAAO,QAAQ,eAAe,YAAY;AAC5C,aAAO,QAAQ,WAAW,YAAY,KAAK;AAAA,IAC7C;AACA,WAAO,QAAQ,cAAc;AAAA,EAC/B;AAEA,QAAM,cAAc,CAAC,eAA0B,UAA+B;AAC5E,QAAI,cAAc,cAAe;AAEjC,gBAAY;AACZ,yBAAqB,aAAa;AAElC,QAAI,UAAU,CAAC,YAAY,KAAK,GAAG;AACjC,eAAS;AACT,oBAAc;AACd;AAAA,IACF;AAEA,UAAM,QAAQ,cAAc,KAAK;AACjC,kBAAc;AACd,iBAAa,WAAW,MAAM;AAC5B,mBAAa;AACb,UAAI,OAAQ;AACZ,UAAI;AACF,gBAAQ;AAAA,MACV,SAAS,OAAgB;AACvB,iBAAS;AACT,sBAAc;AACd,gBAAQ,YAAY,KAAK;AAAA,MAC3B;AAAA,IACF,GAAG,KAAK;AAAA,EACV;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU,cAAc,UAAa,eAAe,OAAW;AAEnE,UAAM,KAAK,IAAI,GAAG,QAAQ,GAAG;AAC7B,gBAAY;AACZ,OAAG,UAAU,CAAC,UAAU,YAAY,IAAI,KAAK;AAC7C,OAAG,SAAS,CAAC,UAAU;AACrB,UAAI,cAAc,MAAM,OAAQ;AAEhC,UAAI;AACJ,UAAI,kBAAkB;AAEtB,iBAAW,WAAW,CAAC,GAAG,YAAY,GAAG;AACvC,YAAI;AACF,kBAAQ,KAAK;AAAA,QACf,SAAS,OAAgB;AACvB,cAAI,CAAC,gBAAiB,gBAAe;AACrC,4BAAkB;AAAA,QACpB;AAAA,MACF;AAEA,UAAI,gBAAiB,OAAM;AAAA,IAC7B;AACA,OAAG,YAAY,CAAC,UAAU;AACxB,UAAI,cAAc,MAAM,OAAQ;AAEhC,UAAI;AACJ,UAAI,kBAAkB;AAEtB,iBAAW,WAAW,CAAC,GAAG,eAAe,GAAG;AAC1C,YAAI;AACF,kBAAQ,KAAK;AAAA,QACf,SAAS,OAAgB;AACvB,cAAI,CAAC,gBAAiB,gBAAe;AACrC,4BAAkB;AAAA,QACpB;AAAA,MACF;AAEA,UAAI,gBAAiB,OAAM;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,aAAkC;AAAA,IACtC,QAAQ;AACN,UAAI,OAAQ;AACZ,eAAS;AACT,sBAAgB;AAEhB,YAAM,gBAAgB;AACtB,UAAI,CAAC,eAAe;AAClB,sBAAc;AACd;AAAA,MACF;AAEA,2BAAqB,aAAa;AAClC,oBAAc;AACd,oBAAc,MAAM;AAAA,IACtB;AAAA,IACA,OAAO,SAAS;AACd,UAAI,OAAQ,QAAO,MAAM;AAEzB,mBAAa,IAAI,OAAO;AACxB,aAAO,MAAM;AACX,qBAAa,OAAO,OAAO;AAAA,MAC7B;AAAA,IACF;AAAA,IACA,UAAU,SAAS;AACjB,UAAI,OAAQ,QAAO,MAAM;AAEzB,sBAAgB,IAAI,OAAO;AAC3B,aAAO,MAAM;AACX,wBAAgB,OAAO,OAAO;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAEA,UAAQ;AACR,SAAO;AACT;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contentful/experiences-live-preview",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Optional live preview for Contentful Experiences",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/contentful/experiences.git",
|
|
30
|
+
"directory": "packages/live-preview"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@contentful/experiences-client": "0.4.8",
|
|
34
|
+
"@contentful/experiences-sdk-core": "0.8.6"
|
|
35
|
+
}
|
|
36
|
+
}
|