@cubist-labs/cubesigner-sdk-browser-storage 0.4.21-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.
@@ -0,0 +1,34 @@
1
+ import type { SessionData, SessionManager, SessionMetadata } from "@cubist-labs/cubesigner-sdk";
2
+ import { EventEmitter } from "@cubist-labs/cubesigner-sdk";
3
+ type BrowserStorageEvents = {
4
+ logout(): void;
5
+ login(): void;
6
+ };
7
+ /**
8
+ * A manager that persists into browser storage and uses weblocks to safely perform refreshes
9
+ */
10
+ export declare class BrowserStorageManager extends EventEmitter<BrowserStorageEvents> implements SessionManager {
11
+ #private;
12
+ /**
13
+ *
14
+ * @param {string} key The storage key to use
15
+ * @param {Storage} [storage] The storage object to use (defaults to localStorage)
16
+ */
17
+ constructor(key: string, storage?: Storage);
18
+ /**
19
+ * Loads the metadata for a session from storage
20
+ * @return {SessionMetadata} The session metadata
21
+ */
22
+ metadata(): Promise<SessionMetadata>;
23
+ /**
24
+ * Loads the current access token from storage
25
+ * @return {string} The access token
26
+ */
27
+ token(): Promise<string>;
28
+ /**
29
+ * Directly set the session (updating all consumers of the session storage)
30
+ * @param {SessionData} [session] The new session
31
+ */
32
+ setSession(session?: SessionData): Promise<any>;
33
+ }
34
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _BrowserStorageManager_instances, _BrowserStorageManager_key, _BrowserStorageManager_storage, _BrowserStorageManager_lock, _BrowserStorageManager_emitIfNecessary, _BrowserStorageManager_loadSession;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.BrowserStorageManager = void 0;
16
+ const cubesigner_sdk_1 = require("@cubist-labs/cubesigner-sdk");
17
+ const locks_1 = require("./locks");
18
+ /**
19
+ * A manager that persists into browser storage and uses weblocks to safely perform refreshes
20
+ */
21
+ class BrowserStorageManager extends cubesigner_sdk_1.EventEmitter {
22
+ /**
23
+ *
24
+ * @param {string} key The storage key to use
25
+ * @param {Storage} [storage] The storage object to use (defaults to localStorage)
26
+ */
27
+ constructor(key, storage = globalThis.localStorage) {
28
+ super();
29
+ _BrowserStorageManager_instances.add(this);
30
+ /** The storage key for the session data */
31
+ _BrowserStorageManager_key.set(this, void 0);
32
+ _BrowserStorageManager_storage.set(this, void 0);
33
+ _BrowserStorageManager_lock.set(this, void 0);
34
+ __classPrivateFieldSet(this, _BrowserStorageManager_key, key, "f");
35
+ __classPrivateFieldSet(this, _BrowserStorageManager_storage, storage, "f");
36
+ (__classPrivateFieldSet(this, _BrowserStorageManager_lock, `CS_SDK_STORAGE_LOCK_${key}`, "f")),
37
+ // Set up listeners to emit events when users log in or log out
38
+ window.addEventListener("storage", (ev) => {
39
+ // We only care about events on our storage object, using our key
40
+ if (ev.storageArea !== storage || ev.key !== key)
41
+ return;
42
+ __classPrivateFieldGet(this, _BrowserStorageManager_instances, "m", _BrowserStorageManager_emitIfNecessary).call(this, ev.oldValue, ev.newValue);
43
+ });
44
+ }
45
+ /**
46
+ * Loads the metadata for a session from storage
47
+ * @return {SessionMetadata} The session metadata
48
+ */
49
+ async metadata() {
50
+ const sess = __classPrivateFieldGet(this, _BrowserStorageManager_instances, "m", _BrowserStorageManager_loadSession).call(this);
51
+ if (!sess) {
52
+ throw new cubesigner_sdk_1.NoSessionFoundError();
53
+ }
54
+ return (0, cubesigner_sdk_1.metadata)(sess);
55
+ }
56
+ /**
57
+ * Loads the current access token from storage
58
+ * @return {string} The access token
59
+ */
60
+ async token() {
61
+ const sess = await (0, locks_1.readTestAndSet)(__classPrivateFieldGet(this, _BrowserStorageManager_lock, "f"), {
62
+ read: () => __classPrivateFieldGet(this, _BrowserStorageManager_instances, "m", _BrowserStorageManager_loadSession).call(this),
63
+ test: (v) => v !== undefined && (0, cubesigner_sdk_1.isStale)(v),
64
+ set: async (oldSession) => {
65
+ if ((0, cubesigner_sdk_1.isRefreshable)(oldSession)) {
66
+ try {
67
+ const newSession = await (0, cubesigner_sdk_1.refresh)(oldSession);
68
+ __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").setItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"), JSON.stringify(newSession));
69
+ console.log("Successfully refreshed");
70
+ }
71
+ catch (e) {
72
+ if (e instanceof cubesigner_sdk_1.ErrResponse && e.status === 403) {
73
+ __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").removeItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"));
74
+ }
75
+ }
76
+ }
77
+ else {
78
+ // If the session is not refreshable, we should remove it
79
+ __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").removeItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"));
80
+ }
81
+ // Notify that the session has changed
82
+ __classPrivateFieldGet(this, _BrowserStorageManager_instances, "m", _BrowserStorageManager_emitIfNecessary).call(this, "", __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").getItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f")));
83
+ },
84
+ });
85
+ if (!sess) {
86
+ throw new cubesigner_sdk_1.NoSessionFoundError();
87
+ }
88
+ return sess.token;
89
+ }
90
+ /**
91
+ * Directly set the session (updating all consumers of the session storage)
92
+ * @param {SessionData} [session] The new session
93
+ */
94
+ async setSession(session) {
95
+ return await navigator.locks.request(__classPrivateFieldGet(this, _BrowserStorageManager_lock, "f"), {
96
+ mode: "exclusive",
97
+ }, async () => {
98
+ const oldValue = __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").getItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"));
99
+ const newValue = session ? JSON.stringify(session) : null;
100
+ // Unlike during refresh, we don't pre-empt the read locks
101
+ // because this operation is synchronous
102
+ if (newValue) {
103
+ __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").setItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"), newValue);
104
+ }
105
+ else {
106
+ __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").removeItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"));
107
+ }
108
+ // Storage events don't fire if the store occurred on the same page,
109
+ // so we have to manually invoke our emit logic
110
+ __classPrivateFieldGet(this, _BrowserStorageManager_instances, "m", _BrowserStorageManager_emitIfNecessary).call(this, oldValue, newValue);
111
+ });
112
+ }
113
+ }
114
+ exports.BrowserStorageManager = BrowserStorageManager;
115
+ _BrowserStorageManager_key = new WeakMap(), _BrowserStorageManager_storage = new WeakMap(), _BrowserStorageManager_lock = new WeakMap(), _BrowserStorageManager_instances = new WeakSet(), _BrowserStorageManager_emitIfNecessary = function _BrowserStorageManager_emitIfNecessary(oldValue, newValue) {
116
+ if (newValue === null && oldValue !== null) {
117
+ this.emit("logout");
118
+ }
119
+ // There is now a session when there didn't used to be.
120
+ else if (oldValue === null && newValue !== null) {
121
+ this.emit("login");
122
+ }
123
+ }, _BrowserStorageManager_loadSession = function _BrowserStorageManager_loadSession() {
124
+ const stored = __classPrivateFieldGet(this, _BrowserStorageManager_storage, "f").getItem(__classPrivateFieldGet(this, _BrowserStorageManager_key, "f"));
125
+ if (stored === null) {
126
+ return;
127
+ }
128
+ return JSON.parse(stored);
129
+ };
130
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7O0FBQ0EsZ0VBUXFDO0FBQ3JDLG1DQUF5QztBQU96Qzs7R0FFRztBQUNILE1BQWEscUJBQ1gsU0FBUSw2QkFBa0M7SUFRMUM7Ozs7T0FJRztJQUNILFlBQVksR0FBVyxFQUFFLFVBQW1CLFVBQVUsQ0FBQyxZQUFZO1FBQ2pFLEtBQUssRUFBRSxDQUFDOztRQVhWLDJDQUEyQztRQUMzQyw2Q0FBYTtRQUNiLGlEQUFrQjtRQUNsQiw4Q0FBYztRQVNaLHVCQUFBLElBQUksOEJBQVEsR0FBRyxNQUFBLENBQUM7UUFDaEIsdUJBQUEsSUFBSSxrQ0FBWSxPQUFPLE1BQUEsQ0FBQztRQUN4QixDQUFDLHVCQUFBLElBQUksK0JBQVMsdUJBQXVCLEdBQUcsRUFBRSxNQUFBLENBQUM7WUFDekMsK0RBQStEO1lBQy9ELE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxFQUFFLEVBQUUsRUFBRTtnQkFDeEMsaUVBQWlFO2dCQUNqRSxJQUFJLEVBQUUsQ0FBQyxXQUFXLEtBQUssT0FBTyxJQUFJLEVBQUUsQ0FBQyxHQUFHLEtBQUssR0FBRztvQkFBRSxPQUFPO2dCQUN6RCx1QkFBQSxJQUFJLGdGQUFpQixNQUFyQixJQUFJLEVBQWtCLEVBQUUsQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1lBQ2xELENBQUMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQztJQStCRDs7O09BR0c7SUFDSCxLQUFLLENBQUMsUUFBUTtRQUNaLE1BQU0sSUFBSSxHQUFHLHVCQUFBLElBQUksNEVBQWEsTUFBakIsSUFBSSxDQUFlLENBQUM7UUFDakMsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1lBQ1YsTUFBTSxJQUFJLG9DQUFtQixFQUFFLENBQUM7UUFDbEMsQ0FBQztRQUNELE9BQU8sSUFBQSx5QkFBUSxFQUFDLElBQUksQ0FBQyxDQUFDO0lBQ3hCLENBQUM7SUFFRDs7O09BR0c7SUFDSCxLQUFLLENBQUMsS0FBSztRQUNULE1BQU0sSUFBSSxHQUFHLE1BQU0sSUFBQSxzQkFBYyxFQUEwQix1QkFBQSxJQUFJLG1DQUFNLEVBQUU7WUFDckUsSUFBSSxFQUFFLEdBQUcsRUFBRSxDQUFDLHVCQUFBLElBQUksNEVBQWEsTUFBakIsSUFBSSxDQUFlO1lBQy9CLElBQUksRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLLFNBQVMsSUFBSSxJQUFBLHdCQUFPLEVBQUMsQ0FBQyxDQUFDO1lBQzFDLEdBQUcsRUFBRSxLQUFLLEVBQUUsVUFBdUIsRUFBRSxFQUFFO2dCQUNyQyxJQUFJLElBQUEsOEJBQWEsRUFBQyxVQUFVLENBQUMsRUFBRSxDQUFDO29CQUM5QixJQUFJLENBQUM7d0JBQ0gsTUFBTSxVQUFVLEdBQUcsTUFBTSxJQUFBLHdCQUFPLEVBQUMsVUFBVSxDQUFDLENBQUM7d0JBQzdDLHVCQUFBLElBQUksc0NBQVMsQ0FBQyxPQUFPLENBQUMsdUJBQUEsSUFBSSxrQ0FBSyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQzt3QkFDN0QsT0FBTyxDQUFDLEdBQUcsQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO29CQUN4QyxDQUFDO29CQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUM7d0JBQ1gsSUFBSSxDQUFDLFlBQVksNEJBQVcsSUFBSSxDQUFDLENBQUMsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDOzRCQUNqRCx1QkFBQSxJQUFJLHNDQUFTLENBQUMsVUFBVSxDQUFDLHVCQUFBLElBQUksa0NBQUssQ0FBQyxDQUFDO3dCQUN0QyxDQUFDO29CQUNILENBQUM7Z0JBQ0gsQ0FBQztxQkFBTSxDQUFDO29CQUNOLHlEQUF5RDtvQkFDekQsdUJBQUEsSUFBSSxzQ0FBUyxDQUFDLFVBQVUsQ0FBQyx1QkFBQSxJQUFJLGtDQUFLLENBQUMsQ0FBQztnQkFDdEMsQ0FBQztnQkFFRCxzQ0FBc0M7Z0JBQ3RDLHVCQUFBLElBQUksZ0ZBQWlCLE1BQXJCLElBQUksRUFBa0IsRUFBRSxFQUFFLHVCQUFBLElBQUksc0NBQVMsQ0FBQyxPQUFPLENBQUMsdUJBQUEsSUFBSSxrQ0FBSyxDQUFDLENBQUMsQ0FBQztZQUM5RCxDQUFDO1NBQ0YsQ0FBQyxDQUFDO1FBQ0gsSUFBSSxDQUFDLElBQUksRUFBRSxDQUFDO1lBQ1YsTUFBTSxJQUFJLG9DQUFtQixFQUFFLENBQUM7UUFDbEMsQ0FBQztRQUNELE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQztJQUNwQixDQUFDO0lBRUQ7OztPQUdHO0lBQ0gsS0FBSyxDQUFDLFVBQVUsQ0FBQyxPQUFxQjtRQUNwQyxPQUFPLE1BQU0sU0FBUyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQ2xDLHVCQUFBLElBQUksbUNBQU0sRUFDVjtZQUNFLElBQUksRUFBRSxXQUFXO1NBQ2xCLEVBQ0QsS0FBSyxJQUFJLEVBQUU7WUFDVCxNQUFNLFFBQVEsR0FBRyx1QkFBQSxJQUFJLHNDQUFTLENBQUMsT0FBTyxDQUFDLHVCQUFBLElBQUksa0NBQUssQ0FBQyxDQUFDO1lBQ2xELE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO1lBRTFELDBEQUEwRDtZQUMxRCx3Q0FBd0M7WUFDeEMsSUFBSSxRQUFRLEVBQUUsQ0FBQztnQkFDYix1QkFBQSxJQUFJLHNDQUFTLENBQUMsT0FBTyxDQUFDLHVCQUFBLElBQUksa0NBQUssRUFBRSxRQUFRLENBQUMsQ0FBQztZQUM3QyxDQUFDO2lCQUFNLENBQUM7Z0JBQ04sdUJBQUEsSUFBSSxzQ0FBUyxDQUFDLFVBQVUsQ0FBQyx1QkFBQSxJQUFJLGtDQUFLLENBQUMsQ0FBQztZQUN0QyxDQUFDO1lBRUQsb0VBQW9FO1lBQ3BFLCtDQUErQztZQUMvQyx1QkFBQSxJQUFJLGdGQUFpQixNQUFyQixJQUFJLEVBQWtCLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztRQUM1QyxDQUFDLENBQ0YsQ0FBQztJQUNKLENBQUM7Q0FDRjtBQWxJRCxzREFrSUM7b1JBbEdrQixRQUF1QixFQUFFLFFBQXVCO0lBQy9ELElBQUksUUFBUSxLQUFLLElBQUksSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDM0MsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN0QixDQUFDO0lBRUQsdURBQXVEO1NBQ2xELElBQUksUUFBUSxLQUFLLElBQUksSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDaEQsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNyQixDQUFDO0FBQ0gsQ0FBQztJQU9DLE1BQU0sTUFBTSxHQUFHLHVCQUFBLElBQUksc0NBQVMsQ0FBQyxPQUFPLENBQUMsdUJBQUEsSUFBSSxrQ0FBSyxDQUFDLENBQUM7SUFDaEQsSUFBSSxNQUFNLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDcEIsT0FBTztJQUNULENBQUM7SUFFRCxPQUFPLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDNUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0eXBlIHsgU2Vzc2lvbkRhdGEsIFNlc3Npb25NYW5hZ2VyLCBTZXNzaW9uTWV0YWRhdGEgfSBmcm9tIFwiQGN1YmlzdC1sYWJzL2N1YmVzaWduZXItc2RrXCI7XG5pbXBvcnQge1xuICBFdmVudEVtaXR0ZXIsXG4gIE5vU2Vzc2lvbkZvdW5kRXJyb3IsXG4gIGlzU3RhbGUsXG4gIGlzUmVmcmVzaGFibGUsXG4gIG1ldGFkYXRhLFxuICByZWZyZXNoLFxuICBFcnJSZXNwb25zZSxcbn0gZnJvbSBcIkBjdWJpc3QtbGFicy9jdWJlc2lnbmVyLXNka1wiO1xuaW1wb3J0IHsgcmVhZFRlc3RBbmRTZXQgfSBmcm9tIFwiLi9sb2Nrc1wiO1xuXG50eXBlIEJyb3dzZXJTdG9yYWdlRXZlbnRzID0ge1xuICBsb2dvdXQoKTogdm9pZDtcbiAgbG9naW4oKTogdm9pZDtcbn07XG5cbi8qKlxuICogQSBtYW5hZ2VyIHRoYXQgcGVyc2lzdHMgaW50byBicm93c2VyIHN0b3JhZ2UgYW5kIHVzZXMgd2VibG9ja3MgdG8gc2FmZWx5IHBlcmZvcm0gcmVmcmVzaGVzXG4gKi9cbmV4cG9ydCBjbGFzcyBCcm93c2VyU3RvcmFnZU1hbmFnZXJcbiAgZXh0ZW5kcyBFdmVudEVtaXR0ZXI8QnJvd3NlclN0b3JhZ2VFdmVudHM+XG4gIGltcGxlbWVudHMgU2Vzc2lvbk1hbmFnZXJcbntcbiAgLyoqIFRoZSBzdG9yYWdlIGtleSBmb3IgdGhlIHNlc3Npb24gZGF0YSAqL1xuICAja2V5OiBzdHJpbmc7XG4gICNzdG9yYWdlOiBTdG9yYWdlO1xuICAjbG9jazogc3RyaW5nO1xuXG4gIC8qKlxuICAgKlxuICAgKiBAcGFyYW0ge3N0cmluZ30ga2V5IFRoZSBzdG9yYWdlIGtleSB0byB1c2VcbiAgICogQHBhcmFtIHtTdG9yYWdlfSBbc3RvcmFnZV0gVGhlIHN0b3JhZ2Ugb2JqZWN0IHRvIHVzZSAoZGVmYXVsdHMgdG8gbG9jYWxTdG9yYWdlKVxuICAgKi9cbiAgY29uc3RydWN0b3Ioa2V5OiBzdHJpbmcsIHN0b3JhZ2U6IFN0b3JhZ2UgPSBnbG9iYWxUaGlzLmxvY2FsU3RvcmFnZSkge1xuICAgIHN1cGVyKCk7XG4gICAgdGhpcy4ja2V5ID0ga2V5O1xuICAgIHRoaXMuI3N0b3JhZ2UgPSBzdG9yYWdlO1xuICAgICh0aGlzLiNsb2NrID0gYENTX1NES19TVE9SQUdFX0xPQ0tfJHtrZXl9YCksXG4gICAgICAvLyBTZXQgdXAgbGlzdGVuZXJzIHRvIGVtaXQgZXZlbnRzIHdoZW4gdXNlcnMgbG9nIGluIG9yIGxvZyBvdXRcbiAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKFwic3RvcmFnZVwiLCAoZXYpID0+IHtcbiAgICAgICAgLy8gV2Ugb25seSBjYXJlIGFib3V0IGV2ZW50cyBvbiBvdXIgc3RvcmFnZSBvYmplY3QsIHVzaW5nIG91ciBrZXlcbiAgICAgICAgaWYgKGV2LnN0b3JhZ2VBcmVhICE9PSBzdG9yYWdlIHx8IGV2LmtleSAhPT0ga2V5KSByZXR1cm47XG4gICAgICAgIHRoaXMuI2VtaXRJZk5lY2Vzc2FyeShldi5vbGRWYWx1ZSwgZXYubmV3VmFsdWUpO1xuICAgICAgfSk7XG4gIH1cblxuICAvKipcbiAgICogRW1pdHMgYSBgbG9naW5gIG9yIGBsb2dvdXRgIGV2ZW50IGlmIG5lY2Vzc2FyeVxuICAgKiBAcGFyYW0ge251bGwgfCBzdHJpbmd9IG9sZFZhbHVlIFRoZSBwcmV2aW91c2x5IHN0b3JlZCB2YWx1ZVxuICAgKiBAcGFyYW0ge251bGwgfCBzdHJpbmd9IG5ld1ZhbHVlIFRoZSBuZXdseSBzdG9yZWQgdmFsdWVcbiAgICovXG4gICNlbWl0SWZOZWNlc3Nhcnkob2xkVmFsdWU6IG51bGwgfCBzdHJpbmcsIG5ld1ZhbHVlOiBzdHJpbmcgfCBudWxsKSB7XG4gICAgaWYgKG5ld1ZhbHVlID09PSBudWxsICYmIG9sZFZhbHVlICE9PSBudWxsKSB7XG4gICAgICB0aGlzLmVtaXQoXCJsb2dvdXRcIik7XG4gICAgfVxuXG4gICAgLy8gVGhlcmUgaXMgbm93IGEgc2Vzc2lvbiB3aGVuIHRoZXJlIGRpZG4ndCB1c2VkIHRvIGJlLlxuICAgIGVsc2UgaWYgKG9sZFZhbHVlID09PSBudWxsICYmIG5ld1ZhbHVlICE9PSBudWxsKSB7XG4gICAgICB0aGlzLmVtaXQoXCJsb2dpblwiKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogTG9hZHMgYSBzZXNzaW9uIGZyb20gdGhlIGNvbmZpZ3VyZWQgc3RvcmFnZSBhdCB0aGUgY29uZmlndXJlZCBrZXlcbiAgICogQHJldHVybiB7U2Vzc2lvbkRhdGEgfCB1bmRlZmluZWR9IFRoZSBzdG9yZWQgZGF0YSBvciB1bmRlZmluZWQgaWYgbm90IHByZXNlbnRcbiAgICovXG4gICNsb2FkU2Vzc2lvbigpOiBTZXNzaW9uRGF0YSB8IHVuZGVmaW5lZCB7XG4gICAgY29uc3Qgc3RvcmVkID0gdGhpcy4jc3RvcmFnZS5nZXRJdGVtKHRoaXMuI2tleSk7XG4gICAgaWYgKHN0b3JlZCA9PT0gbnVsbCkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIHJldHVybiBKU09OLnBhcnNlKHN0b3JlZCk7XG4gIH1cblxuICAvKipcbiAgICogTG9hZHMgdGhlIG1ldGFkYXRhIGZvciBhIHNlc3Npb24gZnJvbSBzdG9yYWdlXG4gICAqIEByZXR1cm4ge1Nlc3Npb25NZXRhZGF0YX0gVGhlIHNlc3Npb24gbWV0YWRhdGFcbiAgICovXG4gIGFzeW5jIG1ldGFkYXRhKCk6IFByb21pc2U8U2Vzc2lvbk1ldGFkYXRhPiB7XG4gICAgY29uc3Qgc2VzcyA9IHRoaXMuI2xvYWRTZXNzaW9uKCk7XG4gICAgaWYgKCFzZXNzKSB7XG4gICAgICB0aHJvdyBuZXcgTm9TZXNzaW9uRm91bmRFcnJvcigpO1xuICAgIH1cbiAgICByZXR1cm4gbWV0YWRhdGEoc2Vzcyk7XG4gIH1cblxuICAvKipcbiAgICogTG9hZHMgdGhlIGN1cnJlbnQgYWNjZXNzIHRva2VuIGZyb20gc3RvcmFnZVxuICAgKiBAcmV0dXJuIHtzdHJpbmd9IFRoZSBhY2Nlc3MgdG9rZW5cbiAgICovXG4gIGFzeW5jIHRva2VuKCk6IFByb21pc2U8c3RyaW5nPiB7XG4gICAgY29uc3Qgc2VzcyA9IGF3YWl0IHJlYWRUZXN0QW5kU2V0PFNlc3Npb25EYXRhIHwgdW5kZWZpbmVkPih0aGlzLiNsb2NrLCB7XG4gICAgICByZWFkOiAoKSA9PiB0aGlzLiNsb2FkU2Vzc2lvbigpLFxuICAgICAgdGVzdDogKHYpID0+IHYgIT09IHVuZGVmaW5lZCAmJiBpc1N0YWxlKHYpLFxuICAgICAgc2V0OiBhc3luYyAob2xkU2Vzc2lvbjogU2Vzc2lvbkRhdGEpID0+IHtcbiAgICAgICAgaWYgKGlzUmVmcmVzaGFibGUob2xkU2Vzc2lvbikpIHtcbiAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgY29uc3QgbmV3U2Vzc2lvbiA9IGF3YWl0IHJlZnJlc2gob2xkU2Vzc2lvbik7XG4gICAgICAgICAgICB0aGlzLiNzdG9yYWdlLnNldEl0ZW0odGhpcy4ja2V5LCBKU09OLnN0cmluZ2lmeShuZXdTZXNzaW9uKSk7XG4gICAgICAgICAgICBjb25zb2xlLmxvZyhcIlN1Y2Nlc3NmdWxseSByZWZyZXNoZWRcIik7XG4gICAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgaWYgKGUgaW5zdGFuY2VvZiBFcnJSZXNwb25zZSAmJiBlLnN0YXR1cyA9PT0gNDAzKSB7XG4gICAgICAgICAgICAgIHRoaXMuI3N0b3JhZ2UucmVtb3ZlSXRlbSh0aGlzLiNrZXkpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAvLyBJZiB0aGUgc2Vzc2lvbiBpcyBub3QgcmVmcmVzaGFibGUsIHdlIHNob3VsZCByZW1vdmUgaXRcbiAgICAgICAgICB0aGlzLiNzdG9yYWdlLnJlbW92ZUl0ZW0odGhpcy4ja2V5KTtcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIE5vdGlmeSB0aGF0IHRoZSBzZXNzaW9uIGhhcyBjaGFuZ2VkXG4gICAgICAgIHRoaXMuI2VtaXRJZk5lY2Vzc2FyeShcIlwiLCB0aGlzLiNzdG9yYWdlLmdldEl0ZW0odGhpcy4ja2V5KSk7XG4gICAgICB9LFxuICAgIH0pO1xuICAgIGlmICghc2Vzcykge1xuICAgICAgdGhyb3cgbmV3IE5vU2Vzc2lvbkZvdW5kRXJyb3IoKTtcbiAgICB9XG4gICAgcmV0dXJuIHNlc3MudG9rZW47XG4gIH1cblxuICAvKipcbiAgICogRGlyZWN0bHkgc2V0IHRoZSBzZXNzaW9uICh1cGRhdGluZyBhbGwgY29uc3VtZXJzIG9mIHRoZSBzZXNzaW9uIHN0b3JhZ2UpXG4gICAqIEBwYXJhbSB7U2Vzc2lvbkRhdGF9IFtzZXNzaW9uXSBUaGUgbmV3IHNlc3Npb25cbiAgICovXG4gIGFzeW5jIHNldFNlc3Npb24oc2Vzc2lvbj86IFNlc3Npb25EYXRhKSB7XG4gICAgcmV0dXJuIGF3YWl0IG5hdmlnYXRvci5sb2Nrcy5yZXF1ZXN0KFxuICAgICAgdGhpcy4jbG9jayxcbiAgICAgIHtcbiAgICAgICAgbW9kZTogXCJleGNsdXNpdmVcIixcbiAgICAgIH0sXG4gICAgICBhc3luYyAoKSA9PiB7XG4gICAgICAgIGNvbnN0IG9sZFZhbHVlID0gdGhpcy4jc3RvcmFnZS5nZXRJdGVtKHRoaXMuI2tleSk7XG4gICAgICAgIGNvbnN0IG5ld1ZhbHVlID0gc2Vzc2lvbiA/IEpTT04uc3RyaW5naWZ5KHNlc3Npb24pIDogbnVsbDtcblxuICAgICAgICAvLyBVbmxpa2UgZHVyaW5nIHJlZnJlc2gsIHdlIGRvbid0IHByZS1lbXB0IHRoZSByZWFkIGxvY2tzXG4gICAgICAgIC8vIGJlY2F1c2UgdGhpcyBvcGVyYXRpb24gaXMgc3luY2hyb25vdXNcbiAgICAgICAgaWYgKG5ld1ZhbHVlKSB7XG4gICAgICAgICAgdGhpcy4jc3RvcmFnZS5zZXRJdGVtKHRoaXMuI2tleSwgbmV3VmFsdWUpO1xuICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgIHRoaXMuI3N0b3JhZ2UucmVtb3ZlSXRlbSh0aGlzLiNrZXkpO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gU3RvcmFnZSBldmVudHMgZG9uJ3QgZmlyZSBpZiB0aGUgc3RvcmUgb2NjdXJyZWQgb24gdGhlIHNhbWUgcGFnZSxcbiAgICAgICAgLy8gc28gd2UgaGF2ZSB0byBtYW51YWxseSBpbnZva2Ugb3VyIGVtaXQgbG9naWNcbiAgICAgICAgdGhpcy4jZW1pdElmTmVjZXNzYXJ5KG9sZFZhbHVlLCBuZXdWYWx1ZSk7XG4gICAgICB9LFxuICAgICk7XG4gIH1cbn1cbiJdfQ==
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Used as an argument to {@link readTestAndSet}.
3
+ * Defines the behavior to perform during the locking strategy
4
+ */
5
+ export type ReadTestAndSet<T> = {
6
+ /** How to read the data */
7
+ read(): T;
8
+ /** If true, perform set, if false, return v */
9
+ test(v: T): boolean;
10
+ /** Persist the value */
11
+ set(prev: T): Promise<void>;
12
+ };
13
+ /**
14
+ * A locking primitive for contentious reads and writes.
15
+ *
16
+ * Guarantees:
17
+ * 1. Set is only called when test(read()) returns true
18
+ * 2. Only 1 set() call is active at a time
19
+ * 3. read() never occurs while set() is running
20
+ *
21
+ * @param {string} lock The name of the lock to use
22
+ * @param {ReadTestAndSet} spec The set of read, test, and set functions to use
23
+ * @return {Promise<T>} The current value
24
+ */
25
+ export declare function readTestAndSet<T>(lock: string, { read, test, set }: ReadTestAndSet<T>): Promise<T>;
package/dist/locks.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readTestAndSet = void 0;
4
+ /**
5
+ * A locking primitive for contentious reads and writes.
6
+ *
7
+ * Guarantees:
8
+ * 1. Set is only called when test(read()) returns true
9
+ * 2. Only 1 set() call is active at a time
10
+ * 3. read() never occurs while set() is running
11
+ *
12
+ * @param {string} lock The name of the lock to use
13
+ * @param {ReadTestAndSet} spec The set of read, test, and set functions to use
14
+ * @return {Promise<T>} The current value
15
+ */
16
+ async function readTestAndSet(lock, { read, test, set }) {
17
+ const value = await navigator.locks.request(lock, { mode: "shared" }, async () => read());
18
+ if (test(value)) {
19
+ return navigator.locks.request(lock, { mode: "exclusive" }, async () => {
20
+ const prev = read();
21
+ if (test(prev)) {
22
+ await set(prev);
23
+ return read();
24
+ }
25
+ return prev;
26
+ });
27
+ }
28
+ return value;
29
+ }
30
+ exports.readTestAndSet = readTestAndSet;
31
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9ja3MuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvbG9ja3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBYUE7Ozs7Ozs7Ozs7O0dBV0c7QUFDSSxLQUFLLFVBQVUsY0FBYyxDQUNsQyxJQUFZLEVBQ1osRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEdBQUcsRUFBcUI7SUFFdEMsTUFBTSxLQUFLLEdBQUcsTUFBTSxTQUFTLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLEVBQUUsS0FBSyxJQUFJLEVBQUUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQzFGLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDaEIsT0FBTyxTQUFTLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDckUsTUFBTSxJQUFJLEdBQUcsSUFBSSxFQUFFLENBQUM7WUFDcEIsSUFBSSxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztnQkFDZixNQUFNLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztnQkFDaEIsT0FBTyxJQUFJLEVBQUUsQ0FBQztZQUNoQixDQUFDO1lBQ0QsT0FBTyxJQUFJLENBQUM7UUFDZCxDQUFDLENBQUMsQ0FBQztJQUNMLENBQUM7SUFDRCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUM7QUFoQkQsd0NBZ0JDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBVc2VkIGFzIGFuIGFyZ3VtZW50IHRvIHtAbGluayByZWFkVGVzdEFuZFNldH0uXG4gKiBEZWZpbmVzIHRoZSBiZWhhdmlvciB0byBwZXJmb3JtIGR1cmluZyB0aGUgbG9ja2luZyBzdHJhdGVneVxuICovXG5leHBvcnQgdHlwZSBSZWFkVGVzdEFuZFNldDxUPiA9IHtcbiAgLyoqIEhvdyB0byByZWFkIHRoZSBkYXRhICovXG4gIHJlYWQoKTogVDtcbiAgLyoqIElmIHRydWUsIHBlcmZvcm0gc2V0LCBpZiBmYWxzZSwgcmV0dXJuIHYgKi9cbiAgdGVzdCh2OiBUKTogYm9vbGVhbjtcbiAgLyoqIFBlcnNpc3QgdGhlIHZhbHVlICovXG4gIHNldChwcmV2OiBUKTogUHJvbWlzZTx2b2lkPjtcbn07XG5cbi8qKlxuICogQSBsb2NraW5nIHByaW1pdGl2ZSBmb3IgY29udGVudGlvdXMgcmVhZHMgYW5kIHdyaXRlcy5cbiAqXG4gKiBHdWFyYW50ZWVzOlxuICogIDEuIFNldCBpcyBvbmx5IGNhbGxlZCB3aGVuIHRlc3QocmVhZCgpKSByZXR1cm5zIHRydWVcbiAqICAyLiBPbmx5IDEgc2V0KCkgY2FsbCBpcyBhY3RpdmUgYXQgYSB0aW1lXG4gKiAgMy4gcmVhZCgpIG5ldmVyIG9jY3VycyB3aGlsZSBzZXQoKSBpcyBydW5uaW5nXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGxvY2sgVGhlIG5hbWUgb2YgdGhlIGxvY2sgdG8gdXNlXG4gKiBAcGFyYW0ge1JlYWRUZXN0QW5kU2V0fSBzcGVjIFRoZSBzZXQgb2YgcmVhZCwgdGVzdCwgYW5kIHNldCBmdW5jdGlvbnMgdG8gdXNlXG4gKiBAcmV0dXJuIHtQcm9taXNlPFQ+fSBUaGUgY3VycmVudCB2YWx1ZVxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVhZFRlc3RBbmRTZXQ8VD4oXG4gIGxvY2s6IHN0cmluZyxcbiAgeyByZWFkLCB0ZXN0LCBzZXQgfTogUmVhZFRlc3RBbmRTZXQ8VD4sXG4pOiBQcm9taXNlPFQ+IHtcbiAgY29uc3QgdmFsdWUgPSBhd2FpdCBuYXZpZ2F0b3IubG9ja3MucmVxdWVzdChsb2NrLCB7IG1vZGU6IFwic2hhcmVkXCIgfSwgYXN5bmMgKCkgPT4gcmVhZCgpKTtcbiAgaWYgKHRlc3QodmFsdWUpKSB7XG4gICAgcmV0dXJuIG5hdmlnYXRvci5sb2Nrcy5yZXF1ZXN0KGxvY2ssIHsgbW9kZTogXCJleGNsdXNpdmVcIiB9LCBhc3luYyAoKSA9PiB7XG4gICAgICBjb25zdCBwcmV2ID0gcmVhZCgpO1xuICAgICAgaWYgKHRlc3QocHJldikpIHtcbiAgICAgICAgYXdhaXQgc2V0KHByZXYpO1xuICAgICAgICByZXR1cm4gcmVhZCgpO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHByZXY7XG4gICAgfSk7XG4gIH1cbiAgcmV0dXJuIHZhbHVlO1xufVxuIl19
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@cubist-labs/cubesigner-sdk-browser-storage",
3
+ "version": "0.4.21-0",
4
+ "description": "Browser-based storage for CubeSigner SDK sessions",
5
+ "license": "MIT OR Apache-2.0",
6
+ "author": "Cubist, Inc.",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "tsconfig.json",
11
+ "src/**",
12
+ "dist/**",
13
+ "../../NOTICE",
14
+ "../../LICENSE-APACHE",
15
+ "../../LICENSE-MIT"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepack": "tsc",
20
+ "test:playwright": "playwright test",
21
+ "test:build": "esbuild test/site/testscript.ts --bundle --format=esm --outfile='test/site/testscript.js' --external:crypto",
22
+ "test:server": "http-server ./test/site -c-1"
23
+ },
24
+ "peerDependencies": {
25
+ "@cubist-labs/cubesigner-sdk": "^0.4.21-0"
26
+ },
27
+ "directories": {
28
+ "test": "test"
29
+ },
30
+ "devDependencies": {
31
+ "@cubist-labs/cubesigner-sdk": "^0.4.21-0",
32
+ "@cubist-labs/cubesigner-sdk-fs-storage": "^0.4.21-0",
33
+ "@playwright/test": "^1.42.1",
34
+ "esbuild": "^0.20.1",
35
+ "http-server": "^14.1.1"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,151 @@
1
+ import type { SessionData, SessionManager, SessionMetadata } from "@cubist-labs/cubesigner-sdk";
2
+ import {
3
+ EventEmitter,
4
+ NoSessionFoundError,
5
+ isStale,
6
+ isRefreshable,
7
+ metadata,
8
+ refresh,
9
+ ErrResponse,
10
+ } from "@cubist-labs/cubesigner-sdk";
11
+ import { readTestAndSet } from "./locks";
12
+
13
+ type BrowserStorageEvents = {
14
+ logout(): void;
15
+ login(): void;
16
+ };
17
+
18
+ /**
19
+ * A manager that persists into browser storage and uses weblocks to safely perform refreshes
20
+ */
21
+ export class BrowserStorageManager
22
+ extends EventEmitter<BrowserStorageEvents>
23
+ implements SessionManager
24
+ {
25
+ /** The storage key for the session data */
26
+ #key: string;
27
+ #storage: Storage;
28
+ #lock: string;
29
+
30
+ /**
31
+ *
32
+ * @param {string} key The storage key to use
33
+ * @param {Storage} [storage] The storage object to use (defaults to localStorage)
34
+ */
35
+ constructor(key: string, storage: Storage = globalThis.localStorage) {
36
+ super();
37
+ this.#key = key;
38
+ this.#storage = storage;
39
+ (this.#lock = `CS_SDK_STORAGE_LOCK_${key}`),
40
+ // Set up listeners to emit events when users log in or log out
41
+ window.addEventListener("storage", (ev) => {
42
+ // We only care about events on our storage object, using our key
43
+ if (ev.storageArea !== storage || ev.key !== key) return;
44
+ this.#emitIfNecessary(ev.oldValue, ev.newValue);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Emits a `login` or `logout` event if necessary
50
+ * @param {null | string} oldValue The previously stored value
51
+ * @param {null | string} newValue The newly stored value
52
+ */
53
+ #emitIfNecessary(oldValue: null | string, newValue: string | null) {
54
+ if (newValue === null && oldValue !== null) {
55
+ this.emit("logout");
56
+ }
57
+
58
+ // There is now a session when there didn't used to be.
59
+ else if (oldValue === null && newValue !== null) {
60
+ this.emit("login");
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Loads a session from the configured storage at the configured key
66
+ * @return {SessionData | undefined} The stored data or undefined if not present
67
+ */
68
+ #loadSession(): SessionData | undefined {
69
+ const stored = this.#storage.getItem(this.#key);
70
+ if (stored === null) {
71
+ return;
72
+ }
73
+
74
+ return JSON.parse(stored);
75
+ }
76
+
77
+ /**
78
+ * Loads the metadata for a session from storage
79
+ * @return {SessionMetadata} The session metadata
80
+ */
81
+ async metadata(): Promise<SessionMetadata> {
82
+ const sess = this.#loadSession();
83
+ if (!sess) {
84
+ throw new NoSessionFoundError();
85
+ }
86
+ return metadata(sess);
87
+ }
88
+
89
+ /**
90
+ * Loads the current access token from storage
91
+ * @return {string} The access token
92
+ */
93
+ async token(): Promise<string> {
94
+ const sess = await readTestAndSet<SessionData | undefined>(this.#lock, {
95
+ read: () => this.#loadSession(),
96
+ test: (v) => v !== undefined && isStale(v),
97
+ set: async (oldSession: SessionData) => {
98
+ if (isRefreshable(oldSession)) {
99
+ try {
100
+ const newSession = await refresh(oldSession);
101
+ this.#storage.setItem(this.#key, JSON.stringify(newSession));
102
+ console.log("Successfully refreshed");
103
+ } catch (e) {
104
+ if (e instanceof ErrResponse && e.status === 403) {
105
+ this.#storage.removeItem(this.#key);
106
+ }
107
+ }
108
+ } else {
109
+ // If the session is not refreshable, we should remove it
110
+ this.#storage.removeItem(this.#key);
111
+ }
112
+
113
+ // Notify that the session has changed
114
+ this.#emitIfNecessary("", this.#storage.getItem(this.#key));
115
+ },
116
+ });
117
+ if (!sess) {
118
+ throw new NoSessionFoundError();
119
+ }
120
+ return sess.token;
121
+ }
122
+
123
+ /**
124
+ * Directly set the session (updating all consumers of the session storage)
125
+ * @param {SessionData} [session] The new session
126
+ */
127
+ async setSession(session?: SessionData) {
128
+ return await navigator.locks.request(
129
+ this.#lock,
130
+ {
131
+ mode: "exclusive",
132
+ },
133
+ async () => {
134
+ const oldValue = this.#storage.getItem(this.#key);
135
+ const newValue = session ? JSON.stringify(session) : null;
136
+
137
+ // Unlike during refresh, we don't pre-empt the read locks
138
+ // because this operation is synchronous
139
+ if (newValue) {
140
+ this.#storage.setItem(this.#key, newValue);
141
+ } else {
142
+ this.#storage.removeItem(this.#key);
143
+ }
144
+
145
+ // Storage events don't fire if the store occurred on the same page,
146
+ // so we have to manually invoke our emit logic
147
+ this.#emitIfNecessary(oldValue, newValue);
148
+ },
149
+ );
150
+ }
151
+ }
package/src/locks.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Used as an argument to {@link readTestAndSet}.
3
+ * Defines the behavior to perform during the locking strategy
4
+ */
5
+ export type ReadTestAndSet<T> = {
6
+ /** How to read the data */
7
+ read(): T;
8
+ /** If true, perform set, if false, return v */
9
+ test(v: T): boolean;
10
+ /** Persist the value */
11
+ set(prev: T): Promise<void>;
12
+ };
13
+
14
+ /**
15
+ * A locking primitive for contentious reads and writes.
16
+ *
17
+ * Guarantees:
18
+ * 1. Set is only called when test(read()) returns true
19
+ * 2. Only 1 set() call is active at a time
20
+ * 3. read() never occurs while set() is running
21
+ *
22
+ * @param {string} lock The name of the lock to use
23
+ * @param {ReadTestAndSet} spec The set of read, test, and set functions to use
24
+ * @return {Promise<T>} The current value
25
+ */
26
+ export async function readTestAndSet<T>(
27
+ lock: string,
28
+ { read, test, set }: ReadTestAndSet<T>,
29
+ ): Promise<T> {
30
+ const value = await navigator.locks.request(lock, { mode: "shared" }, async () => read());
31
+ if (test(value)) {
32
+ return navigator.locks.request(lock, { mode: "exclusive" }, async () => {
33
+ const prev = read();
34
+ if (test(prev)) {
35
+ await set(prev);
36
+ return read();
37
+ }
38
+ return prev;
39
+ });
40
+ }
41
+ return value;
42
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "Node16",
5
+ "moduleResolution": "node16",
6
+ "outDir": "./dist"
7
+ },
8
+ "typedocOptions": {
9
+ "out": "./docs",
10
+ "entryPoints": ["src/index.ts"]
11
+ },
12
+ "exclude": ["node_modules", "dist"],
13
+ "include": ["src/**/*.ts"]
14
+ }