@ccheever/exact-ibex-runtime 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/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BroadcastChannel - Cross-context messaging API
|
|
3
|
+
*
|
|
4
|
+
* Allows communication between different browsing contexts (views, windows)
|
|
5
|
+
* with the same origin. In Exact, this enables communication between
|
|
6
|
+
* different views within the same app.
|
|
7
|
+
*
|
|
8
|
+
* @see https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { EventTarget } from '../events/EventTarget';
|
|
12
|
+
import { MessageEvent } from '../events/MessageEvent';
|
|
13
|
+
import { structuredClone } from '../clone/structuredClone';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registry of all active BroadcastChannel instances by channel name.
|
|
17
|
+
* This enables message delivery within the same runtime.
|
|
18
|
+
*
|
|
19
|
+
* For cross-view messaging, the native layer should call
|
|
20
|
+
* `BroadcastChannel._deliverMessage(name, data)` when a message arrives
|
|
21
|
+
* from another view.
|
|
22
|
+
*/
|
|
23
|
+
const channelRegistry = new Map<string, Set<BroadcastChannel>>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Native broadcast module interface for cross-view messaging.
|
|
27
|
+
* The native layer should provide this for true cross-context communication.
|
|
28
|
+
*/
|
|
29
|
+
interface NativeBroadcastModule {
|
|
30
|
+
/**
|
|
31
|
+
* Post a message to all other contexts with channels of this name.
|
|
32
|
+
*/
|
|
33
|
+
postMessage(channelName: string, data: unknown): void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register a channel to receive messages from other contexts.
|
|
37
|
+
*/
|
|
38
|
+
registerChannel(channelName: string): void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unregister a channel (called when all local channels with this name close).
|
|
42
|
+
*/
|
|
43
|
+
unregisterChannel(channelName: string): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let nativeBroadcastModule: NativeBroadcastModule | null = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set the native broadcast module for cross-view messaging.
|
|
50
|
+
* Called by the native layer during initialization.
|
|
51
|
+
*/
|
|
52
|
+
export function setNativeBroadcastModule(module: NativeBroadcastModule): void {
|
|
53
|
+
nativeBroadcastModule = module;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* BroadcastChannel enables simple communication between browsing contexts
|
|
58
|
+
* (views, windows, tabs) with the same origin.
|
|
59
|
+
*
|
|
60
|
+
* Messages sent via postMessage() are delivered to all other BroadcastChannel
|
|
61
|
+
* objects with the same name, but NOT to the sender.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // In View A
|
|
66
|
+
* const channel = new BroadcastChannel('my-channel');
|
|
67
|
+
* channel.postMessage({ type: 'update', data: 'hello' });
|
|
68
|
+
*
|
|
69
|
+
* // In View B
|
|
70
|
+
* const channel = new BroadcastChannel('my-channel');
|
|
71
|
+
* channel.onmessage = (event) => {
|
|
72
|
+
* console.log('Received:', event.data);
|
|
73
|
+
* };
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export class BroadcastChannel extends EventTarget {
|
|
77
|
+
readonly name: string;
|
|
78
|
+
|
|
79
|
+
#closed = false;
|
|
80
|
+
|
|
81
|
+
// Event handlers
|
|
82
|
+
onmessage: ((this: BroadcastChannel, ev: MessageEvent) => any) | null = null;
|
|
83
|
+
onmessageerror: ((this: BroadcastChannel, ev: MessageEvent) => any) | null = null;
|
|
84
|
+
|
|
85
|
+
constructor(name: string) {
|
|
86
|
+
super();
|
|
87
|
+
|
|
88
|
+
// Convert to string per spec
|
|
89
|
+
this.name = String(name);
|
|
90
|
+
|
|
91
|
+
// Register in the channel registry
|
|
92
|
+
let channels = channelRegistry.get(this.name);
|
|
93
|
+
if (!channels) {
|
|
94
|
+
channels = new Set();
|
|
95
|
+
channelRegistry.set(this.name, channels);
|
|
96
|
+
|
|
97
|
+
// Notify native layer of new channel name
|
|
98
|
+
nativeBroadcastModule?.registerChannel(this.name);
|
|
99
|
+
}
|
|
100
|
+
channels.add(this);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Send a message to all other BroadcastChannel objects with the same name.
|
|
105
|
+
* The message is NOT delivered to this channel instance.
|
|
106
|
+
*
|
|
107
|
+
* @param message - The message to send (will be structured cloned)
|
|
108
|
+
* @throws DOMException if the channel is closed
|
|
109
|
+
*/
|
|
110
|
+
postMessage(message: unknown): void {
|
|
111
|
+
if (this.#closed) {
|
|
112
|
+
throw new DOMException(
|
|
113
|
+
'BroadcastChannel is closed',
|
|
114
|
+
'InvalidStateError'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clone the message to ensure isolation
|
|
119
|
+
let clonedMessage: unknown;
|
|
120
|
+
try {
|
|
121
|
+
clonedMessage = structuredClone(message);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// If cloning fails, dispatch messageerror to all channels
|
|
124
|
+
this.#dispatchMessageError(error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Deliver to all other local channels with the same name
|
|
129
|
+
const channels = channelRegistry.get(this.name);
|
|
130
|
+
if (channels) {
|
|
131
|
+
for (const channel of channels) {
|
|
132
|
+
if (channel !== this && !channel.#closed) {
|
|
133
|
+
// Queue message delivery asynchronously per spec
|
|
134
|
+
queueMicrotask(() => {
|
|
135
|
+
if (!channel.#closed) {
|
|
136
|
+
channel.#receiveMessage(clonedMessage);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Send to native layer for cross-view delivery
|
|
144
|
+
if (nativeBroadcastModule) {
|
|
145
|
+
try {
|
|
146
|
+
nativeBroadcastModule.postMessage(this.name, clonedMessage);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Native errors are logged but don't affect local delivery
|
|
149
|
+
console.warn('[BroadcastChannel] Native postMessage failed:', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Close this BroadcastChannel. After closing, postMessage() will throw
|
|
156
|
+
* and no more messages will be received.
|
|
157
|
+
*/
|
|
158
|
+
close(): void {
|
|
159
|
+
if (this.#closed) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.#closed = true;
|
|
164
|
+
|
|
165
|
+
// Remove from registry
|
|
166
|
+
const channels = channelRegistry.get(this.name);
|
|
167
|
+
if (channels) {
|
|
168
|
+
channels.delete(this);
|
|
169
|
+
|
|
170
|
+
// If no more channels with this name, clean up
|
|
171
|
+
if (channels.size === 0) {
|
|
172
|
+
channelRegistry.delete(this.name);
|
|
173
|
+
|
|
174
|
+
// Notify native layer
|
|
175
|
+
nativeBroadcastModule?.unregisterChannel(this.name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Internal method to receive a message.
|
|
182
|
+
* Called when a message arrives from another channel.
|
|
183
|
+
*/
|
|
184
|
+
#receiveMessage(data: unknown): void {
|
|
185
|
+
if (this.#closed) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const event = new MessageEvent('message', {
|
|
190
|
+
data,
|
|
191
|
+
origin: '', // Same-origin, no origin needed
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Call onmessage handler
|
|
195
|
+
if (this.onmessage) {
|
|
196
|
+
try {
|
|
197
|
+
this.onmessage.call(this, event);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[BroadcastChannel] onmessage handler threw:', error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Dispatch event
|
|
204
|
+
this.dispatchEvent(event);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Dispatch a messageerror event when message cloning fails.
|
|
209
|
+
*/
|
|
210
|
+
#dispatchMessageError(error: unknown): void {
|
|
211
|
+
const channels = channelRegistry.get(this.name);
|
|
212
|
+
if (channels) {
|
|
213
|
+
for (const channel of channels) {
|
|
214
|
+
if (channel !== this && !channel.#closed) {
|
|
215
|
+
queueMicrotask(() => {
|
|
216
|
+
if (!channel.#closed) {
|
|
217
|
+
const event = new MessageEvent('messageerror', {
|
|
218
|
+
data: error,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (channel.onmessageerror) {
|
|
222
|
+
try {
|
|
223
|
+
channel.onmessageerror.call(channel, event);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error('[BroadcastChannel] onmessageerror handler threw:', e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
channel.dispatchEvent(event);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Static method for native layer to deliver messages from other contexts.
|
|
239
|
+
* This should be called when a message arrives from another view.
|
|
240
|
+
*
|
|
241
|
+
* @param channelName - The channel name
|
|
242
|
+
* @param data - The message data (already cloned by sender)
|
|
243
|
+
*/
|
|
244
|
+
static _deliverMessage(channelName: string, data: unknown): void {
|
|
245
|
+
const channels = channelRegistry.get(channelName);
|
|
246
|
+
if (channels) {
|
|
247
|
+
for (const channel of channels) {
|
|
248
|
+
if (!channel.#closed) {
|
|
249
|
+
queueMicrotask(() => {
|
|
250
|
+
if (!channel.#closed) {
|
|
251
|
+
channel.#receiveMessage(data);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Static method to get the count of active channels for a name.
|
|
261
|
+
* Useful for debugging and testing.
|
|
262
|
+
*/
|
|
263
|
+
static _getChannelCount(channelName: string): number {
|
|
264
|
+
return channelRegistry.get(channelName)?.size ?? 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Static method to get all active channel names.
|
|
269
|
+
* Useful for debugging.
|
|
270
|
+
*/
|
|
271
|
+
static _getChannelNames(): string[] {
|
|
272
|
+
return Array.from(channelRegistry.keys());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get [Symbol.toStringTag](): string {
|
|
276
|
+
return 'BroadcastChannel';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default BroadcastChannel;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache API Implementation (In-Memory)
|
|
3
|
+
*
|
|
4
|
+
* Implements the WHATWG Cache API interface for the Ibex runtime.
|
|
5
|
+
* This is a purely in-memory cache with no persistence.
|
|
6
|
+
*
|
|
7
|
+
* @see https://w3c.github.io/ServiceWorker/#cache-interface
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Request, type RequestInput } from '../fetch/Request';
|
|
11
|
+
import { Response } from '../fetch/Response';
|
|
12
|
+
import { Headers } from '../fetch/Headers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options for Cache matching operations.
|
|
16
|
+
*/
|
|
17
|
+
export interface CacheQueryOptions {
|
|
18
|
+
/** If true, the query string portion of the URL is ignored during matching. */
|
|
19
|
+
ignoreSearch?: boolean;
|
|
20
|
+
/** If true, the HTTP method is ignored during matching (normally only GET is matched). */
|
|
21
|
+
ignoreMethod?: boolean;
|
|
22
|
+
/** If true, the Vary header is ignored during matching. */
|
|
23
|
+
ignoreVary?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Internal cache entry storing a request/response pair.
|
|
28
|
+
*/
|
|
29
|
+
interface CacheEntry {
|
|
30
|
+
request: Request;
|
|
31
|
+
response: Response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip the query string (search) from a URL string.
|
|
36
|
+
*/
|
|
37
|
+
function stripSearch(url: string): string {
|
|
38
|
+
const idx = url.indexOf('?');
|
|
39
|
+
if (idx === -1) return url;
|
|
40
|
+
// Also need to keep the hash if present
|
|
41
|
+
const hashIdx = url.indexOf('#', idx);
|
|
42
|
+
if (hashIdx === -1) return url.substring(0, idx);
|
|
43
|
+
return url.substring(0, idx) + url.substring(hashIdx);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize input into a Request object.
|
|
48
|
+
*/
|
|
49
|
+
function toRequest(input: RequestInput): Request {
|
|
50
|
+
if (input instanceof Request) {
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
return new Request(input);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toFetchHeaders(headers: Headers): Record<string, string> {
|
|
57
|
+
const result: Record<string, string> = {};
|
|
58
|
+
headers.forEach((value, name) => {
|
|
59
|
+
result[name] = value;
|
|
60
|
+
});
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The Cache interface provides a persistent storage mechanism for
|
|
66
|
+
* Request/Response object pairs. In this implementation, storage
|
|
67
|
+
* is purely in-memory with no disk persistence.
|
|
68
|
+
*/
|
|
69
|
+
export class Cache {
|
|
70
|
+
private _entries: CacheEntry[] = [];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a request URL matches a cached entry URL.
|
|
74
|
+
*/
|
|
75
|
+
private _urlMatches(
|
|
76
|
+
cachedUrl: string,
|
|
77
|
+
requestUrl: string,
|
|
78
|
+
options?: CacheQueryOptions
|
|
79
|
+
): boolean {
|
|
80
|
+
if (options?.ignoreSearch) {
|
|
81
|
+
return stripSearch(cachedUrl) === stripSearch(requestUrl);
|
|
82
|
+
}
|
|
83
|
+
return cachedUrl === requestUrl;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a request matches a cached entry considering all options.
|
|
88
|
+
*/
|
|
89
|
+
private _requestMatches(
|
|
90
|
+
cachedRequest: Request,
|
|
91
|
+
request: Request,
|
|
92
|
+
options?: CacheQueryOptions
|
|
93
|
+
): boolean {
|
|
94
|
+
// Check method unless ignoreMethod is set
|
|
95
|
+
if (!options?.ignoreMethod && cachedRequest.method !== request.method) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check URL
|
|
100
|
+
if (!this._urlMatches(cachedRequest.url, request.url, options)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check Vary header unless ignoreVary is set
|
|
105
|
+
if (!options?.ignoreVary) {
|
|
106
|
+
const varyHeader = this._getVaryHeaderForEntry(cachedRequest);
|
|
107
|
+
if (varyHeader) {
|
|
108
|
+
const varyFields = varyHeader.split(',').map((f) => f.trim().toLowerCase());
|
|
109
|
+
if (varyFields.includes('*')) {
|
|
110
|
+
// Vary: * means the response varies on everything, so never match
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
for (const field of varyFields) {
|
|
114
|
+
const cachedValue = cachedRequest.headers.get(field);
|
|
115
|
+
const requestValue = request.headers.get(field);
|
|
116
|
+
if (cachedValue !== requestValue) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the Vary header value for a cached entry's response.
|
|
128
|
+
*/
|
|
129
|
+
private _getVaryHeaderForEntry(cachedRequest: Request): string | null {
|
|
130
|
+
const entry = this._entries.find((e) => e.request === cachedRequest);
|
|
131
|
+
return entry?.response.headers.get('vary') ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the first matching Response for a given request, or undefined.
|
|
136
|
+
*
|
|
137
|
+
* @param request - The request to match against cached entries.
|
|
138
|
+
* @param options - Options to control matching behavior.
|
|
139
|
+
* @returns A Promise resolving to the matching Response, or undefined.
|
|
140
|
+
*/
|
|
141
|
+
async match(
|
|
142
|
+
request: RequestInput,
|
|
143
|
+
options?: CacheQueryOptions
|
|
144
|
+
): Promise<Response | undefined> {
|
|
145
|
+
const req = toRequest(request);
|
|
146
|
+
|
|
147
|
+
// Per spec, only GET requests are matched unless ignoreMethod is set
|
|
148
|
+
if (!options?.ignoreMethod && req.method !== 'GET') {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const entry of this._entries) {
|
|
153
|
+
if (this._requestMatches(entry.request, req, options)) {
|
|
154
|
+
return entry.response.clone();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns all matching Responses for a given request.
|
|
163
|
+
* If no request is provided, returns all cached responses.
|
|
164
|
+
*
|
|
165
|
+
* @param request - The request to match, or undefined to return all.
|
|
166
|
+
* @param options - Options to control matching behavior.
|
|
167
|
+
* @returns A Promise resolving to an array of matching Responses.
|
|
168
|
+
*/
|
|
169
|
+
async matchAll(
|
|
170
|
+
request?: RequestInput,
|
|
171
|
+
options?: CacheQueryOptions
|
|
172
|
+
): Promise<Response[]> {
|
|
173
|
+
if (request === undefined) {
|
|
174
|
+
return this._entries.map((entry) => entry.response.clone());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const req = toRequest(request);
|
|
178
|
+
|
|
179
|
+
// Per spec, only GET requests are matched unless ignoreMethod is set
|
|
180
|
+
if (!options?.ignoreMethod && req.method !== 'GET') {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const results: Response[] = [];
|
|
185
|
+
for (const entry of this._entries) {
|
|
186
|
+
if (this._requestMatches(entry.request, req, options)) {
|
|
187
|
+
results.push(entry.response.clone());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fetches a URL and adds the response to the cache.
|
|
196
|
+
* Throws if the response is not ok (status 200-299).
|
|
197
|
+
*
|
|
198
|
+
* @param request - The request or URL to fetch and cache.
|
|
199
|
+
*/
|
|
200
|
+
async add(request: RequestInput): Promise<void> {
|
|
201
|
+
await this.addAll([request]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fetches multiple URLs and adds all responses to the cache.
|
|
206
|
+
* Throws if any response is not ok (status 200-299).
|
|
207
|
+
*
|
|
208
|
+
* @param requests - The requests or URLs to fetch and cache.
|
|
209
|
+
*/
|
|
210
|
+
async addAll(requests: RequestInput[]): Promise<void> {
|
|
211
|
+
const reqs = requests.map((r) => toRequest(r));
|
|
212
|
+
|
|
213
|
+
// Validate: only GET requests
|
|
214
|
+
for (const req of reqs) {
|
|
215
|
+
if (req.method !== 'GET') {
|
|
216
|
+
throw new TypeError('Request method must be GET for Cache.add/addAll');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fetch all requests
|
|
221
|
+
const fetchFn =
|
|
222
|
+
typeof globalThis.fetch === 'function' ? globalThis.fetch : null;
|
|
223
|
+
if (!fetchFn) {
|
|
224
|
+
throw new TypeError(
|
|
225
|
+
'Cache.add/addAll requires a global fetch function'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const responses = await Promise.all(
|
|
230
|
+
reqs.map((req) => fetchFn(req.url, {
|
|
231
|
+
method: req.method,
|
|
232
|
+
headers: toFetchHeaders(req.headers),
|
|
233
|
+
}))
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Validate all responses
|
|
237
|
+
for (let i = 0; i < responses.length; i++) {
|
|
238
|
+
const response = responses[i];
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new TypeError(
|
|
241
|
+
`Request failed with status ${response.status}: ${reqs[i].url}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Store all entries
|
|
247
|
+
for (let i = 0; i < reqs.length; i++) {
|
|
248
|
+
await this.put(reqs[i], responses[i] as unknown as Response);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Stores a request/response pair in the cache.
|
|
254
|
+
* The response is cloned before storing so the original can still be used.
|
|
255
|
+
* Any existing entry with the same URL is replaced.
|
|
256
|
+
*
|
|
257
|
+
* @param request - The request to use as the cache key.
|
|
258
|
+
* @param response - The response to store.
|
|
259
|
+
*/
|
|
260
|
+
async put(request: RequestInput, response: Response): Promise<void> {
|
|
261
|
+
const req = toRequest(request);
|
|
262
|
+
|
|
263
|
+
// Per spec, only GET requests can be stored
|
|
264
|
+
if (req.method !== 'GET') {
|
|
265
|
+
throw new TypeError('Request method must be GET for Cache.put');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Per spec, cannot store responses with status 206 (Partial Content)
|
|
269
|
+
if (response.status === 206) {
|
|
270
|
+
throw new TypeError('Cannot cache partial (206) responses');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Remove any existing entry with the same URL
|
|
274
|
+
this._entries = this._entries.filter(
|
|
275
|
+
(entry) => entry.request.url !== req.url
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Clone request and response so the caller retains ownership of the originals.
|
|
279
|
+
// Per Fetch/Cache semantics, stored requests should not retain lifecycle signals.
|
|
280
|
+
this._entries.push({
|
|
281
|
+
request: new Request(req.url, {
|
|
282
|
+
method: req.method,
|
|
283
|
+
headers: toFetchHeaders(req.headers),
|
|
284
|
+
mode: req.mode,
|
|
285
|
+
credentials: req.credentials,
|
|
286
|
+
cache: req.cache,
|
|
287
|
+
redirect: req.redirect,
|
|
288
|
+
referrer: req.referrer,
|
|
289
|
+
referrerPolicy: req.referrerPolicy,
|
|
290
|
+
integrity: req.integrity,
|
|
291
|
+
signal: null,
|
|
292
|
+
}),
|
|
293
|
+
response: response.clone(),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Deletes all cached entries matching the given request.
|
|
299
|
+
*
|
|
300
|
+
* @param request - The request to match for deletion.
|
|
301
|
+
* @param options - Options to control matching behavior.
|
|
302
|
+
* @returns A Promise resolving to true if any entry was deleted, false otherwise.
|
|
303
|
+
*/
|
|
304
|
+
async delete(
|
|
305
|
+
request: RequestInput,
|
|
306
|
+
options?: CacheQueryOptions
|
|
307
|
+
): Promise<boolean> {
|
|
308
|
+
const req = toRequest(request);
|
|
309
|
+
const initialLength = this._entries.length;
|
|
310
|
+
|
|
311
|
+
this._entries = this._entries.filter(
|
|
312
|
+
(entry) => !this._requestMatches(entry.request, req, options)
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return this._entries.length < initialLength;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns all cached request keys.
|
|
320
|
+
* If a request is provided, only matching keys are returned.
|
|
321
|
+
*
|
|
322
|
+
* @param request - Optional request to filter keys.
|
|
323
|
+
* @param options - Options to control matching behavior.
|
|
324
|
+
* @returns A Promise resolving to an array of cached Request objects.
|
|
325
|
+
*/
|
|
326
|
+
async keys(
|
|
327
|
+
request?: RequestInput,
|
|
328
|
+
options?: CacheQueryOptions
|
|
329
|
+
): Promise<Request[]> {
|
|
330
|
+
if (request === undefined) {
|
|
331
|
+
return this._entries.map((entry) => entry.request.clone());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const req = toRequest(request);
|
|
335
|
+
const results: Request[] = [];
|
|
336
|
+
|
|
337
|
+
for (const entry of this._entries) {
|
|
338
|
+
if (this._requestMatches(entry.request, req, options)) {
|
|
339
|
+
results.push(entry.request.clone());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
get [Symbol.toStringTag](): string {
|
|
347
|
+
return 'Cache';
|
|
348
|
+
}
|
|
349
|
+
}
|