@loomup/astro 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +99 -40
- package/package.json +4 -3
package/dist/client.js
CHANGED
|
@@ -27,6 +27,9 @@ function resolveBrowserUrl(explicit) {
|
|
|
27
27
|
throw new Error("@loomup/astro: createBrowserClient requires `url` or PUBLIC_LOOMUP_URL (set via the loomup() integration)");
|
|
28
28
|
}
|
|
29
29
|
let coordinatorSequence = 0;
|
|
30
|
+
const SESSION_LOCK_DATABASE = "@loomup/astro:auth-locks";
|
|
31
|
+
const SESSION_LOCK_STORE = "leases";
|
|
32
|
+
const SESSION_LOCK_TTL_MS = 120_000;
|
|
30
33
|
function delay(milliseconds) {
|
|
31
34
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
32
35
|
}
|
|
@@ -57,9 +60,9 @@ function browserLockManager() {
|
|
|
57
60
|
return undefined;
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
|
-
function
|
|
63
|
+
function browserIndexedDB() {
|
|
61
64
|
try {
|
|
62
|
-
return globalThis.
|
|
65
|
+
return globalThis.indexedDB;
|
|
63
66
|
}
|
|
64
67
|
catch {
|
|
65
68
|
return undefined;
|
|
@@ -72,24 +75,86 @@ function leaseOwner() {
|
|
|
72
75
|
coordinatorSequence += 1;
|
|
73
76
|
return `${Date.now()}-${coordinatorSequence}`;
|
|
74
77
|
}
|
|
75
|
-
function
|
|
78
|
+
function storedLease(value) {
|
|
79
|
+
if (!value || typeof value !== "object")
|
|
80
|
+
return null;
|
|
81
|
+
const lease = value;
|
|
82
|
+
if (typeof lease.owner !== "string" || typeof lease.expiresAt !== "number")
|
|
83
|
+
return null;
|
|
84
|
+
return { owner: lease.owner, expiresAt: lease.expiresAt };
|
|
85
|
+
}
|
|
86
|
+
function openLeaseDatabase(factory) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const request = factory.open(SESSION_LOCK_DATABASE, 1);
|
|
89
|
+
request.onupgradeneeded = () => {
|
|
90
|
+
if (!request.result.objectStoreNames.contains(SESSION_LOCK_STORE)) {
|
|
91
|
+
request.result.createObjectStore(SESSION_LOCK_STORE);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
request.onsuccess = () => resolve(request.result);
|
|
95
|
+
request.onerror = () => reject(request.error ?? new Error("could not open session lock database"));
|
|
96
|
+
request.onblocked = () => reject(new Error("session lock database is blocked"));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function mutateLease(database, key, decide) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const transaction = database.transaction(SESSION_LOCK_STORE, "readwrite");
|
|
102
|
+
const store = transaction.objectStore(SESSION_LOCK_STORE);
|
|
103
|
+
const request = store.get(key);
|
|
104
|
+
let result = false;
|
|
105
|
+
request.onsuccess = () => {
|
|
106
|
+
const mutation = decide(storedLease(request.result));
|
|
107
|
+
result = mutation.result;
|
|
108
|
+
if (mutation.write === "put" && mutation.lease)
|
|
109
|
+
store.put(mutation.lease, key);
|
|
110
|
+
else if (mutation.write === "delete")
|
|
111
|
+
store.delete(key);
|
|
112
|
+
};
|
|
113
|
+
transaction.oncomplete = () => resolve(result);
|
|
114
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("session lock transaction failed"));
|
|
115
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("session lock transaction aborted"));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function acquireLease(database, key, owner) {
|
|
119
|
+
return mutateLease(database, key, (current) => {
|
|
120
|
+
if (current && current.expiresAt > Date.now()) {
|
|
121
|
+
return { result: false, write: "none" };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
result: true,
|
|
125
|
+
write: "put",
|
|
126
|
+
lease: { owner, expiresAt: Date.now() + SESSION_LOCK_TTL_MS },
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function renewLease(database, key, owner) {
|
|
131
|
+
return mutateLease(database, key, (current) => current?.owner === owner
|
|
132
|
+
? {
|
|
133
|
+
result: true,
|
|
134
|
+
write: "put",
|
|
135
|
+
lease: { owner, expiresAt: Date.now() + SESSION_LOCK_TTL_MS },
|
|
136
|
+
}
|
|
137
|
+
: { result: false, write: "none" });
|
|
138
|
+
}
|
|
139
|
+
function releaseLease(database, key, owner) {
|
|
140
|
+
return mutateLease(database, key, (current) => current?.owner === owner
|
|
141
|
+
? { result: true, write: "delete" }
|
|
142
|
+
: { result: false, write: "none" });
|
|
143
|
+
}
|
|
144
|
+
async function withLease(name, operation) {
|
|
145
|
+
const factory = browserIndexedDB();
|
|
146
|
+
if (!factory) {
|
|
147
|
+
if (typeof window === "undefined")
|
|
148
|
+
return operation();
|
|
149
|
+
throw new LoomupError("browser storage cannot coordinate session refresh", "auth_lock_unavailable", 503);
|
|
150
|
+
}
|
|
151
|
+
let database;
|
|
76
152
|
try {
|
|
77
|
-
|
|
78
|
-
if (!raw)
|
|
79
|
-
return null;
|
|
80
|
-
const value = JSON.parse(raw);
|
|
81
|
-
if (typeof value.owner !== "string" || typeof value.expiresAt !== "number")
|
|
82
|
-
return null;
|
|
83
|
-
return { owner: value.owner, expiresAt: value.expiresAt };
|
|
153
|
+
database = await openLeaseDatabase(factory);
|
|
84
154
|
}
|
|
85
155
|
catch {
|
|
86
|
-
|
|
156
|
+
throw new LoomupError("browser storage cannot coordinate session refresh", "auth_lock_unavailable", 503);
|
|
87
157
|
}
|
|
88
|
-
}
|
|
89
|
-
async function withLease(name, operation) {
|
|
90
|
-
const storage = browserStorage();
|
|
91
|
-
if (!storage)
|
|
92
|
-
return operation();
|
|
93
158
|
const key = `@loomup/astro:auth-lock:${name}`;
|
|
94
159
|
const owner = leaseOwner();
|
|
95
160
|
const deadline = Date.now() + 30_000;
|
|
@@ -98,32 +163,25 @@ async function withLease(name, operation) {
|
|
|
98
163
|
: undefined;
|
|
99
164
|
try {
|
|
100
165
|
while (Date.now() < deadline) {
|
|
101
|
-
|
|
102
|
-
|
|
166
|
+
if (await acquireLease(database, key, owner)) {
|
|
167
|
+
let heartbeatInFlight = null;
|
|
168
|
+
const heartbeat = setInterval(() => {
|
|
169
|
+
if (heartbeatInFlight)
|
|
170
|
+
return;
|
|
171
|
+
heartbeatInFlight = renewLease(database, key, owner)
|
|
172
|
+
.catch(() => false)
|
|
173
|
+
.finally(() => { heartbeatInFlight = null; });
|
|
174
|
+
}, 5_000);
|
|
103
175
|
try {
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return operation();
|
|
176
|
+
return await operation();
|
|
108
177
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
}, 5_000);
|
|
118
|
-
try {
|
|
119
|
-
return await operation();
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
clearInterval(heartbeat);
|
|
123
|
-
if (readLease(storage, key)?.owner === owner)
|
|
124
|
-
storage.removeItem(key);
|
|
125
|
-
channel?.postMessage("released");
|
|
126
|
-
}
|
|
178
|
+
finally {
|
|
179
|
+
clearInterval(heartbeat);
|
|
180
|
+
const pendingHeartbeat = heartbeatInFlight;
|
|
181
|
+
if (pendingHeartbeat)
|
|
182
|
+
await pendingHeartbeat;
|
|
183
|
+
await releaseLease(database, key, owner).catch(() => false);
|
|
184
|
+
channel?.postMessage("released");
|
|
127
185
|
}
|
|
128
186
|
}
|
|
129
187
|
await delay(50);
|
|
@@ -132,6 +190,7 @@ async function withLease(name, operation) {
|
|
|
132
190
|
}
|
|
133
191
|
finally {
|
|
134
192
|
channel?.close();
|
|
193
|
+
database.close();
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
async function withBrowserSessionLock(name, operation) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomup/astro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Astro integration and SSR helpers for Loomup Realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,8 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"
|
|
50
|
-
"
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"fake-indexeddb": "^6.2.5",
|
|
51
|
+
"typescript": "^5.7.0"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=18"
|