adjacent-react 0.1.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/badge/app-badge.d.ts +36 -0
- package/dist/badge/app-badge.d.ts.map +1 -0
- package/dist/badge/app-badge.js +49 -0
- package/dist/badge/app-badge.js.map +1 -0
- package/dist/badge/use-app-badge.d.ts +12 -0
- package/dist/badge/use-app-badge.d.ts.map +1 -0
- package/dist/badge/use-app-badge.js +20 -0
- package/dist/badge/use-app-badge.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/install/install-element.d.ts +19 -0
- package/dist/install/install-element.d.ts.map +1 -0
- package/dist/install/install-element.js +16 -0
- package/dist/install/install-element.js.map +1 -0
- package/dist/install/install-store.d.ts +33 -0
- package/dist/install/install-store.d.ts.map +1 -0
- package/dist/install/install-store.js +74 -0
- package/dist/install/install-store.js.map +1 -0
- package/dist/install/use-install.d.ts +7 -0
- package/dist/install/use-install.d.ts.map +1 -0
- package/dist/install/use-install.js +8 -0
- package/dist/install/use-install.js.map +1 -0
- package/dist/media/media-session.d.ts +38 -0
- package/dist/media/media-session.d.ts.map +1 -0
- package/dist/media/media-session.js +42 -0
- package/dist/media/media-session.js.map +1 -0
- package/dist/media/use-media-session.d.ts +24 -0
- package/dist/media/use-media-session.d.ts.map +1 -0
- package/dist/media/use-media-session.js +87 -0
- package/dist/media/use-media-session.js.map +1 -0
- package/dist/notifications/notifications.d.ts +48 -0
- package/dist/notifications/notifications.d.ts.map +1 -0
- package/dist/notifications/notifications.js +42 -0
- package/dist/notifications/notifications.js.map +1 -0
- package/dist/notifications/push.d.ts +26 -0
- package/dist/notifications/push.d.ts.map +1 -0
- package/dist/notifications/push.js +61 -0
- package/dist/notifications/push.js.map +1 -0
- package/dist/notifications/use-notifications.d.ts +12 -0
- package/dist/notifications/use-notifications.d.ts.map +1 -0
- package/dist/notifications/use-notifications.js +61 -0
- package/dist/notifications/use-notifications.js.map +1 -0
- package/dist/runtime/runtime-store.d.ts +58 -0
- package/dist/runtime/runtime-store.d.ts.map +1 -0
- package/dist/runtime/runtime-store.js +166 -0
- package/dist/runtime/runtime-store.js.map +1 -0
- package/dist/runtime/use-adjacent.d.ts +14 -0
- package/dist/runtime/use-adjacent.d.ts.map +1 -0
- package/dist/runtime/use-adjacent.js +18 -0
- package/dist/runtime/use-adjacent.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { supportsMediaSession, toMetadataInit, toPositionState, } from "./media-session.js";
|
|
4
|
+
const ACTION_NAMES = [
|
|
5
|
+
"nexttrack",
|
|
6
|
+
"pause",
|
|
7
|
+
"play",
|
|
8
|
+
"previoustrack",
|
|
9
|
+
"seekbackward",
|
|
10
|
+
"seekforward",
|
|
11
|
+
"seekto",
|
|
12
|
+
"stop",
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Publishes what is playing to the operating system: the lock screen, the
|
|
16
|
+
* media control in a notification shade or menu bar, and the hardware keys on
|
|
17
|
+
* a keyboard or headset.
|
|
18
|
+
*
|
|
19
|
+
* It renders nothing and owns no audio. The application keeps its own element
|
|
20
|
+
* and its own state, and describes them here, which is the same division the
|
|
21
|
+
* rest of Adjacent uses.
|
|
22
|
+
*/
|
|
23
|
+
export function useMediaSession({ actions, metadata, playbackState, position, }) {
|
|
24
|
+
// Read through a ref so a handler written inline does not re-register every
|
|
25
|
+
// render, while still calling the latest one.
|
|
26
|
+
const handlers = useRef(actions ?? {});
|
|
27
|
+
handlers.current = actions ?? {};
|
|
28
|
+
// Which actions exist, rather than which functions: registering is what has
|
|
29
|
+
// to happen again, and only when an action appears or disappears.
|
|
30
|
+
const registered = ACTION_NAMES.filter((name) => actions?.[name] !== undefined).join();
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!supportsMediaSession())
|
|
33
|
+
return;
|
|
34
|
+
const session = navigator.mediaSession;
|
|
35
|
+
const names = registered.length > 0 ? registered.split(",") : [];
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
try {
|
|
38
|
+
session.setActionHandler(name, (details) => handlers.current[name]?.(details));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Thrown for an action this browser does not know. The others still
|
|
42
|
+
// register, which is the point of setting them one at a time.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return () => {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
try {
|
|
48
|
+
session.setActionHandler(name, null);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Unknown here too, so it was never set.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}, [registered]);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!supportsMediaSession())
|
|
58
|
+
return;
|
|
59
|
+
navigator.mediaSession.metadata = metadata ? new MediaMetadata(toMetadataInit(metadata)) : null;
|
|
60
|
+
}, [
|
|
61
|
+
metadata?.album,
|
|
62
|
+
metadata?.artist,
|
|
63
|
+
metadata?.title,
|
|
64
|
+
metadata?.artwork?.map((image) => image.src).join(),
|
|
65
|
+
]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!supportsMediaSession())
|
|
68
|
+
return;
|
|
69
|
+
navigator.mediaSession.playbackState = playbackState ?? "none";
|
|
70
|
+
}, [playbackState]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!supportsMediaSession() || !navigator.mediaSession.setPositionState)
|
|
73
|
+
return;
|
|
74
|
+
const state = position ? toPositionState(position) : undefined;
|
|
75
|
+
navigator.mediaSession.setPositionState(state);
|
|
76
|
+
}, [position?.duration, position?.playbackRate, position?.position]);
|
|
77
|
+
useEffect(() => () => {
|
|
78
|
+
// The player is gone, so the OS should stop offering it. Metadata and
|
|
79
|
+
// state are global to the document: left alone they outlive the component
|
|
80
|
+
// that set them.
|
|
81
|
+
if (!supportsMediaSession())
|
|
82
|
+
return;
|
|
83
|
+
navigator.mediaSession.metadata = null;
|
|
84
|
+
navigator.mediaSession.playbackState = "none";
|
|
85
|
+
}, []);
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=use-media-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-media-session.js","sourceRoot":"","sources":["../../src/media/use-media-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `"unsupported"` is a fourth state the browser does not have: it separates a
|
|
3
|
+
* browser that cannot show notifications from a user who has said no, which the
|
|
4
|
+
* interface has to treat differently. Asking again is worth offering to one and
|
|
5
|
+
* not the other.
|
|
6
|
+
*/
|
|
7
|
+
export type NotificationPermissionState = "default" | "denied" | "granted" | "unsupported";
|
|
8
|
+
export interface NotificationAction {
|
|
9
|
+
readonly action: string;
|
|
10
|
+
readonly icon?: string;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ShowNotificationOptions {
|
|
14
|
+
/** Buttons on the notification. Only a notification shown through the
|
|
15
|
+
* service worker can carry them, which is why this always goes through it. */
|
|
16
|
+
readonly actions?: readonly NotificationAction[];
|
|
17
|
+
readonly badge?: string;
|
|
18
|
+
readonly body?: string;
|
|
19
|
+
/** Carried back to the service worker's click handler untouched. */
|
|
20
|
+
readonly data?: unknown;
|
|
21
|
+
readonly icon?: string;
|
|
22
|
+
/** Replaces an earlier notification with the same tag instead of stacking. */
|
|
23
|
+
readonly tag?: string;
|
|
24
|
+
readonly requireInteraction?: boolean;
|
|
25
|
+
readonly silent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function supportsNotifications(): boolean;
|
|
28
|
+
export declare function readNotificationPermission(): NotificationPermissionState;
|
|
29
|
+
/**
|
|
30
|
+
* Whether asking is worth doing. A browser only prompts from the `default`
|
|
31
|
+
* state: once denied, calling again resolves to `denied` without showing
|
|
32
|
+
* anything, so an interface that offers to ask is offering nothing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function canRequestNotificationPermission(permission: NotificationPermissionState): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Asks the user, and returns where that left things.
|
|
37
|
+
*
|
|
38
|
+
* The browser will only prompt in response to something the user did, so call
|
|
39
|
+
* this from an event handler rather than on load, where it is either ignored or
|
|
40
|
+
* spent on someone who has not met the feature yet.
|
|
41
|
+
*/
|
|
42
|
+
export declare function requestNotificationPermission(): Promise<NotificationPermissionState>;
|
|
43
|
+
/**
|
|
44
|
+
* Shows a notification through the service worker registration, which is the
|
|
45
|
+
* only kind that can carry actions and the only kind that outlives the page.
|
|
46
|
+
*/
|
|
47
|
+
export declare function showNotification(title: string, options?: ShowNotificationOptions): Promise<boolean>;
|
|
48
|
+
//# sourceMappingURL=notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/notifications/notifications.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAE3F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC;kFAC8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED,wBAAgB,0BAA0B,IAAI,2BAA2B,CAGxE;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,2BAA2B,GACtC,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,2BAA2B,CAAC,CAG1F;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,OAAO,CAAC,CAOlB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function supportsNotifications() {
|
|
2
|
+
return typeof window !== "undefined" && "Notification" in window;
|
|
3
|
+
}
|
|
4
|
+
export function readNotificationPermission() {
|
|
5
|
+
if (!supportsNotifications())
|
|
6
|
+
return "unsupported";
|
|
7
|
+
return Notification.permission;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether asking is worth doing. A browser only prompts from the `default`
|
|
11
|
+
* state: once denied, calling again resolves to `denied` without showing
|
|
12
|
+
* anything, so an interface that offers to ask is offering nothing.
|
|
13
|
+
*/
|
|
14
|
+
export function canRequestNotificationPermission(permission) {
|
|
15
|
+
return permission === "default";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Asks the user, and returns where that left things.
|
|
19
|
+
*
|
|
20
|
+
* The browser will only prompt in response to something the user did, so call
|
|
21
|
+
* this from an event handler rather than on load, where it is either ignored or
|
|
22
|
+
* spent on someone who has not met the feature yet.
|
|
23
|
+
*/
|
|
24
|
+
export async function requestNotificationPermission() {
|
|
25
|
+
if (!supportsNotifications())
|
|
26
|
+
return "unsupported";
|
|
27
|
+
return await Notification.requestPermission();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Shows a notification through the service worker registration, which is the
|
|
31
|
+
* only kind that can carry actions and the only kind that outlives the page.
|
|
32
|
+
*/
|
|
33
|
+
export async function showNotification(title, options = {}) {
|
|
34
|
+
if (readNotificationPermission() !== "granted")
|
|
35
|
+
return false;
|
|
36
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator))
|
|
37
|
+
return false;
|
|
38
|
+
const registration = await navigator.serviceWorker.ready;
|
|
39
|
+
await registration.showNotification(title, options);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/notifications/notifications.ts"],"names":[],"mappings":"AA6BA,MAAM,UAAU,qBAAqB;IACnC,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,cAAc,IAAI,MAAM,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,0BAA0B;IACxC,IAAI,CAAC,qBAAqB,EAAE;QAAE,OAAO,aAAa,CAAC;IACnD,OAAO,YAAY,CAAC,UAAU,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gCAAgC,CAC9C,UAAuC;IAEvC,OAAO,UAAU,KAAK,SAAS,CAAC;AAClC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B;IACjD,IAAI,CAAC,qBAAqB,EAAE;QAAE,OAAO,aAAa,CAAC;IACnD,OAAO,MAAM,YAAY,CAAC,iBAAiB,EAAE,CAAC;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAa,EACb,UAAmC,EAAE;IAErC,IAAI,0BAA0B,EAAE,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7D,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC,CAAC,eAAe,IAAI,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtF,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC;IACzD,MAAM,YAAY,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAA8B,CAAC,CAAC;IAC3E,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a VAPID public key into the bytes `pushManager.subscribe` wants.
|
|
3
|
+
*
|
|
4
|
+
* Keys are handed out as base64url, which is base64 with two characters
|
|
5
|
+
* swapped and the padding dropped, and the subscribe call takes neither a
|
|
6
|
+
* string nor base64. Every application that sends push writes this function,
|
|
7
|
+
* usually by copying it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decodeVapidKey(base64Url: string): Uint8Array<ArrayBuffer>;
|
|
10
|
+
export declare function supportsPush(): boolean;
|
|
11
|
+
/** The subscription this browser already has, or null. */
|
|
12
|
+
export declare function getPushSubscription(): Promise<PushSubscription | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes this browser to push, or returns the subscription it already has.
|
|
15
|
+
*
|
|
16
|
+
* `userVisibleOnly` is not an option: browsers require it, and one of them
|
|
17
|
+
* refuses the subscription outright without it. Every push shows a
|
|
18
|
+
* notification, which is what Adjacent's service worker does with one.
|
|
19
|
+
*
|
|
20
|
+
* Send what this returns to your own server. Adjacent has no opinion about
|
|
21
|
+
* where subscriptions are kept.
|
|
22
|
+
*/
|
|
23
|
+
export declare function subscribeToPush(vapidPublicKey: string): Promise<PushSubscription | null>;
|
|
24
|
+
/** Ends the subscription. False when there was none to end. */
|
|
25
|
+
export declare function unsubscribeFromPush(): Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/notifications/push.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAazE;AAED,wBAAgB,YAAY,IAAI,OAAO,CAMtC;AAED,0DAA0D;AAC1D,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAI5E;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAWlC;AAED,+DAA+D;AAC/D,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAI5D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a VAPID public key into the bytes `pushManager.subscribe` wants.
|
|
3
|
+
*
|
|
4
|
+
* Keys are handed out as base64url, which is base64 with two characters
|
|
5
|
+
* swapped and the padding dropped, and the subscribe call takes neither a
|
|
6
|
+
* string nor base64. Every application that sends push writes this function,
|
|
7
|
+
* usually by copying it.
|
|
8
|
+
*/
|
|
9
|
+
export function decodeVapidKey(base64Url) {
|
|
10
|
+
const padded = base64Url.padEnd(base64Url.length + ((4 - (base64Url.length % 4)) % 4), "=");
|
|
11
|
+
const binary = atob(padded.replaceAll("-", "+").replaceAll("_", "/"));
|
|
12
|
+
// Built rather than mapped from the string: `subscribe` wants a view backed
|
|
13
|
+
// by an ArrayBuffer, and `Uint8Array.from` is typed over any buffer kind.
|
|
14
|
+
const bytes = new Uint8Array(binary.length);
|
|
15
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
16
|
+
bytes[index] = binary.charCodeAt(index);
|
|
17
|
+
}
|
|
18
|
+
return bytes;
|
|
19
|
+
}
|
|
20
|
+
export function supportsPush() {
|
|
21
|
+
return (typeof navigator !== "undefined" &&
|
|
22
|
+
"serviceWorker" in navigator &&
|
|
23
|
+
typeof PushManager !== "undefined");
|
|
24
|
+
}
|
|
25
|
+
/** The subscription this browser already has, or null. */
|
|
26
|
+
export async function getPushSubscription() {
|
|
27
|
+
if (!supportsPush())
|
|
28
|
+
return null;
|
|
29
|
+
const registration = await navigator.serviceWorker.ready;
|
|
30
|
+
return await registration.pushManager.getSubscription();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Subscribes this browser to push, or returns the subscription it already has.
|
|
34
|
+
*
|
|
35
|
+
* `userVisibleOnly` is not an option: browsers require it, and one of them
|
|
36
|
+
* refuses the subscription outright without it. Every push shows a
|
|
37
|
+
* notification, which is what Adjacent's service worker does with one.
|
|
38
|
+
*
|
|
39
|
+
* Send what this returns to your own server. Adjacent has no opinion about
|
|
40
|
+
* where subscriptions are kept.
|
|
41
|
+
*/
|
|
42
|
+
export async function subscribeToPush(vapidPublicKey) {
|
|
43
|
+
if (!supportsPush())
|
|
44
|
+
return null;
|
|
45
|
+
const registration = await navigator.serviceWorker.ready;
|
|
46
|
+
const existing = await registration.pushManager.getSubscription();
|
|
47
|
+
if (existing)
|
|
48
|
+
return existing;
|
|
49
|
+
return await registration.pushManager.subscribe({
|
|
50
|
+
applicationServerKey: decodeVapidKey(vapidPublicKey),
|
|
51
|
+
userVisibleOnly: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Ends the subscription. False when there was none to end. */
|
|
55
|
+
export async function unsubscribeFromPush() {
|
|
56
|
+
const subscription = await getPushSubscription();
|
|
57
|
+
if (!subscription)
|
|
58
|
+
return false;
|
|
59
|
+
return await subscription.unsubscribe();
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/notifications/push.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAC7B,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EACrD,GAAG,CACJ,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACtE,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,CACL,OAAO,SAAS,KAAK,WAAW;QAChC,eAAe,IAAI,SAAS;QAC5B,OAAO,WAAW,KAAK,WAAW,CACnC,CAAC;AACJ,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,IAAI,CAAC,YAAY,EAAE;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC;IACzD,OAAO,MAAM,YAAY,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC;AAC1D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,cAAsB;IAEtB,IAAI,CAAC,YAAY,EAAE;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC;IACzD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC;IAClE,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,OAAO,MAAM,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC;QAC9C,oBAAoB,EAAE,cAAc,CAAC,cAAc,CAAC;QACpD,eAAe,EAAE,IAAI;KACtB,CAAC,CAAC;AACL,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,YAAY,GAAG,MAAM,mBAAmB,EAAE,CAAC;IACjD,IAAI,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAChC,OAAO,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type NotificationPermissionState, type ShowNotificationOptions } from "./notifications.js";
|
|
2
|
+
export interface UseNotificationsResult {
|
|
3
|
+
/** Whether asking would show anything. False once denied, and once granted. */
|
|
4
|
+
readonly canRequest: boolean;
|
|
5
|
+
readonly permission: NotificationPermissionState;
|
|
6
|
+
/** Call from a click. The browser ignores a request that follows nothing. */
|
|
7
|
+
readonly request: () => Promise<NotificationPermissionState>;
|
|
8
|
+
/** Resolves false when permission is missing, rather than throwing. */
|
|
9
|
+
readonly show: (title: string, options?: ShowNotificationOptions) => Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
export declare function useNotifications(): UseNotificationsResult;
|
|
12
|
+
//# sourceMappingURL=use-notifications.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-notifications.d.ts","sourceRoot":"","sources":["../../src/notifications/use-notifications.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,2BAA2B,EAChC,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAmD5B,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,2BAA2B,CAAC;IACjD,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAC7D,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,uBAAuB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvF;AAED,wBAAgB,gBAAgB,IAAI,sBAAsB,CAezD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
3
|
+
import { canRequestNotificationPermission, readNotificationPermission, requestNotificationPermission, showNotification, } from "./notifications.js";
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
let snapshot;
|
|
6
|
+
let watching = false;
|
|
7
|
+
function publish() {
|
|
8
|
+
const next = readNotificationPermission();
|
|
9
|
+
if (next === snapshot)
|
|
10
|
+
return;
|
|
11
|
+
snapshot = next;
|
|
12
|
+
for (const listener of listeners)
|
|
13
|
+
listener();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The permission can change without this application doing anything, because
|
|
17
|
+
* the user can revoke it in browser settings while the page is open. The
|
|
18
|
+
* Permissions API reports that; where it is missing the value is still correct
|
|
19
|
+
* after any request, which is the only change an application causes itself.
|
|
20
|
+
*/
|
|
21
|
+
function watchPermission() {
|
|
22
|
+
if (watching || typeof navigator === "undefined" || !navigator.permissions)
|
|
23
|
+
return;
|
|
24
|
+
watching = true;
|
|
25
|
+
navigator.permissions
|
|
26
|
+
.query({ name: "notifications" })
|
|
27
|
+
.then((status) => {
|
|
28
|
+
status.addEventListener("change", publish);
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
// Queried by a name this browser does not know. The value stays correct
|
|
32
|
+
// after a request, which is the common case.
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function subscribe(listener) {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
watchPermission();
|
|
38
|
+
return () => listeners.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
function getSnapshot() {
|
|
41
|
+
snapshot ??= readNotificationPermission();
|
|
42
|
+
return snapshot;
|
|
43
|
+
}
|
|
44
|
+
function getServerSnapshot() {
|
|
45
|
+
return "unsupported";
|
|
46
|
+
}
|
|
47
|
+
export function useNotifications() {
|
|
48
|
+
const permission = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
49
|
+
const request = useCallback(async () => {
|
|
50
|
+
const result = await requestNotificationPermission();
|
|
51
|
+
publish();
|
|
52
|
+
return result;
|
|
53
|
+
}, []);
|
|
54
|
+
return {
|
|
55
|
+
canRequest: canRequestNotificationPermission(permission),
|
|
56
|
+
permission,
|
|
57
|
+
request,
|
|
58
|
+
show: showNotification,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=use-notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-notifications.js","sourceRoot":"","sources":["../../src/notifications/use-notifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE1D,OAAO,EACL,gCAAgC,EAChC,0BAA0B,EAC1B,6BAA6B,EAC7B,gBAAgB,GAGjB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;AACtC,IAAI,QAAiD,CAAC;AACtD,IAAI,QAAQ,GAAG,KAAK,CAAC;AAErB,SAAS,OAAO;IACd,MAAM,IAAI,GAAG,0BAA0B,EAAE,CAAC;IAC1C,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO;IAC9B,QAAQ,GAAG,IAAI,CAAC;IAChB,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe;IACtB,IAAI,QAAQ,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,WAAW;QAAE,OAAO;IACnF,QAAQ,GAAG,IAAI,CAAC;IAEhB,SAAS,CAAC,WAAW;SAClB,KAAK,CAAC,EAAE,IAAI,EAAE,eAAiC,EAAE,CAAC;SAClD,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QACf,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE;QACV,wEAAwE;QACxE,6CAA6C;IAC/C,CAAC,CAAC,CAAC;AACP,CAAC;AAED,SAAS,SAAS,CAAC,QAAkB;IACnC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxB,eAAe,EAAE,CAAC;IAClB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,WAAW;IAClB,QAAQ,KAAK,0BAA0B,EAAE,CAAC;IAC1C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO,aAAa,CAAC;AACvB,CAAC;AAYD,MAAM,UAAU,gBAAgB;IAC9B,MAAM,UAAU,GAAG,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAEnF,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,MAAM,MAAM,GAAG,MAAM,6BAA6B,EAAE,CAAC;QACrD,OAAO,EAAE,CAAC;QACV,OAAO,MAAM,CAAC;IAChB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QACL,UAAU,EAAE,gCAAgC,CAAC,UAAU,CAAC;QACxD,UAAU;QACV,OAAO;QACP,IAAI,EAAE,gBAAgB;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type AdjacentCacheTarget, type AdjacentWorkerResponse } from "adjacent-core";
|
|
2
|
+
export type ConnectivityState = "offline" | "online";
|
|
3
|
+
export type ServiceWorkerState = {
|
|
4
|
+
readonly status: "checking";
|
|
5
|
+
} | {
|
|
6
|
+
readonly status: "unsupported";
|
|
7
|
+
} | {
|
|
8
|
+
readonly status: "registering";
|
|
9
|
+
} | {
|
|
10
|
+
readonly status: "ready";
|
|
11
|
+
} | {
|
|
12
|
+
readonly status: "update-available";
|
|
13
|
+
} | {
|
|
14
|
+
readonly error: Error;
|
|
15
|
+
readonly status: "error";
|
|
16
|
+
};
|
|
17
|
+
export interface AdjacentRuntimeSnapshot {
|
|
18
|
+
readonly connectivity: ConnectivityState;
|
|
19
|
+
readonly serviceWorker: ServiceWorkerState;
|
|
20
|
+
}
|
|
21
|
+
type Listener = () => void;
|
|
22
|
+
export declare function isWorkerResponse(value: unknown): value is AdjacentWorkerResponse;
|
|
23
|
+
declare class RuntimeStore {
|
|
24
|
+
private listening;
|
|
25
|
+
private readonly listeners;
|
|
26
|
+
private readonly observedRegistrations;
|
|
27
|
+
private registration?;
|
|
28
|
+
private snapshot;
|
|
29
|
+
readonly subscribe: (listener: Listener) => (() => void);
|
|
30
|
+
readonly getSnapshot: () => AdjacentRuntimeSnapshot;
|
|
31
|
+
readonly getServerSnapshot: () => AdjacentRuntimeSnapshot;
|
|
32
|
+
readonly activateUpdate: () => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Asks the browser to re-fetch the worker script, and answers whether a new
|
|
35
|
+
* one is now waiting.
|
|
36
|
+
*
|
|
37
|
+
* Browsers check for a new worker on navigation, so an application that is
|
|
38
|
+
* opened and left open never learns that it was redeployed. An installed PWA
|
|
39
|
+
* is exactly that: opened once and left. Call this on an interval, or when
|
|
40
|
+
* the window regains focus.
|
|
41
|
+
*
|
|
42
|
+
* Resolves false rather than throwing when the script cannot be fetched,
|
|
43
|
+
* because being offline is the ordinary case for the applications this is
|
|
44
|
+
* for, and `connectivity` already says so.
|
|
45
|
+
*/
|
|
46
|
+
readonly checkForUpdate: () => Promise<boolean>;
|
|
47
|
+
readonly clearCache: (target?: AdjacentCacheTarget) => Promise<void>;
|
|
48
|
+
readonly revalidate: (urls: readonly string[]) => Promise<void>;
|
|
49
|
+
private start;
|
|
50
|
+
private readonly updateConnectivity;
|
|
51
|
+
private observeRegistration;
|
|
52
|
+
private sendToController;
|
|
53
|
+
private send;
|
|
54
|
+
private patch;
|
|
55
|
+
}
|
|
56
|
+
export declare const runtimeStore: RuntimeStore;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=runtime-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-store.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAExB,KAAK,sBAAsB,EAC5B,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErD,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GACvC;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;CAC5C;AAED,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAY3B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,sBAAsB,CAKhF;AAED,cAAM,YAAY;IAChB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IACjD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA4C;IAClF,OAAO,CAAC,YAAY,CAAC,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAA4C;IAE5D,QAAQ,CAAC,SAAS,GAAI,UAAU,QAAQ,KAAG,CAAC,MAAM,IAAI,CAAC,CAIrD;IAEF,QAAQ,CAAC,WAAW,QAAO,uBAAuB,CAAkB;IACpE,QAAQ,CAAC,iBAAiB,QAAO,uBAAuB,CAAoB;IAE5E,QAAQ,CAAC,cAAc,QAAa,OAAO,CAAC,IAAI,CAAC,CAY/C;IAEF;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,cAAc,QAAa,OAAO,CAAC,OAAO,CAAC,CAelD;IAEF,QAAQ,CAAC,UAAU,GAAU,SAAQ,mBAA2B,KAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;IAEF,QAAQ,CAAC,UAAU,GAAU,MAAM,SAAS,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;IAEF,OAAO,CAAC,KAAK;IAoBb,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAEjC;IAEF,OAAO,CAAC,mBAAmB;YAuBb,gBAAgB;IAW9B,OAAO,CAAC,IAAI;IAqBZ,OAAO,CAAC,KAAK;CAId;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { WORKER_COMMAND, } from "adjacent-core";
|
|
2
|
+
const SERVER_SNAPSHOT = {
|
|
3
|
+
connectivity: "online",
|
|
4
|
+
serviceWorker: { status: "checking" },
|
|
5
|
+
};
|
|
6
|
+
const MESSAGE_TIMEOUT_MS = 5_000;
|
|
7
|
+
function toError(error) {
|
|
8
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
9
|
+
}
|
|
10
|
+
export function isWorkerResponse(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || !("ok" in value))
|
|
12
|
+
return false;
|
|
13
|
+
const response = value;
|
|
14
|
+
return response.ok === true ||
|
|
15
|
+
(response.ok === false && typeof response.error === "string");
|
|
16
|
+
}
|
|
17
|
+
class RuntimeStore {
|
|
18
|
+
listening = false;
|
|
19
|
+
listeners = new Set();
|
|
20
|
+
observedRegistrations = new WeakSet();
|
|
21
|
+
registration;
|
|
22
|
+
snapshot = SERVER_SNAPSHOT;
|
|
23
|
+
subscribe = (listener) => {
|
|
24
|
+
this.listeners.add(listener);
|
|
25
|
+
this.start();
|
|
26
|
+
return () => this.listeners.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
getSnapshot = () => this.snapshot;
|
|
29
|
+
getServerSnapshot = () => SERVER_SNAPSHOT;
|
|
30
|
+
activateUpdate = async () => {
|
|
31
|
+
const waiting = this.registration?.waiting;
|
|
32
|
+
if (!waiting)
|
|
33
|
+
throw new Error("No Adjacent service worker update is waiting.");
|
|
34
|
+
const controllerChanged = new Promise((resolve) => {
|
|
35
|
+
navigator.serviceWorker.addEventListener("controllerchange", () => resolve(), {
|
|
36
|
+
once: true,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
await this.send(waiting, { type: WORKER_COMMAND.activateUpdate });
|
|
40
|
+
await controllerChanged;
|
|
41
|
+
location.reload();
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Asks the browser to re-fetch the worker script, and answers whether a new
|
|
45
|
+
* one is now waiting.
|
|
46
|
+
*
|
|
47
|
+
* Browsers check for a new worker on navigation, so an application that is
|
|
48
|
+
* opened and left open never learns that it was redeployed. An installed PWA
|
|
49
|
+
* is exactly that: opened once and left. Call this on an interval, or when
|
|
50
|
+
* the window regains focus.
|
|
51
|
+
*
|
|
52
|
+
* Resolves false rather than throwing when the script cannot be fetched,
|
|
53
|
+
* because being offline is the ordinary case for the applications this is
|
|
54
|
+
* for, and `connectivity` already says so.
|
|
55
|
+
*/
|
|
56
|
+
checkForUpdate = async () => {
|
|
57
|
+
if (!("serviceWorker" in navigator))
|
|
58
|
+
return false;
|
|
59
|
+
const registration = this.registration ?? (await navigator.serviceWorker.ready);
|
|
60
|
+
this.registration = registration;
|
|
61
|
+
try {
|
|
62
|
+
await registration.update();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const waiting = registration.waiting !== null;
|
|
68
|
+
if (waiting)
|
|
69
|
+
this.patch({ serviceWorker: { status: "update-available" } });
|
|
70
|
+
return waiting;
|
|
71
|
+
};
|
|
72
|
+
clearCache = async (target = "all") => {
|
|
73
|
+
await this.sendToController({ target, type: WORKER_COMMAND.clearCache });
|
|
74
|
+
};
|
|
75
|
+
revalidate = async (urls) => {
|
|
76
|
+
if (urls.length === 0)
|
|
77
|
+
return;
|
|
78
|
+
await this.sendToController({ type: WORKER_COMMAND.revalidate, urls });
|
|
79
|
+
};
|
|
80
|
+
start() {
|
|
81
|
+
if (this.listening || typeof window === "undefined")
|
|
82
|
+
return;
|
|
83
|
+
this.listening = true;
|
|
84
|
+
this.patch({
|
|
85
|
+
connectivity: navigator.onLine ? "online" : "offline",
|
|
86
|
+
serviceWorker: "serviceWorker" in navigator
|
|
87
|
+
? { status: "checking" }
|
|
88
|
+
: { status: "unsupported" },
|
|
89
|
+
});
|
|
90
|
+
window.addEventListener("online", this.updateConnectivity);
|
|
91
|
+
window.addEventListener("offline", this.updateConnectivity);
|
|
92
|
+
if ("serviceWorker" in navigator) {
|
|
93
|
+
void navigator.serviceWorker.ready
|
|
94
|
+
.then((registration) => this.observeRegistration(registration))
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
this.patch({ serviceWorker: { error: toError(error), status: "error" } });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
updateConnectivity = () => {
|
|
101
|
+
this.patch({ connectivity: navigator.onLine ? "online" : "offline" });
|
|
102
|
+
};
|
|
103
|
+
observeRegistration(registration) {
|
|
104
|
+
this.registration = registration;
|
|
105
|
+
this.patch({
|
|
106
|
+
serviceWorker: registration.waiting
|
|
107
|
+
? { status: "update-available" }
|
|
108
|
+
: { status: "ready" },
|
|
109
|
+
});
|
|
110
|
+
if (this.observedRegistrations.has(registration))
|
|
111
|
+
return;
|
|
112
|
+
this.observedRegistrations.add(registration);
|
|
113
|
+
registration.addEventListener("updatefound", () => {
|
|
114
|
+
const installing = registration.installing;
|
|
115
|
+
if (!installing)
|
|
116
|
+
return;
|
|
117
|
+
installing.addEventListener("statechange", () => {
|
|
118
|
+
if (installing.state !== "installed")
|
|
119
|
+
return;
|
|
120
|
+
this.patch({
|
|
121
|
+
serviceWorker: navigator.serviceWorker.controller
|
|
122
|
+
? { status: "update-available" }
|
|
123
|
+
: { status: "ready" },
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async sendToController(command) {
|
|
129
|
+
if (!("serviceWorker" in navigator)) {
|
|
130
|
+
throw new Error("Service workers are not supported in this browser.");
|
|
131
|
+
}
|
|
132
|
+
const registration = this.registration ?? await navigator.serviceWorker.ready;
|
|
133
|
+
this.registration = registration;
|
|
134
|
+
const worker = navigator.serviceWorker.controller ?? registration.active;
|
|
135
|
+
if (!worker)
|
|
136
|
+
throw new Error("The Adjacent service worker is not active yet.");
|
|
137
|
+
await this.send(worker, command);
|
|
138
|
+
}
|
|
139
|
+
send(worker, command) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const channel = new MessageChannel();
|
|
142
|
+
const timeout = window.setTimeout(() => {
|
|
143
|
+
reject(new Error(`Adjacent service worker command timed out: ${command.type}`));
|
|
144
|
+
}, MESSAGE_TIMEOUT_MS);
|
|
145
|
+
channel.port1.onmessage = ({ data }) => {
|
|
146
|
+
window.clearTimeout(timeout);
|
|
147
|
+
if (!isWorkerResponse(data)) {
|
|
148
|
+
reject(new Error("Adjacent service worker returned an invalid response."));
|
|
149
|
+
}
|
|
150
|
+
else if (!data.ok) {
|
|
151
|
+
reject(new Error(data.error));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
resolve();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
worker.postMessage(command, [channel.port2]);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
patch(patch) {
|
|
161
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
162
|
+
this.listeners.forEach((listener) => listener());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export const runtimeStore = new RuntimeStore();
|
|
166
|
+
//# sourceMappingURL=runtime-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-store.js","sourceRoot":"","sources":["../../src/runtime/runtime-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AdjacentCacheTarget } from "adjacent-core";
|
|
2
|
+
import { type UseInstallResult } from "../install/use-install.js";
|
|
3
|
+
import { type ConnectivityState, type ServiceWorkerState } from "./runtime-store.js";
|
|
4
|
+
export interface UseAdjacentResult extends UseInstallResult {
|
|
5
|
+
readonly activateUpdate: () => Promise<void>;
|
|
6
|
+
/** Asks whether a new worker has been deployed. See `runtimeStore.checkForUpdate`. */
|
|
7
|
+
readonly checkForUpdate: () => Promise<boolean>;
|
|
8
|
+
readonly clearCache: (target?: AdjacentCacheTarget) => Promise<void>;
|
|
9
|
+
readonly connectivity: ConnectivityState;
|
|
10
|
+
readonly revalidate: (urls: readonly string[]) => Promise<void>;
|
|
11
|
+
readonly serviceWorker: ServiceWorkerState;
|
|
12
|
+
}
|
|
13
|
+
export declare function useAdjacent(): UseAdjacentResult;
|
|
14
|
+
//# sourceMappingURL=use-adjacent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-adjacent.d.ts","sourceRoot":"","sources":["../../src/runtime/use-adjacent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGzD,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,QAAQ,CAAC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,sFAAsF;IACtF,QAAQ,CAAC,cAAc,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,aAAa,EAAE,kBAAkB,CAAC;CAC5C;AAED,wBAAgB,WAAW,IAAI,iBAAiB,CAiB/C"}
|