almostnode 0.2.7 → 0.2.8
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/README.md +1 -1
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
- package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -1
- package/dist/frameworks/next-config-parser.d.ts +16 -0
- package/dist/frameworks/next-config-parser.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +6 -6
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/next-html-generator.d.ts +35 -0
- package/dist/frameworks/next-html-generator.d.ts.map +1 -0
- package/dist/frameworks/next-shims.d.ts +79 -0
- package/dist/frameworks/next-shims.d.ts.map +1 -0
- package/dist/index.cjs +2895 -2454
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3208 -2782
- package/dist/index.mjs.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +2 -0
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/shims/crypto.d.ts +2 -0
- package/dist/shims/crypto.d.ts.map +1 -1
- package/dist/shims/esbuild.d.ts.map +1 -1
- package/dist/shims/fs.d.ts.map +1 -1
- package/dist/shims/http.d.ts +29 -0
- package/dist/shims/http.d.ts.map +1 -1
- package/dist/shims/path.d.ts.map +1 -1
- package/dist/shims/stream.d.ts.map +1 -1
- package/dist/shims/vfs-adapter.d.ts.map +1 -1
- package/dist/shims/ws.d.ts +2 -0
- package/dist/shims/ws.d.ts.map +1 -1
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/convex-app-demo-entry.ts +229 -35
- package/src/frameworks/code-transforms.ts +5 -1
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +76 -1675
- package/src/frameworks/next-html-generator.ts +597 -0
- package/src/frameworks/next-shims.ts +1050 -0
- package/src/frameworks/tailwind-config-loader.ts +1 -1
- package/src/index.ts +2 -0
- package/src/runtime.ts +94 -15
- package/src/server-bridge.ts +61 -28
- package/src/shims/crypto.ts +13 -0
- package/src/shims/esbuild.ts +4 -1
- package/src/shims/fs.ts +9 -11
- package/src/shims/http.ts +309 -3
- package/src/shims/path.ts +6 -13
- package/src/shims/stream.ts +12 -26
- package/src/shims/vfs-adapter.ts +5 -2
- package/src/shims/ws.ts +92 -2
- package/src/utils/binary-encoding.ts +43 -0
- package/src/virtual-fs.ts +7 -15
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
package/README.md
CHANGED
package/dist/__sw__.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service Worker for Mini WebContainers
|
|
3
3
|
* Intercepts fetch requests and routes them to virtual servers
|
|
4
|
-
* Version:
|
|
4
|
+
* Version: 15 - cleanup: extract helpers, gate debug logs, remove test endpoints
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const DEBUG = false;
|
|
8
|
+
|
|
7
9
|
// Communication port with main thread
|
|
8
10
|
let mainPort = null;
|
|
9
11
|
|
|
@@ -14,30 +16,45 @@ let requestId = 0;
|
|
|
14
16
|
// Registered virtual server ports
|
|
15
17
|
const registeredPorts = new Set();
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Decode base64 string to Uint8Array
|
|
21
|
+
*/
|
|
22
|
+
function base64ToBytes(base64) {
|
|
23
|
+
const binary = atob(base64);
|
|
24
|
+
const bytes = new Uint8Array(binary.length);
|
|
25
|
+
for (let i = 0; i < binary.length; i++) {
|
|
26
|
+
bytes[i] = binary.charCodeAt(i);
|
|
27
|
+
}
|
|
28
|
+
return bytes;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Handle messages from main thread
|
|
19
33
|
*/
|
|
20
34
|
self.addEventListener('message', (event) => {
|
|
21
35
|
const { type, data } = event.data;
|
|
22
36
|
|
|
23
|
-
console.log('[SW] Received message:', type, 'hasPort in event.ports:', event.ports?.length > 0);
|
|
37
|
+
DEBUG && console.log('[SW] Received message:', type, 'hasPort in event.ports:', event.ports?.length > 0);
|
|
24
38
|
|
|
25
39
|
// When a MessagePort is transferred, it's in event.ports[0], not event.data.port
|
|
26
40
|
if (type === 'init' && event.ports && event.ports[0]) {
|
|
27
41
|
// Initialize communication channel
|
|
28
42
|
mainPort = event.ports[0];
|
|
29
43
|
mainPort.onmessage = handleMainMessage;
|
|
30
|
-
console.log('[SW] Initialized communication channel with transferred port');
|
|
44
|
+
DEBUG && console.log('[SW] Initialized communication channel with transferred port');
|
|
45
|
+
// Re-claim clients so that pages opened after SW activation get controlled.
|
|
46
|
+
// Without this, controllerchange never fires for late-arriving pages.
|
|
47
|
+
self.clients.claim();
|
|
31
48
|
}
|
|
32
49
|
|
|
33
50
|
if (type === 'server-registered' && data) {
|
|
34
51
|
registeredPorts.add(data.port);
|
|
35
|
-
console.log(`[SW] Server registered on port ${data.port}`);
|
|
52
|
+
DEBUG && console.log(`[SW] Server registered on port ${data.port}`);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
if (type === 'server-unregistered' && data) {
|
|
39
56
|
registeredPorts.delete(data.port);
|
|
40
|
-
console.log(`[SW] Server unregistered from port ${data.port}`);
|
|
57
|
+
DEBUG && console.log(`[SW] Server unregistered from port ${data.port}`);
|
|
41
58
|
}
|
|
42
59
|
});
|
|
43
60
|
|
|
@@ -47,20 +64,20 @@ self.addEventListener('message', (event) => {
|
|
|
47
64
|
function handleMainMessage(event) {
|
|
48
65
|
const { type, id, data, error } = event.data;
|
|
49
66
|
|
|
50
|
-
console.log('[SW] Received message from main:', type, 'id:', id);
|
|
67
|
+
DEBUG && console.log('[SW] Received message from main:', type, 'id:', id);
|
|
51
68
|
|
|
52
69
|
if (type === 'response') {
|
|
53
70
|
const pending = pendingRequests.get(id);
|
|
54
|
-
console.log('[SW] Looking for pending request:', id, 'found:', !!pending);
|
|
71
|
+
DEBUG && console.log('[SW] Looking for pending request:', id, 'found:', !!pending);
|
|
55
72
|
|
|
56
73
|
if (pending) {
|
|
57
74
|
pendingRequests.delete(id);
|
|
58
75
|
|
|
59
76
|
if (error) {
|
|
60
|
-
console.log('[SW] Response error:', error);
|
|
77
|
+
DEBUG && console.log('[SW] Response error:', error);
|
|
61
78
|
pending.reject(new Error(error));
|
|
62
79
|
} else {
|
|
63
|
-
console.log('[SW] Response data:', {
|
|
80
|
+
DEBUG && console.log('[SW] Response data:', {
|
|
64
81
|
statusCode: data?.statusCode,
|
|
65
82
|
statusMessage: data?.statusMessage,
|
|
66
83
|
headers: data?.headers,
|
|
@@ -74,50 +91,46 @@ function handleMainMessage(event) {
|
|
|
74
91
|
|
|
75
92
|
// Handle streaming responses
|
|
76
93
|
if (type === 'stream-start') {
|
|
77
|
-
console.log('[SW]
|
|
94
|
+
DEBUG && console.log('[SW] stream-start received, id:', id);
|
|
78
95
|
const pending = pendingRequests.get(id);
|
|
79
96
|
if (pending && pending.streamController) {
|
|
80
97
|
// Store headers/status for the streaming response
|
|
81
98
|
pending.streamData = data;
|
|
82
99
|
pending.resolveHeaders(data);
|
|
83
|
-
console.log('[SW]
|
|
100
|
+
DEBUG && console.log('[SW] headers resolved for stream', id);
|
|
84
101
|
} else {
|
|
85
|
-
console.log('[SW]
|
|
102
|
+
DEBUG && console.log('[SW] No pending request or controller for stream-start', id, !!pending, pending?.streamController);
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
if (type === 'stream-chunk') {
|
|
90
|
-
console.log('[SW]
|
|
107
|
+
DEBUG && console.log('[SW] stream-chunk received, id:', id, 'size:', data?.chunkBase64?.length);
|
|
91
108
|
const pending = pendingRequests.get(id);
|
|
92
109
|
if (pending && pending.streamController) {
|
|
93
110
|
try {
|
|
94
111
|
// Decode base64 chunk and enqueue
|
|
95
112
|
if (data.chunkBase64) {
|
|
96
|
-
const
|
|
97
|
-
const bytes = new Uint8Array(binary.length);
|
|
98
|
-
for (let i = 0; i < binary.length; i++) {
|
|
99
|
-
bytes[i] = binary.charCodeAt(i);
|
|
100
|
-
}
|
|
113
|
+
const bytes = base64ToBytes(data.chunkBase64);
|
|
101
114
|
pending.streamController.enqueue(bytes);
|
|
102
|
-
console.log('[SW]
|
|
115
|
+
DEBUG && console.log('[SW] chunk enqueued, bytes:', bytes.length);
|
|
103
116
|
}
|
|
104
117
|
} catch (e) {
|
|
105
118
|
console.error('[SW] Error enqueueing chunk:', e);
|
|
106
119
|
}
|
|
107
120
|
} else {
|
|
108
|
-
console.log('[SW]
|
|
121
|
+
DEBUG && console.log('[SW] No pending request or controller for stream-chunk', id);
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
if (type === 'stream-end') {
|
|
113
|
-
console.log('[SW]
|
|
126
|
+
DEBUG && console.log('[SW] stream-end received, id:', id);
|
|
114
127
|
const pending = pendingRequests.get(id);
|
|
115
128
|
if (pending && pending.streamController) {
|
|
116
129
|
try {
|
|
117
130
|
pending.streamController.close();
|
|
118
|
-
console.log('[SW]
|
|
131
|
+
DEBUG && console.log('[SW] stream closed');
|
|
119
132
|
} catch (e) {
|
|
120
|
-
console.log('[SW] stream already closed');
|
|
133
|
+
DEBUG && console.log('[SW] stream already closed');
|
|
121
134
|
}
|
|
122
135
|
pendingRequests.delete(id);
|
|
123
136
|
}
|
|
@@ -128,11 +141,23 @@ function handleMainMessage(event) {
|
|
|
128
141
|
* Send request to main thread and wait for response
|
|
129
142
|
*/
|
|
130
143
|
async function sendRequest(port, method, url, headers, body) {
|
|
131
|
-
console.log('[SW] sendRequest called, mainPort:', !!mainPort, 'url:', url);
|
|
144
|
+
DEBUG && console.log('[SW] sendRequest called, mainPort:', !!mainPort, 'url:', url);
|
|
132
145
|
|
|
133
146
|
if (!mainPort) {
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
// Ask all clients to re-send the init message
|
|
148
|
+
const allClients = await self.clients.matchAll({ type: 'window' });
|
|
149
|
+
for (const client of allClients) {
|
|
150
|
+
client.postMessage({ type: 'sw-needs-init' });
|
|
151
|
+
}
|
|
152
|
+
// Wait up to 5s for a client to re-initialize the port
|
|
153
|
+
// (main thread may be busy with heavy operations like CLI execution)
|
|
154
|
+
await new Promise(resolve => {
|
|
155
|
+
const check = setInterval(() => { if (mainPort) { clearInterval(check); resolve(); } }, 50);
|
|
156
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
157
|
+
});
|
|
158
|
+
if (!mainPort) {
|
|
159
|
+
throw new Error('Service Worker not initialized - no connection to main thread');
|
|
160
|
+
}
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
const id = ++requestId;
|
|
@@ -160,11 +185,22 @@ async function sendRequest(port, method, url, headers, body) {
|
|
|
160
185
|
* Send streaming request to main thread
|
|
161
186
|
* Returns a ReadableStream that receives chunks from main thread
|
|
162
187
|
*/
|
|
163
|
-
function sendStreamingRequest(port, method, url, headers, body) {
|
|
164
|
-
console.log('[SW] sendStreamingRequest called, url:', url);
|
|
188
|
+
async function sendStreamingRequest(port, method, url, headers, body) {
|
|
189
|
+
DEBUG && console.log('[SW] sendStreamingRequest called, url:', url);
|
|
165
190
|
|
|
166
191
|
if (!mainPort) {
|
|
167
|
-
|
|
192
|
+
// Ask all clients to re-send the init message
|
|
193
|
+
const allClients = await self.clients.matchAll({ type: 'window' });
|
|
194
|
+
for (const client of allClients) {
|
|
195
|
+
client.postMessage({ type: 'sw-needs-init' });
|
|
196
|
+
}
|
|
197
|
+
await new Promise(resolve => {
|
|
198
|
+
const check = setInterval(() => { if (mainPort) { clearInterval(check); resolve(); } }, 50);
|
|
199
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 5000);
|
|
200
|
+
});
|
|
201
|
+
if (!mainPort) {
|
|
202
|
+
throw new Error('Service Worker not initialized');
|
|
203
|
+
}
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
const id = ++requestId;
|
|
@@ -206,7 +242,7 @@ function sendStreamingRequest(port, method, url, headers, body) {
|
|
|
206
242
|
self.addEventListener('fetch', (event) => {
|
|
207
243
|
const url = new URL(event.request.url);
|
|
208
244
|
|
|
209
|
-
console.log('[SW] Fetch:', url.pathname, 'mainPort:', !!mainPort);
|
|
245
|
+
DEBUG && console.log('[SW] Fetch:', url.pathname, 'mainPort:', !!mainPort);
|
|
210
246
|
|
|
211
247
|
// Check if this is a virtual server request
|
|
212
248
|
const match = url.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
|
|
@@ -229,12 +265,12 @@ self.addEventListener('fetch', (event) => {
|
|
|
229
265
|
if (event.request.mode === 'navigate') {
|
|
230
266
|
// Navigation requests: redirect to include the virtual prefix
|
|
231
267
|
const redirectUrl = url.origin + virtualPrefix + targetPath;
|
|
232
|
-
console.log('[SW] Redirecting navigation from virtual context:', url.pathname, '->', redirectUrl);
|
|
268
|
+
DEBUG && console.log('[SW] Redirecting navigation from virtual context:', url.pathname, '->', redirectUrl);
|
|
233
269
|
event.respondWith(Response.redirect(redirectUrl, 302));
|
|
234
270
|
return;
|
|
235
271
|
} else {
|
|
236
272
|
// Non-navigation requests (images, scripts, etc.): forward to virtual server
|
|
237
|
-
console.log('[SW] Forwarding resource from virtual context:', url.pathname);
|
|
273
|
+
DEBUG && console.log('[SW] Forwarding resource from virtual context:', url.pathname);
|
|
238
274
|
event.respondWith(handleVirtualRequest(event.request, virtualPort, targetPath));
|
|
239
275
|
return;
|
|
240
276
|
}
|
|
@@ -247,45 +283,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
247
283
|
return;
|
|
248
284
|
}
|
|
249
285
|
|
|
250
|
-
console.log('[SW] Virtual request:', url.pathname);
|
|
286
|
+
DEBUG && console.log('[SW] Virtual request:', url.pathname);
|
|
251
287
|
|
|
252
288
|
const port = parseInt(match[1], 10);
|
|
253
289
|
const path = match[2] || '/';
|
|
254
290
|
|
|
255
|
-
// TEST MODE: Return hardcoded response to verify SW is working
|
|
256
|
-
if (url.searchParams.has('__sw_test__')) {
|
|
257
|
-
event.respondWith(new Response(
|
|
258
|
-
'<!DOCTYPE html><html><body><h1>SW Test OK</h1><div id="root">Service Worker is responding correctly!</div></body></html>',
|
|
259
|
-
{
|
|
260
|
-
status: 200,
|
|
261
|
-
headers: { 'Content-Type': 'text/html' },
|
|
262
|
-
}
|
|
263
|
-
));
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// DEBUG MODE: Return info about what SW receives
|
|
268
|
-
if (url.searchParams.has('__sw_debug__')) {
|
|
269
|
-
event.respondWith((async () => {
|
|
270
|
-
try {
|
|
271
|
-
const response = await sendRequest(port, 'GET', path, {}, null);
|
|
272
|
-
return new Response(
|
|
273
|
-
`<!DOCTYPE html><html><body><h1>SW Debug</h1><pre>${JSON.stringify({
|
|
274
|
-
statusCode: response.statusCode,
|
|
275
|
-
statusMessage: response.statusMessage,
|
|
276
|
-
headers: response.headers,
|
|
277
|
-
bodyBase64Length: response.bodyBase64?.length,
|
|
278
|
-
bodyBase64Start: response.bodyBase64?.substring(0, 100),
|
|
279
|
-
}, null, 2)}</pre></body></html>`,
|
|
280
|
-
{ status: 200, headers: { 'Content-Type': 'text/html' } }
|
|
281
|
-
);
|
|
282
|
-
} catch (error) {
|
|
283
|
-
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
284
|
-
}
|
|
285
|
-
})());
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
291
|
event.respondWith(handleVirtualRequest(event.request, port, path + url.search));
|
|
290
292
|
});
|
|
291
293
|
|
|
@@ -310,17 +312,15 @@ async function handleVirtualRequest(request, port, path) {
|
|
|
310
312
|
const isStreamingCandidate = request.method === 'POST' && path.startsWith('/api/');
|
|
311
313
|
|
|
312
314
|
if (isStreamingCandidate) {
|
|
313
|
-
console.log('[SW]
|
|
315
|
+
DEBUG && console.log('[SW] Using streaming mode for:', path);
|
|
314
316
|
return handleStreamingRequest(port, request.method, path, headers, body);
|
|
315
317
|
}
|
|
316
|
-
console.log('[SW] Using non-streaming mode for:', request.method, path);
|
|
317
|
-
|
|
318
|
-
console.log('[SW] Sending request to main thread:', port, request.method, path);
|
|
318
|
+
DEBUG && console.log('[SW] Using non-streaming mode for:', request.method, path);
|
|
319
319
|
|
|
320
320
|
// Send to main thread
|
|
321
321
|
const response = await sendRequest(port, request.method, path, headers, body);
|
|
322
322
|
|
|
323
|
-
console.log('[SW] Got response from main thread:', {
|
|
323
|
+
DEBUG && console.log('[SW] Got response from main thread:', {
|
|
324
324
|
statusCode: response.statusCode,
|
|
325
325
|
headersKeys: response.headers ? Object.keys(response.headers) : [],
|
|
326
326
|
bodyBase64Length: response.bodyBase64?.length,
|
|
@@ -330,16 +330,12 @@ async function handleVirtualRequest(request, port, path) {
|
|
|
330
330
|
let finalResponse;
|
|
331
331
|
if (response.bodyBase64 && response.bodyBase64.length > 0) {
|
|
332
332
|
try {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
for (let i = 0; i < binary.length; i++) {
|
|
336
|
-
bytes[i] = binary.charCodeAt(i);
|
|
337
|
-
}
|
|
338
|
-
console.log('[SW] Decoded body length:', bytes.length);
|
|
333
|
+
const bytes = base64ToBytes(response.bodyBase64);
|
|
334
|
+
DEBUG && console.log('[SW] Decoded body length:', bytes.length);
|
|
339
335
|
|
|
340
336
|
// Use Blob to ensure proper body handling
|
|
341
337
|
const blob = new Blob([bytes], { type: response.headers['Content-Type'] || 'application/octet-stream' });
|
|
342
|
-
console.log('[SW] Created blob size:', blob.size);
|
|
338
|
+
DEBUG && console.log('[SW] Created blob size:', blob.size);
|
|
343
339
|
|
|
344
340
|
// Merge response headers with CORP/COEP headers to allow iframe embedding
|
|
345
341
|
// The parent page has COEP: credentialless, so we need matching headers
|
|
@@ -370,7 +366,7 @@ async function handleVirtualRequest(request, port, path) {
|
|
|
370
366
|
});
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
console.log('[SW] Final Response created, status:', finalResponse.status);
|
|
369
|
+
DEBUG && console.log('[SW] Final Response created, status:', finalResponse.status);
|
|
374
370
|
|
|
375
371
|
return finalResponse;
|
|
376
372
|
} catch (error) {
|
|
@@ -387,12 +383,12 @@ async function handleVirtualRequest(request, port, path) {
|
|
|
387
383
|
* Handle a streaming request
|
|
388
384
|
*/
|
|
389
385
|
async function handleStreamingRequest(port, method, path, headers, body) {
|
|
390
|
-
const { stream, headersPromise, id } = sendStreamingRequest(port, method, path, headers, body);
|
|
386
|
+
const { stream, headersPromise, id } = await sendStreamingRequest(port, method, path, headers, body);
|
|
391
387
|
|
|
392
388
|
// Wait for headers to arrive
|
|
393
389
|
const responseData = await headersPromise;
|
|
394
390
|
|
|
395
|
-
console.log('[SW] Streaming response started:', responseData?.statusCode);
|
|
391
|
+
DEBUG && console.log('[SW] Streaming response started:', responseData?.statusCode);
|
|
396
392
|
|
|
397
393
|
// Build response headers
|
|
398
394
|
const respHeaders = new Headers(responseData?.headers || {});
|
|
@@ -412,7 +408,7 @@ async function handleStreamingRequest(port, method, path, headers, body) {
|
|
|
412
408
|
* Activate immediately
|
|
413
409
|
*/
|
|
414
410
|
self.addEventListener('install', (event) => {
|
|
415
|
-
console.log('[SW] Installing...');
|
|
411
|
+
DEBUG && console.log('[SW] Installing...');
|
|
416
412
|
event.waitUntil(self.skipWaiting());
|
|
417
413
|
});
|
|
418
414
|
|
|
@@ -420,6 +416,6 @@ self.addEventListener('install', (event) => {
|
|
|
420
416
|
* Claim all clients immediately
|
|
421
417
|
*/
|
|
422
418
|
self.addEventListener('activate', (event) => {
|
|
423
|
-
console.log('[SW] Activated');
|
|
419
|
+
DEBUG && console.log('[SW] Activated');
|
|
424
420
|
event.waitUntil(self.clients.claim());
|
|
425
421
|
});
|