@inkandswitch/patchwork-bootloader 0.2.6 → 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/CHANGELOG.md +18 -0
- package/dist/automerge-worker.d.ts +1 -0
- package/dist/automerge-worker.js +505 -0
- package/dist/externals.js +0 -1
- package/dist/service-worker.js +99 -273
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +89 -106
- package/dist/site.d.ts +3 -7
- package/dist/site.js +26 -55
- package/dist/sync-config.d.ts +8 -0
- package/dist/sync-config.js +13 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +7 -1
- package/dist/vite/service-worker-plugin.js +25 -9
- package/package.json +19 -19
- package/src/automerge-worker.ts +647 -0
- package/src/externals.ts +0 -1
- package/src/service-worker.ts +124 -349
- package/src/setup.ts +105 -118
- package/src/site.ts +31 -66
- package/src/sync-config.ts +23 -0
- package/src/types.ts +98 -0
- package/src/vite/service-worker-plugin.ts +26 -11
- package/tsconfig.json +1 -1
- package/dist/sw-logger.d.ts +0 -105
- package/dist/sw-logger.js +0 -366
- package/src/sw-logger.ts +0 -463
package/src/sw-logger.ts
DELETED
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistent ring-buffer logger for service workers.
|
|
3
|
-
*
|
|
4
|
-
* Stores log entries in a dedicated IndexedDB database (`sw-logs`) that is
|
|
5
|
-
* completely separate from the `automerge` database used by the Repo, so
|
|
6
|
-
* writes here never contend with storage or hydration transactions.
|
|
7
|
-
*
|
|
8
|
-
* Entries are accumulated in memory and batch-flushed to IDB periodically
|
|
9
|
-
* (every {@link FLUSH_INTERVAL_MS}) or when the buffer reaches
|
|
10
|
-
* {@link FLUSH_THRESHOLD} entries — whichever comes first.
|
|
11
|
-
*
|
|
12
|
-
* The on-disk store is a ring buffer capped at {@link MAX_ENTRIES}. Oldest
|
|
13
|
-
* entries are pruned on each flush when the cap is exceeded.
|
|
14
|
-
*
|
|
15
|
-
* ## Usage
|
|
16
|
-
*
|
|
17
|
-
* ```ts
|
|
18
|
-
* import { SwLogger } from "./sw-logger.js"
|
|
19
|
-
*
|
|
20
|
-
* const log = await SwLogger.open()
|
|
21
|
-
* log.info("repo initialized")
|
|
22
|
-
* log.warn("connection dropped", { url })
|
|
23
|
-
* log.error("sync threw", error)
|
|
24
|
-
*
|
|
25
|
-
* // From the SW inspector console:
|
|
26
|
-
* self.printLogs() // prints last 200 entries
|
|
27
|
-
* self.printLogs(5000) // prints last 5 000 entries
|
|
28
|
-
* self.tailLogs(100) // returns last 100 entries as an array
|
|
29
|
-
* self.exportLogs() // returns all entries as JSON string
|
|
30
|
-
* self.clearLogs() // wipes the log database
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
// ── Configuration ───────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const DB_NAME = "sw-logs";
|
|
37
|
-
const DB_VERSION = 1;
|
|
38
|
-
const STORE_NAME = "entries";
|
|
39
|
-
const MAX_ENTRIES = 50_000;
|
|
40
|
-
const FLUSH_INTERVAL_MS = 1_000;
|
|
41
|
-
const FLUSH_THRESHOLD = 128;
|
|
42
|
-
|
|
43
|
-
// ── Types ───────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
export interface LogEntry {
|
|
46
|
-
/** Auto-incremented IDB key (doubles as ordering index). */
|
|
47
|
-
id?: number;
|
|
48
|
-
/** ISO-8601 timestamp */
|
|
49
|
-
ts: string;
|
|
50
|
-
/** Monotonic high-res timestamp (ms since SW start) */
|
|
51
|
-
hrt: number;
|
|
52
|
-
/** Log level */
|
|
53
|
-
level: "debug" | "info" | "warn" | "error";
|
|
54
|
-
/** Log message */
|
|
55
|
-
msg: string;
|
|
56
|
-
/** Optional structured data (must be cloneable) */
|
|
57
|
-
data?: unknown;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ── Console method lookup ───────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
const consoleMethods: Record<LogEntry["level"], (...args: unknown[]) => void> =
|
|
63
|
-
{
|
|
64
|
-
debug: console.debug.bind(console),
|
|
65
|
-
info: console.info.bind(console),
|
|
66
|
-
warn: console.warn.bind(console),
|
|
67
|
-
error: console.error.bind(console),
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// ── Logger interface (shared by real and noop implementations) ──────────
|
|
71
|
-
|
|
72
|
-
export interface SwLoggerInterface {
|
|
73
|
-
debug(msg: string, data?: unknown): void;
|
|
74
|
-
info(msg: string, data?: unknown): void;
|
|
75
|
-
warn(msg: string, data?: unknown): void;
|
|
76
|
-
error(msg: string, data?: unknown): void;
|
|
77
|
-
flush(): Promise<void>;
|
|
78
|
-
tail(n?: number): Promise<LogEntry[]>;
|
|
79
|
-
exportAll(): Promise<string>;
|
|
80
|
-
clear(): Promise<void>;
|
|
81
|
-
dispose(): void;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── No-op fallback (used when IDB is unavailable) ───────────────────────
|
|
85
|
-
|
|
86
|
-
class NoopLogger implements SwLoggerInterface {
|
|
87
|
-
debug(msg: string, data?: unknown) {
|
|
88
|
-
consoleMethods.debug(
|
|
89
|
-
`[sw:debug]`,
|
|
90
|
-
msg,
|
|
91
|
-
...(data !== undefined ? [data] : [])
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
info(msg: string, data?: unknown) {
|
|
95
|
-
consoleMethods.info(
|
|
96
|
-
`[sw:info]`,
|
|
97
|
-
msg,
|
|
98
|
-
...(data !== undefined ? [data] : [])
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
warn(msg: string, data?: unknown) {
|
|
102
|
-
consoleMethods.warn(
|
|
103
|
-
`[sw:warn]`,
|
|
104
|
-
msg,
|
|
105
|
-
...(data !== undefined ? [data] : [])
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
error(msg: string, data?: unknown) {
|
|
109
|
-
consoleMethods.error(
|
|
110
|
-
`[sw:error]`,
|
|
111
|
-
msg,
|
|
112
|
-
...(data !== undefined ? [data] : [])
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
async flush() {}
|
|
116
|
-
async tail() {
|
|
117
|
-
return [];
|
|
118
|
-
}
|
|
119
|
-
async exportAll() {
|
|
120
|
-
return "[]";
|
|
121
|
-
}
|
|
122
|
-
async clear() {}
|
|
123
|
-
dispose() {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Implementation ──────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
export class SwLogger implements SwLoggerInterface {
|
|
129
|
-
#db: IDBDatabase;
|
|
130
|
-
#buffer: LogEntry[] = [];
|
|
131
|
-
#flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
132
|
-
|
|
133
|
-
private constructor(db: IDBDatabase) {
|
|
134
|
-
this.#db = db;
|
|
135
|
-
this.#flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Open (or create) the log database and return a ready logger.
|
|
140
|
-
* If the database cannot be opened (quota, permissions, etc.),
|
|
141
|
-
* returns a {@link NoopLogger} that writes to the console only.
|
|
142
|
-
*/
|
|
143
|
-
static async open(): Promise<SwLoggerInterface> {
|
|
144
|
-
try {
|
|
145
|
-
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
146
|
-
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
147
|
-
|
|
148
|
-
req.onupgradeneeded = () => {
|
|
149
|
-
const db = req.result;
|
|
150
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
151
|
-
db.createObjectStore(STORE_NAME, {
|
|
152
|
-
keyPath: "id",
|
|
153
|
-
autoIncrement: true,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
req.onsuccess = () => resolve(req.result);
|
|
159
|
-
req.onerror = () => reject(req.error);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
return new SwLogger(db);
|
|
163
|
-
} catch (e) {
|
|
164
|
-
console.warn(
|
|
165
|
-
"[sw-logger] failed to open IDB, falling back to console-only:",
|
|
166
|
-
e
|
|
167
|
-
);
|
|
168
|
-
return new NoopLogger();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── Public API ──────────────────────────────────────────────────────
|
|
173
|
-
|
|
174
|
-
debug(msg: string, data?: unknown) {
|
|
175
|
-
this.#append("debug", msg, data);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
info(msg: string, data?: unknown) {
|
|
179
|
-
this.#append("info", msg, data);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
warn(msg: string, data?: unknown) {
|
|
183
|
-
this.#append("warn", msg, data);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
error(msg: string, data?: unknown) {
|
|
187
|
-
this.#append("error", msg, data);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Force an immediate flush of the in-memory buffer to IDB. */
|
|
191
|
-
async flush(): Promise<void> {
|
|
192
|
-
if (this.#buffer.length === 0) return;
|
|
193
|
-
|
|
194
|
-
const batch = this.#buffer.splice(0);
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const tx = this.#db.transaction(STORE_NAME, "readwrite");
|
|
198
|
-
const store = tx.objectStore(STORE_NAME);
|
|
199
|
-
|
|
200
|
-
for (const entry of batch) {
|
|
201
|
-
store.add(entry);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await txComplete(tx);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
// If the write fails, put the entries back so the next flush retries.
|
|
207
|
-
this.#buffer.unshift(...batch);
|
|
208
|
-
console.warn("[sw-logger] flush failed:", e);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
await this.#prune();
|
|
214
|
-
} catch (e) {
|
|
215
|
-
// Prune failures are non-fatal — entries are already committed.
|
|
216
|
-
console.warn("[sw-logger] prune failed:", e);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Read the last `n` entries (default 200). */
|
|
221
|
-
async tail(n = 200): Promise<LogEntry[]> {
|
|
222
|
-
// Flush pending entries first so the tail is up to date.
|
|
223
|
-
await this.flush();
|
|
224
|
-
|
|
225
|
-
const tx = this.#db.transaction(STORE_NAME, "readonly");
|
|
226
|
-
const store = tx.objectStore(STORE_NAME);
|
|
227
|
-
|
|
228
|
-
return new Promise<LogEntry[]>((resolve, reject) => {
|
|
229
|
-
const entries: LogEntry[] = [];
|
|
230
|
-
const req = store.openCursor(null, "prev");
|
|
231
|
-
|
|
232
|
-
req.onsuccess = () => {
|
|
233
|
-
const cursor = req.result;
|
|
234
|
-
if (cursor && entries.length < n) {
|
|
235
|
-
entries.push(cursor.value as LogEntry);
|
|
236
|
-
cursor.continue();
|
|
237
|
-
} else {
|
|
238
|
-
resolve(entries.reverse());
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
req.onerror = () => reject(req.error);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Return all entries as a JSON string (for copy-paste from console). */
|
|
247
|
-
async exportAll(): Promise<string> {
|
|
248
|
-
await this.flush();
|
|
249
|
-
|
|
250
|
-
const tx = this.#db.transaction(STORE_NAME, "readonly");
|
|
251
|
-
const store = tx.objectStore(STORE_NAME);
|
|
252
|
-
|
|
253
|
-
return new Promise<string>((resolve, reject) => {
|
|
254
|
-
const req = store.getAll();
|
|
255
|
-
req.onsuccess = () => resolve(JSON.stringify(req.result, null, 2));
|
|
256
|
-
req.onerror = () => reject(req.error);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Delete all log entries. */
|
|
261
|
-
async clear(): Promise<void> {
|
|
262
|
-
const tx = this.#db.transaction(STORE_NAME, "readwrite");
|
|
263
|
-
tx.objectStore(STORE_NAME).clear();
|
|
264
|
-
await txComplete(tx);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Stop the periodic flush timer. */
|
|
268
|
-
dispose() {
|
|
269
|
-
if (this.#flushTimer) {
|
|
270
|
-
clearInterval(this.#flushTimer);
|
|
271
|
-
this.#flushTimer = null;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Internals ─────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
#append(level: LogEntry["level"], msg: string, data?: unknown) {
|
|
278
|
-
this.#buffer.push({
|
|
279
|
-
ts: new Date().toISOString(),
|
|
280
|
-
hrt: performance.now(),
|
|
281
|
-
level,
|
|
282
|
-
msg,
|
|
283
|
-
data: data !== undefined ? safeClone(data) : undefined,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Mirror to console using the appropriate severity method.
|
|
287
|
-
const log = consoleMethods[level];
|
|
288
|
-
const tag = `[sw:${level}]`;
|
|
289
|
-
if (data !== undefined) {
|
|
290
|
-
log(tag, msg, data);
|
|
291
|
-
} else {
|
|
292
|
-
log(tag, msg);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (this.#buffer.length >= FLUSH_THRESHOLD) {
|
|
296
|
-
this.flush();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async #prune() {
|
|
301
|
-
// Count in a separate readonly transaction to avoid
|
|
302
|
-
// TransactionInactiveError from awaiting within a single transaction.
|
|
303
|
-
const count = await new Promise<number>((resolve, reject) => {
|
|
304
|
-
const tx = this.#db.transaction(STORE_NAME, "readonly");
|
|
305
|
-
const store = tx.objectStore(STORE_NAME);
|
|
306
|
-
const req = store.count();
|
|
307
|
-
req.onsuccess = () => resolve(req.result);
|
|
308
|
-
req.onerror = () => reject(req.error);
|
|
309
|
-
tx.onabort = () =>
|
|
310
|
-
reject(tx.error ?? new Error("sw-logs count transaction aborted"));
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (count <= MAX_ENTRIES) return;
|
|
314
|
-
|
|
315
|
-
// Delete the oldest entries in a separate readwrite transaction.
|
|
316
|
-
const excess = count - MAX_ENTRIES;
|
|
317
|
-
await new Promise<void>((resolve, reject) => {
|
|
318
|
-
const tx = this.#db.transaction(STORE_NAME, "readwrite");
|
|
319
|
-
const store = tx.objectStore(STORE_NAME);
|
|
320
|
-
let deleted = 0;
|
|
321
|
-
const req = store.openCursor();
|
|
322
|
-
|
|
323
|
-
req.onsuccess = () => {
|
|
324
|
-
const cursor = req.result;
|
|
325
|
-
if (cursor && deleted < excess) {
|
|
326
|
-
cursor.delete();
|
|
327
|
-
deleted++;
|
|
328
|
-
cursor.continue();
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
req.onerror = () => reject(req.error);
|
|
333
|
-
tx.oncomplete = () => resolve();
|
|
334
|
-
tx.onabort = () =>
|
|
335
|
-
reject(tx.error ?? new Error("sw-logs prune transaction aborted"));
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
function txComplete(tx: IDBTransaction): Promise<void> {
|
|
343
|
-
return new Promise((resolve, reject) => {
|
|
344
|
-
tx.oncomplete = () => resolve();
|
|
345
|
-
tx.onerror = () => reject(tx.error);
|
|
346
|
-
tx.onabort = () => reject(tx.error ?? new Error("transaction aborted"));
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ── Read-only access (usable from any context, including main thread) ───
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Read-only accessor for the SW log database.
|
|
354
|
-
*
|
|
355
|
-
* Unlike {@link SwLogger}, this class does not hold a persistent IDB
|
|
356
|
-
* connection — each method opens a fresh connection and closes it after
|
|
357
|
-
* use. This avoids interfering with the SW's write transactions.
|
|
358
|
-
*
|
|
359
|
-
* @example
|
|
360
|
-
* ```ts
|
|
361
|
-
* import { SwLogReader } from "@inkandswitch/patchwork-bootloader/sw-logger"
|
|
362
|
-
*
|
|
363
|
-
* const last100 = await SwLogReader.tail(100)
|
|
364
|
-
* const json = await SwLogReader.exportAll()
|
|
365
|
-
* await SwLogReader.clear()
|
|
366
|
-
* ```
|
|
367
|
-
*/
|
|
368
|
-
export class SwLogReader {
|
|
369
|
-
/** Read the last `n` entries (default 200), oldest-first. */
|
|
370
|
-
static async tail(n = 200): Promise<LogEntry[]> {
|
|
371
|
-
const db = await openDb();
|
|
372
|
-
try {
|
|
373
|
-
const tx = db.transaction(STORE_NAME, "readonly");
|
|
374
|
-
const store = tx.objectStore(STORE_NAME);
|
|
375
|
-
|
|
376
|
-
return await new Promise<LogEntry[]>((resolve, reject) => {
|
|
377
|
-
const entries: LogEntry[] = [];
|
|
378
|
-
const req = store.openCursor(null, "prev");
|
|
379
|
-
|
|
380
|
-
req.onsuccess = () => {
|
|
381
|
-
const cursor = req.result;
|
|
382
|
-
if (cursor && entries.length < n) {
|
|
383
|
-
entries.push(cursor.value as LogEntry);
|
|
384
|
-
cursor.continue();
|
|
385
|
-
} else {
|
|
386
|
-
resolve(entries.reverse());
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
req.onerror = () => reject(req.error);
|
|
391
|
-
});
|
|
392
|
-
} finally {
|
|
393
|
-
db.close();
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/** Return all entries as a JSON string. */
|
|
398
|
-
static async exportAll(): Promise<string> {
|
|
399
|
-
const db = await openDb();
|
|
400
|
-
try {
|
|
401
|
-
const tx = db.transaction(STORE_NAME, "readonly");
|
|
402
|
-
const store = tx.objectStore(STORE_NAME);
|
|
403
|
-
|
|
404
|
-
return await new Promise<string>((resolve, reject) => {
|
|
405
|
-
const req = store.getAll();
|
|
406
|
-
req.onsuccess = () => resolve(JSON.stringify(req.result, null, 2));
|
|
407
|
-
req.onerror = () => reject(req.error);
|
|
408
|
-
});
|
|
409
|
-
} finally {
|
|
410
|
-
db.close();
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** Delete all log entries. */
|
|
415
|
-
static async clear(): Promise<void> {
|
|
416
|
-
const db = await openDb();
|
|
417
|
-
try {
|
|
418
|
-
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
419
|
-
tx.objectStore(STORE_NAME).clear();
|
|
420
|
-
await txComplete(tx);
|
|
421
|
-
} finally {
|
|
422
|
-
db.close();
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/** Open a short-lived connection to the log database. */
|
|
428
|
-
function openDb(): Promise<IDBDatabase> {
|
|
429
|
-
return new Promise<IDBDatabase>((resolve, reject) => {
|
|
430
|
-
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
431
|
-
req.onupgradeneeded = () => {
|
|
432
|
-
const db = req.result;
|
|
433
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
434
|
-
db.createObjectStore(STORE_NAME, {
|
|
435
|
-
keyPath: "id",
|
|
436
|
-
autoIncrement: true,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
req.onsuccess = () => resolve(req.result);
|
|
441
|
-
req.onerror = () => reject(req.error);
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Best-effort structured clone for the `data` field. Falls back to string. */
|
|
446
|
-
function safeClone(value: unknown): unknown {
|
|
447
|
-
if (value === null || value === undefined) return value;
|
|
448
|
-
if (
|
|
449
|
-
typeof value === "string" ||
|
|
450
|
-
typeof value === "number" ||
|
|
451
|
-
typeof value === "boolean"
|
|
452
|
-
) {
|
|
453
|
-
return value;
|
|
454
|
-
}
|
|
455
|
-
if (value instanceof Error) {
|
|
456
|
-
return { name: value.name, message: value.message, stack: value.stack };
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
return structuredClone(value);
|
|
460
|
-
} catch {
|
|
461
|
-
return String(value);
|
|
462
|
-
}
|
|
463
|
-
}
|