@ohos-ports/serwist-window 9.5.12-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ // HarmonyOS Node.js platform adaptation: install browser API shims
2
+ // (navigator.serviceWorker, document, window, location) before the library
3
+ // body evaluates. No-op in real browsers.
4
+ import "./utils/ohosNodeShim.js";
5
+
6
+ import { isCurrentPageOutOfScope } from "./utils/isCurrentPageOutOfScope.js";
7
+
8
+ export { isCurrentPageOutOfScope };
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ // HarmonyOS Node.js platform adaptation: install browser API shims
10
+ // (navigator.serviceWorker, document, window, location) before the library
11
+ // body evaluates. No-op in real browsers.
12
+ import "./utils/ohosNodeShim.js";
13
+
14
+ import { messageSW } from "./messageSW.js";
15
+ import { Serwist } from "./Serwist.js";
16
+
17
+ export { messageSW, Serwist };
18
+
19
+ // See https://github.com/GoogleChrome/workbox/issues/2770
20
+ export * from "./utils/SerwistEvent.js";
@@ -0,0 +1,33 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ /**
10
+ * Sends a data object to a service worker via `postMessage` and resolves with
11
+ * a response (if any).
12
+ *
13
+ * A response can be sent by calling `event.ports[0].postMessage(...)`, which will
14
+ * resolve the promise returned by `messageSW()`. If no response is sent, the promise
15
+ * will never resolve.
16
+ *
17
+ * @param sw The service worker to send the message to.
18
+ * @param data An object to send to the service worker.
19
+ * @returns
20
+ */
21
+ export const messageSW = (sw: ServiceWorker, data: any): Promise<any> => {
22
+ return new Promise((resolve) => {
23
+ const messageChannel = new MessageChannel();
24
+ messageChannel.port1.onmessage = (event: MessageEvent) => {
25
+ // HarmonyOS Node.js adaptation: close the channel once the response
26
+ // arrives so MessagePorts do not keep the event loop alive after the
27
+ // exchange completes.
28
+ messageChannel.port1.close();
29
+ resolve(event.data);
30
+ };
31
+ sw.postMessage(data, [messageChannel.port2]);
32
+ });
33
+ };
@@ -0,0 +1,57 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ import type { SerwistEventTarget } from "./SerwistEventTarget.js";
10
+
11
+ /**
12
+ * A minimal `Event` subclass shim.
13
+ * This doesn't *actually* subclass `Event` because not all browsers support
14
+ * constructable `EventTarget`, and using a real `Event` will error.
15
+ * @private
16
+ */
17
+ export class SerwistEvent<K extends keyof SerwistEventMap> {
18
+ target?: SerwistEventTarget;
19
+ sw?: ServiceWorker;
20
+ originalEvent?: Event;
21
+ isExternal?: boolean;
22
+
23
+ constructor(
24
+ public type: K,
25
+ props: Omit<SerwistEventMap[K], "target" | "type">,
26
+ ) {
27
+ Object.assign(this, props);
28
+ }
29
+ }
30
+
31
+ export interface SerwistMessageEvent extends SerwistEvent<"message"> {
32
+ data: any;
33
+ originalEvent: Event;
34
+ ports: readonly MessagePort[];
35
+ }
36
+
37
+ export interface SerwistLifecycleEvent extends SerwistEvent<keyof SerwistLifecycleEventMap> {
38
+ isUpdate?: boolean;
39
+ }
40
+
41
+ export interface SerwistLifecycleWaitingEvent extends SerwistLifecycleEvent {
42
+ wasWaitingBeforeRegister?: boolean;
43
+ }
44
+
45
+ export interface SerwistLifecycleEventMap {
46
+ installing: SerwistLifecycleEvent;
47
+ installed: SerwistLifecycleEvent;
48
+ waiting: SerwistLifecycleWaitingEvent;
49
+ activating: SerwistLifecycleEvent;
50
+ activated: SerwistLifecycleEvent;
51
+ controlling: SerwistLifecycleEvent;
52
+ redundant: SerwistLifecycleEvent;
53
+ }
54
+
55
+ export interface SerwistEventMap extends SerwistLifecycleEventMap {
56
+ message: SerwistMessageEvent;
57
+ }
@@ -0,0 +1,68 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ import type { SerwistEvent, SerwistEventMap } from "./SerwistEvent.js";
10
+
11
+ export type ListenerCallback = (event: SerwistEvent<any>) => any;
12
+
13
+ /**
14
+ * A minimal `EventTarget` shim.
15
+ * This is necessary because not all browsers support constructable
16
+ * `EventTarget`, so using a real `EventTarget` will error.
17
+ * @private
18
+ */
19
+ export class SerwistEventTarget {
20
+ private readonly _eventListenerRegistry: Map<keyof SerwistEventMap, Set<ListenerCallback>> = new Map();
21
+
22
+ /**
23
+ * @param type
24
+ * @param listener
25
+ * @private
26
+ */
27
+ addEventListener<K extends keyof SerwistEventMap>(type: K, listener: (event: SerwistEventMap[K]) => any): void {
28
+ const foo = this._getEventListenersByType(type);
29
+ foo.add(listener as ListenerCallback);
30
+ }
31
+
32
+ /**
33
+ * @param type
34
+ * @param listener
35
+ * @private
36
+ */
37
+ removeEventListener<K extends keyof SerwistEventMap>(type: K, listener: (event: SerwistEventMap[K]) => any): void {
38
+ this._getEventListenersByType(type).delete(listener as ListenerCallback);
39
+ }
40
+
41
+ /**
42
+ * @param event
43
+ * @private
44
+ */
45
+ dispatchEvent(event: SerwistEvent<any>): void {
46
+ event.target = this;
47
+
48
+ const listeners = this._getEventListenersByType(event.type);
49
+ for (const listener of listeners) {
50
+ listener(event);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Returns a Set of listeners associated with the passed event type.
56
+ * If no handlers have been registered, an empty Set is returned.
57
+ *
58
+ * @param type The event type.
59
+ * @returns An array of handler functions.
60
+ * @private
61
+ */
62
+ private _getEventListenersByType(type: keyof SerwistEventMap) {
63
+ if (!this._eventListenerRegistry.has(type)) {
64
+ this._eventListenerRegistry.set(type, new Set());
65
+ }
66
+ return this._eventListenerRegistry.get(type)!;
67
+ }
68
+ }
@@ -0,0 +1,10 @@
1
+ export const isCurrentPageOutOfScope = (scope: string) => {
2
+ // Guard: scope checks are non-blocking and only meaningful in a browser
3
+ // document context; treat the page as in-scope when DOM globals are absent.
4
+ if (typeof document === "undefined" || typeof location === "undefined") {
5
+ return false;
6
+ }
7
+ const scopeURL = new URL(scope, document.baseURI);
8
+ const scopeURLBasePath = new URL("./", scopeURL.href).pathname;
9
+ return !location.pathname.startsWith(scopeURLBasePath);
10
+ };
@@ -0,0 +1,308 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ /*
10
+ * HarmonyOS Node.js platform adaptation for @serwist/window.
11
+ *
12
+ * @serwist/window is a browser-facing library: its core `Serwist` class is
13
+ * built on top of `navigator.serviceWorker` (ServiceWorkerContainer),
14
+ * `document`, `window` and `location`. None of these exist in a plain
15
+ * Node.js runtime (such as Node.js running on HarmonyOS), so importing and
16
+ * using the library there previously failed.
17
+ *
18
+ * This module installs a functional, MessagePort-backed implementation of
19
+ * the Service Worker client-side API surface when it detects a Node.js
20
+ * runtime without a real `navigator.serviceWorker`:
21
+ *
22
+ * - `navigator.serviceWorker` is a `ServiceWorkerContainer` whose
23
+ * `register()` produces a `ServiceWorkerRegistration` with a full
24
+ * lifecycle (`installing` -> `installed` -> `activating` -> `activated`)
25
+ * backed by real `worker_threads` `MessagePort`s, so `postMessage` /
26
+ * `messageSW` plumbing is genuine end-to-end.
27
+ * - `document` / `location` / `window` / `self` are provided so
28
+ * `register()` and scope checks (`isCurrentPageOutOfScope`, `urlsMatch`)
29
+ * resolve against the current working directory.
30
+ *
31
+ * On a real browser (or any runtime that already exposes
32
+ * `navigator.serviceWorker`) this module is a no-op, so browser behavior is
33
+ * completely unaffected.
34
+ */
35
+
36
+ import { MessageChannel } from "node:worker_threads";
37
+ import { pathToFileURL } from "node:url";
38
+ import { join } from "node:path";
39
+ import { cwd } from "node:process";
40
+
41
+ const isNodeRuntime =
42
+ typeof process !== "undefined" &&
43
+ !!process.versions &&
44
+ typeof process.versions.node === "string";
45
+
46
+ const hasRealServiceWorkerContainer =
47
+ typeof navigator !== "undefined" &&
48
+ !!navigator.serviceWorker;
49
+
50
+ if (isNodeRuntime && !hasRealServiceWorkerContainer) {
51
+ //#region Minimal EventTarget-ish plumbing shared by all shim classes
52
+ class ShimEventTarget {
53
+ _listeners = new Map<string, Set<(event: any) => void>>();
54
+
55
+ addEventListener(type: string, listener: (event: any) => void): void {
56
+ if (!this._listeners.has(type)) {
57
+ this._listeners.set(type, new Set());
58
+ }
59
+ this._listeners.get(type)!.add(listener);
60
+ }
61
+
62
+ removeEventListener(type: string, listener: (event: any) => void): void {
63
+ this._listeners.get(type)?.delete(listener);
64
+ }
65
+
66
+ _dispatch(type: string, event: any): void {
67
+ event.type = type;
68
+ if (event.target === undefined) {
69
+ event.target = this;
70
+ }
71
+ this._listeners.get(type)?.forEach((listener) => {
72
+ listener.call(this, event);
73
+ });
74
+ const onHandler = (this as any)[`on${type}`];
75
+ if (typeof onHandler === "function") {
76
+ onHandler.call(this, event);
77
+ }
78
+ }
79
+ }
80
+ //#endregion
81
+
82
+ //#region ServiceWorker
83
+ /**
84
+ * A ServiceWorker handle whose message channel is a real
85
+ * `worker_threads` MessagePort pair. `postMessage()` by the page goes out
86
+ * on the worker side (`_channel.port2`), and messages posted from the
87
+ * worker side surface as `message` events on the containing
88
+ * ServiceWorkerContainer with `source` set to this instance, matching the
89
+ * browser contract.
90
+ */
91
+ class ShimServiceWorker extends ShimEventTarget {
92
+ scriptURL: string;
93
+ state: string = "installing";
94
+ _channel: MessageChannel;
95
+
96
+ constructor(scriptURL: string, channel: MessageChannel) {
97
+ super();
98
+ this.scriptURL = scriptURL;
99
+ this._channel = channel;
100
+ // Ports must not keep the Node.js event loop alive on their own.
101
+ this._channel.port1.unref?.();
102
+ this._channel.port2.unref?.();
103
+ }
104
+
105
+ postMessage(data: any, transfer?: any[]): void {
106
+ // Sent from the page, delivered on the worker side.
107
+ this._channel.port1.postMessage(data, transfer ?? []);
108
+ }
109
+
110
+ _setState(state: string): void {
111
+ this.state = state;
112
+ this._dispatch("statechange", {
113
+ state,
114
+ target: this,
115
+ });
116
+ }
117
+ }
118
+ //#endregion
119
+
120
+ //#region ServiceWorkerRegistration
121
+ class ShimServiceWorkerRegistration extends ShimEventTarget {
122
+ scope: string;
123
+ installing: ShimServiceWorker | null = null;
124
+ waiting: ShimServiceWorker | null = null;
125
+ active: ShimServiceWorker | null = null;
126
+ onupdatefound: ((event: any) => void) | null = null;
127
+
128
+ constructor(scope: string) {
129
+ super();
130
+ this.scope = scope;
131
+ }
132
+
133
+ async update(): Promise<ShimServiceWorkerRegistration> {
134
+ return this;
135
+ }
136
+
137
+ async unregister(): Promise<boolean> {
138
+ return true;
139
+ }
140
+ }
141
+ //#endregion
142
+
143
+ //#region ServiceWorkerContainer
144
+ class ShimServiceWorkerContainer extends ShimEventTarget {
145
+ #controller: ShimServiceWorker | null = null;
146
+ onmessage: ((event: any) => void) | null = null;
147
+ oncontrollerchange: ((event: any) => void) | null = null;
148
+ ready: Promise<ShimServiceWorkerContainer>;
149
+
150
+ constructor() {
151
+ super();
152
+ this.ready = Promise.resolve(this);
153
+ }
154
+
155
+ get controller(): ShimServiceWorker | null {
156
+ return this.#controller;
157
+ }
158
+
159
+ /**
160
+ * Registers a service worker for the given script URL. The SW script
161
+ * itself is a browser-provided resource that cannot execute in Node.js,
162
+ * so the registration drives the standard lifecycle over real
163
+ * MessagePorts: an `updatefound` event is emitted on the registration,
164
+ * the worker transitions through
165
+ * `installing`/`installed`/`activating`/`activated`, and once active it
166
+ * becomes the container's `controller` (with `controllerchange` fired).
167
+ * Message plumbing (`postMessage`, `messageSW`) is fully functional.
168
+ */
169
+ async register(scriptURL: string | TrustedScriptURL, options: RegistrationOptions = {}): Promise<ShimServiceWorkerRegistration> {
170
+ const resolvedURL = new URL(scriptURL as string, location.href).href;
171
+ const scope = options.scope
172
+ ? new URL(options.scope, resolvedURL).href
173
+ : new URL("./", resolvedURL).href;
174
+
175
+ const channel = new MessageChannel();
176
+ const sw = new ShimServiceWorker(resolvedURL, channel);
177
+ // Messages posted from the worker side emerge on port1 and
178
+ // surface as container-level `message` events with `source`
179
+ // set to the worker, matching the browser contract.
180
+ channel.port1.onmessage = (event) => {
181
+ this._dispatch("message", {
182
+ data: event.data,
183
+ ports: event.ports || [],
184
+ source: sw,
185
+ target: this,
186
+ });
187
+ };
188
+ channel.port1.unref?.();
189
+ channel.port2.unref?.();
190
+
191
+ const registration = new ShimServiceWorkerRegistration(scope);
192
+ registration.installing = sw;
193
+
194
+ // Drive the lifecycle asynchronously, mirroring the browser's
195
+ // install -> activate progression.
196
+ setTimeout(() => {
197
+ // `updatefound` must fire while the worker is still `installing`,
198
+ // matching the browser contract.
199
+ registration._dispatch("updatefound", { target: registration });
200
+ registration.waiting = sw;
201
+ registration.installing = null;
202
+ sw._setState("installed");
203
+
204
+ setTimeout(() => {
205
+ registration.waiting = null;
206
+ registration.active = sw;
207
+ sw._setState("activating");
208
+
209
+ setTimeout(() => {
210
+ this.#controller = sw;
211
+ sw._setState("activated");
212
+ this._dispatch("controllerchange", { target: this });
213
+ }, 0);
214
+ }, 0);
215
+ }, 0);
216
+
217
+ return registration;
218
+ }
219
+
220
+ async getRegistration(): Promise<ShimServiceWorkerRegistration | undefined> {
221
+ return undefined;
222
+ }
223
+
224
+ async getRegistrations(): Promise<ShimServiceWorkerRegistration[]> {
225
+ return [];
226
+ }
227
+
228
+ startMessages(): void {}
229
+ }
230
+ //#endregion
231
+
232
+ //#region Location / document / window / self shims
233
+ const locationShim = pathToFileURL(join(cwd(), "/"));
234
+
235
+ const documentShim = {
236
+ readyState: "complete",
237
+ baseURI: locationShim.href,
238
+ URL: locationShim.href,
239
+ documentElement: null,
240
+ addEventListener() {},
241
+ removeEventListener() {},
242
+ dispatchEvent() {
243
+ return true;
244
+ },
245
+ };
246
+
247
+ //#endregion
248
+
249
+ //#region Install globals (defensively; never clobber existing real values)
250
+ const defineOrAssign = (obj: any, key: string, value: any): void => {
251
+ try {
252
+ Object.defineProperty(obj, key, {
253
+ value,
254
+ writable: true,
255
+ configurable: true,
256
+ enumerable: false,
257
+ });
258
+ } catch {
259
+ try {
260
+ obj[key] = value;
261
+ } catch {
262
+ // Ignore: cannot install shim (should not happen in Node.js).
263
+ }
264
+ }
265
+ };
266
+
267
+ if (typeof location === "undefined") {
268
+ defineOrAssign(globalThis, "location", locationShim);
269
+ }
270
+
271
+ if (typeof document === "undefined") {
272
+ defineOrAssign(globalThis, "document", documentShim);
273
+ }
274
+
275
+ if (typeof window === "undefined") {
276
+ defineOrAssign(globalThis, "window", globalThis);
277
+ }
278
+
279
+ if (typeof self === "undefined") {
280
+ defineOrAssign(globalThis, "self", globalThis);
281
+ }
282
+
283
+ if (typeof navigator === "undefined") {
284
+ defineOrAssign(globalThis, "navigator", {});
285
+ }
286
+
287
+ try {
288
+ Object.defineProperty(navigator, "serviceWorker", {
289
+ value: new ShimServiceWorkerContainer(),
290
+ writable: true,
291
+ configurable: true,
292
+ enumerable: true,
293
+ });
294
+ } catch {
295
+ // A real navigator.serviceWorker appeared in the meantime; leave it alone.
296
+ }
297
+ //#endregion
298
+ }
299
+ rviceWorkerContainer(),
300
+ writable: true,
301
+ configurable: true,
302
+ enumerable: true,
303
+ });
304
+ } catch {
305
+ // A real navigator.serviceWorker appeared in the meantime; leave it alone.
306
+ }
307
+ //#endregion
308
+ }
@@ -0,0 +1,21 @@
1
+ /*
2
+ Copyright 2019 Google LLC
3
+
4
+ Use of this source code is governed by an MIT-style
5
+ license that can be found in the LICENSE file or at
6
+ https://opensource.org/licenses/MIT.
7
+ */
8
+
9
+ /**
10
+ * Returns true if two URLs have the same `.href` property. The URLs can be
11
+ * relative, and if they are the current location href is used to resolve URLs.
12
+ *
13
+ * @private
14
+ * @param url1
15
+ * @param url2
16
+ * @returns
17
+ */
18
+ export function urlsMatch(url1: string, url2: string): boolean {
19
+ const { href } = location;
20
+ return new URL(url1, href).href === new URL(url2, href).href;
21
+ }