@deeprespond/im-tauri 0.0.0-stage → 1.0.0-alpha

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,1161 @@
1
+ import { DRError } from '@deeprespond/im-web';
2
+ import { WorkerTimers, extractKey, isValidKey } from '@deeprespond/im-web/platform';
3
+ import { isTauri, Channel, invoke } from '@tauri-apps/api/core';
4
+
5
+ // src/adapter/files.ts
6
+ var CHUNK = 4 * 1024 * 1024;
7
+ var taskSeq = 0;
8
+ function newTask(prefix) {
9
+ taskSeq += 1;
10
+ return `${prefix}-${Date.now().toString(36)}-${taskSeq}-${Math.random().toString(36).slice(2, 8)}`;
11
+ }
12
+ function fileUrl(path, os) {
13
+ const encoded = encodeURIComponent(path);
14
+ return os === "windows" ? `http://drim-file.localhost/${encoded}` : `drim-file://localhost/${encoded}`;
15
+ }
16
+ function osFromUserAgent(ua) {
17
+ if (/Windows/.test(ua)) return "windows";
18
+ if (/Mac OS X|Macintosh/.test(ua)) return "macos";
19
+ return "linux";
20
+ }
21
+ function pluginFiles(bridge) {
22
+ const os = () => bridge.info?.os ?? osFromUserAgent(typeof navigator !== "undefined" ? navigator.userAgent : "");
23
+ const writeBlob = async (blob, opts = {}) => {
24
+ let handle = "";
25
+ let result = null;
26
+ const name = opts.name ?? blob.name ?? "";
27
+ for (let offset = 0; offset < blob.size || result === null; offset += CHUNK) {
28
+ const end = Math.min(blob.size, offset + CHUNK);
29
+ const bytes = new Uint8Array(await blob.slice(offset, end).arrayBuffer());
30
+ const final = end >= blob.size;
31
+ result = await bridge.callRaw("files_write", bytes, {
32
+ "x-drim-handle": handle,
33
+ "x-drim-name": encodeURIComponent(name),
34
+ "x-drim-final": final ? "1" : "0"
35
+ });
36
+ handle = result.handle;
37
+ if (final) break;
38
+ }
39
+ const out = { path: result.path, temp: true, size: result.size };
40
+ if (name) out.name = name;
41
+ if (blob.type) out.type = blob.type;
42
+ return out;
43
+ };
44
+ return {
45
+ stat: (path) => bridge.call("files_stat", { path }),
46
+ read: async (path, offset, length) => new Uint8Array(await bridge.call("files_read", { path, offset, length })),
47
+ writeBlob,
48
+ writeBlobTemp: (blob, name) => writeBlob(blob, { name }),
49
+ persist: (file, opts) => bridge.call("files_persist", { path: file.path, temp: file.temp === true, app_key: opts.appKey, username: opts.username, copy_max_bytes: opts.copyMaxBytes }),
50
+ release: (path, owned) => bridge.call("files_release", { path, owned }),
51
+ uploadPart: async (req) => {
52
+ const task = newTask("up");
53
+ const off = bridge.on("upload_progress", (event) => {
54
+ if (event.task === task && typeof event.loaded === "number") req.onProgress?.(event.loaded);
55
+ });
56
+ const onAbort = () => void bridge.call("files_cancel", { task }).catch(() => {
57
+ });
58
+ req.signal?.addEventListener("abort", onAbort, { once: true });
59
+ try {
60
+ const r = await bridge.call("files_upload_part", {
61
+ task,
62
+ path: req.path,
63
+ offset: req.offset,
64
+ length: req.length,
65
+ url: req.url,
66
+ method: req.method,
67
+ headers: req.headers,
68
+ expect: req.expect ?? null,
69
+ patches: req.patches ?? null
70
+ });
71
+ return r.status;
72
+ } finally {
73
+ off();
74
+ req.signal?.removeEventListener("abort", onAbort);
75
+ }
76
+ },
77
+ prepareImage: (path, opts) => bridge.call("files_prepare_image", {
78
+ path,
79
+ original: opts.original,
80
+ max_side: opts.maxSide,
81
+ quality: opts.quality,
82
+ max_bytes: opts.maxBytes,
83
+ max_pixels: opts.maxPixels
84
+ }),
85
+ prepareVideo: (path) => bridge.call("files_prepare_video", { path }),
86
+ url: (path) => fileUrl(path, os())
87
+ };
88
+ }
89
+ async function blobsToPaths(value, write) {
90
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
91
+ const written = await write(value, value.name);
92
+ const out = { path: written.path, temp: true };
93
+ if (written.name) out.name = written.name;
94
+ if (value.type) out.type = value.type;
95
+ return out;
96
+ }
97
+ if (Array.isArray(value)) return Promise.all(value.map((v) => blobsToPaths(v, write)));
98
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
99
+ const entries = await Promise.all(Object.entries(value).map(async ([k, v]) => [k, await blobsToPaths(v, write)]));
100
+ return Object.fromEntries(entries);
101
+ }
102
+ return value;
103
+ }
104
+ function pluginChannel(bridge, name, onError) {
105
+ let closed = false;
106
+ let pending = [];
107
+ let flushing = false;
108
+ const joined = bridge.call("bus_join", { name }).catch(onError);
109
+ const flush = async () => {
110
+ if (flushing) return;
111
+ flushing = true;
112
+ try {
113
+ await joined;
114
+ while (pending.length > 0) {
115
+ const msgs = pending;
116
+ pending = [];
117
+ await bridge.call("bus_post", { name, msgs }).catch(onError);
118
+ }
119
+ } finally {
120
+ flushing = false;
121
+ if (pending.length > 0 && !closed) void flush();
122
+ }
123
+ };
124
+ let willSet = false;
125
+ const wrapper = {
126
+ onmessage: null,
127
+ postMessage: (msg) => {
128
+ if (closed) return;
129
+ pending.push(encode(msg));
130
+ const m = msg;
131
+ if (!willSet && m && typeof m === "object" && typeof m.from === "string" && typeof m.v === "number") {
132
+ willSet = true;
133
+ void joined.then(() => bridge.call("bus_will", { name, msg: encode({ v: m.v, from: m.from, type: "bye" }) })).catch(() => {
134
+ });
135
+ }
136
+ void flush();
137
+ },
138
+ close: () => {
139
+ if (closed) return;
140
+ closed = true;
141
+ off();
142
+ void flush().then(() => bridge.call("bus_leave", { name })).catch(() => {
143
+ });
144
+ }
145
+ };
146
+ const off = bridge.on("bus", (event) => {
147
+ if (closed || event.name !== name || typeof event.msg !== "string") return;
148
+ let decoded;
149
+ try {
150
+ decoded = decode(event.msg);
151
+ } catch {
152
+ return;
153
+ }
154
+ wrapper.onmessage?.(decoded);
155
+ });
156
+ return wrapper;
157
+ }
158
+ function encode(msg) {
159
+ return JSON.stringify(msg, function replacer(_key, value) {
160
+ if (value instanceof DRError) return { $drerr: value.toData() };
161
+ if (value instanceof Uint8Array) return { $u8: toBase64(value) };
162
+ if (typeof Blob !== "undefined" && value instanceof Blob) throw Object.assign(new Error("Blob 不能经插件的频道传递"), { name: "DataCloneError" });
163
+ if (typeof value === "function") throw Object.assign(new Error("函数不能经插件的频道传递"), { name: "DataCloneError" });
164
+ if (value instanceof Error) return { $err: { name: value.name, message: value.message } };
165
+ return value;
166
+ });
167
+ }
168
+ function decode(text) {
169
+ return JSON.parse(text, (_key, value) => {
170
+ if (value && typeof value === "object" && !Array.isArray(value)) {
171
+ const v = value;
172
+ if ("$drerr" in v) return DRError.fromData(v.$drerr);
173
+ if ("$u8" in v && typeof v.$u8 === "string") return fromBase64(v.$u8);
174
+ if ("$err" in v) {
175
+ const e = v.$err;
176
+ return Object.assign(new Error(e.message ?? ""), { name: e.name ?? "Error" });
177
+ }
178
+ }
179
+ return value;
180
+ });
181
+ }
182
+ function toBase64(bytes) {
183
+ let s = "";
184
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
185
+ return btoa(s);
186
+ }
187
+ function fromBase64(text) {
188
+ const s = atob(text);
189
+ const out = new Uint8Array(s.length);
190
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
191
+ return out;
192
+ }
193
+ var QUEUE_WAIT_MS = 1e4;
194
+ var TxAbort = class extends Error {
195
+ constructor(reason) {
196
+ super("aborted");
197
+ this.reason = reason;
198
+ }
199
+ reason;
200
+ };
201
+ var TauriDatabase = class _TauriDatabase {
202
+ constructor(bridge, handle, schema, appKey, username, reset) {
203
+ this.bridge = bridge;
204
+ this.handle = handle;
205
+ this.appKey = appKey;
206
+ this.username = username;
207
+ this.storeNames = schema.map((s) => s.name);
208
+ this.#schemas = new Map(schema.map((s) => [s.name, s]));
209
+ if (reset) this.resetReason = reset;
210
+ this.#off = bridge.on("db_deleted", (event) => {
211
+ if (event.app_key !== appKey || String(event.username).toLowerCase() !== username) return;
212
+ for (const listener of [...this.#listeners]) listener(null);
213
+ });
214
+ }
215
+ bridge;
216
+ handle;
217
+ appKey;
218
+ username;
219
+ storeNames;
220
+ resetReason;
221
+ #schemas;
222
+ #writeChain = Promise.resolve();
223
+ #closed = false;
224
+ #listeners = /* @__PURE__ */ new Set();
225
+ #off;
226
+ static async open(bridge, appKey, name, version, upgrade, encrypt) {
227
+ const prefix = `im:${appKey}:`;
228
+ if (!name.startsWith(prefix)) throw new DRError({ code: "invalid_state", message: `数据库名不对:${name}`, details: { reason: "database_name" }, source: "local" });
229
+ const username = name.slice(prefix.length).toLowerCase();
230
+ const schema = upgrade(0, version);
231
+ const r = await bridge.call("db_open", { app_key: appKey, username, version, schema, encrypt });
232
+ return new _TauriDatabase(bridge, r.db, schema, appKey, username, r.reset);
233
+ }
234
+ async transaction(stores, mode, fn, opts) {
235
+ if (this.#closed) throw new DRError({ code: "storage_error", message: "closed", details: { reason: "closed" }, source: "local" });
236
+ for (const store of stores) if (!this.#schemas.has(store)) throw new DRError({ code: "storage_error", message: `没有对象仓库 ${store}`, details: { reason: "NotFoundError" }, source: "local" });
237
+ let release = () => {
238
+ };
239
+ if (mode === "readwrite") {
240
+ const previous = this.#writeChain;
241
+ this.#writeChain = new Promise((resolve) => release = resolve);
242
+ await waitFor(previous);
243
+ } else {
244
+ await waitFor(this.#writeChain);
245
+ }
246
+ try {
247
+ const { tx } = await this.bridge.call("tx_begin", { db: this.handle, stores, mode, durability: opts?.durability ?? "default" });
248
+ const txLike = new PluginTx(this.bridge, tx, this.#schemas, mode);
249
+ let result;
250
+ try {
251
+ result = await fn(txLike);
252
+ } catch (err) {
253
+ await this.bridge.call("tx_abort", { tx }).catch(() => {
254
+ });
255
+ if (err instanceof TxAbort) throw err.reason;
256
+ throw err;
257
+ }
258
+ if (txLike.aborted) {
259
+ await this.bridge.call("tx_abort", { tx }).catch(() => {
260
+ });
261
+ throw txLike.aborted.reason;
262
+ }
263
+ await this.bridge.call("tx_commit", { tx, ops: txLike.takeWrites() });
264
+ return result;
265
+ } finally {
266
+ release();
267
+ }
268
+ }
269
+ close() {
270
+ if (this.#closed) return;
271
+ this.#closed = true;
272
+ this.#off();
273
+ void this.bridge.call("db_close", { db: this.handle }).catch(() => {
274
+ });
275
+ }
276
+ onVersionChange(listener) {
277
+ this.#listeners.add(listener);
278
+ return () => this.#listeners.delete(listener);
279
+ }
280
+ };
281
+ function waitFor(p) {
282
+ return Promise.race([p, new Promise((resolve) => setTimeout(resolve, QUEUE_WAIT_MS))]);
283
+ }
284
+ var PluginTx = class {
285
+ constructor(bridge, tx, schemas, mode) {
286
+ this.bridge = bridge;
287
+ this.tx = tx;
288
+ this.schemas = schemas;
289
+ this.mode = mode;
290
+ }
291
+ bridge;
292
+ tx;
293
+ schemas;
294
+ mode;
295
+ #writes = [];
296
+ aborted = null;
297
+ takeWrites() {
298
+ const out = this.#writes;
299
+ this.#writes = [];
300
+ return out;
301
+ }
302
+ async #flush() {
303
+ if (this.#writes.length === 0) return;
304
+ await this.bridge.call("tx_write", { tx: this.tx, ops: this.takeWrites() });
305
+ }
306
+ async #read(args) {
307
+ this.#check();
308
+ await this.#flush();
309
+ return this.bridge.call("tx_read", { tx: this.tx, ...args });
310
+ }
311
+ #check() {
312
+ if (this.aborted) throw this.aborted;
313
+ }
314
+ #requireWrite(store) {
315
+ this.#check();
316
+ if (this.mode !== "readwrite") throw new DRError({ code: "storage_error", message: "ReadOnlyError", details: { reason: "ReadOnlyError" }, source: "local" });
317
+ const schema = this.schemas.get(store);
318
+ if (!schema) throw new DRError({ code: "storage_error", message: `没有对象仓库 ${store}`, details: { reason: "NotFoundError" }, source: "local" });
319
+ return schema;
320
+ }
321
+ async get(store, key) {
322
+ const value = await this.#read({ op: "get", store, key });
323
+ return value ?? void 0;
324
+ }
325
+ async getAll(store, p = {}) {
326
+ return this.#read({ op: "get_all", store, index: p.index, range: p.range, limit: p.limit });
327
+ }
328
+ async count(store, p = {}) {
329
+ return this.#read({ op: "count", store, index: p.index, range: p.range });
330
+ }
331
+ async *iterate(store, p) {
332
+ let remaining = p.limit;
333
+ let cursor = null;
334
+ for (; ; ) {
335
+ const batch = await this.#read({ op: "iterate", store, index: p.index, range: p.range, direction: p.direction ?? "next", limit: remaining, cursor });
336
+ for (const item of batch.items) yield item;
337
+ if (remaining !== void 0) remaining -= batch.items.length;
338
+ if (!batch.cursor || remaining !== void 0 && remaining <= 0) return;
339
+ cursor = batch.cursor;
340
+ }
341
+ }
342
+ async put(store, value) {
343
+ const schema = this.#requireWrite(store);
344
+ if (extractKey(value, schema.keyPath) === void 0) throw new DRError({ code: "storage_error", message: `${store} 的记录没有合法的主键`, details: { reason: "DataError" }, source: "local" });
345
+ this.#writes.push({ op: "put", store, value });
346
+ }
347
+ async delete(store, key) {
348
+ this.#requireWrite(store);
349
+ if (isValidKey(key)) this.#writes.push({ op: "delete", store, key });
350
+ else this.#writes.push({ op: "delete", store, range: key });
351
+ }
352
+ async clear(store) {
353
+ this.#requireWrite(store);
354
+ this.#writes.push({ op: "clear", store });
355
+ }
356
+ abort(reason) {
357
+ this.aborted = new TxAbort(reason);
358
+ throw this.aborted;
359
+ }
360
+ };
361
+
362
+ // src/adapter/kv.ts
363
+ var DEVICE_ID_KEY = "im:device_id";
364
+ function parse(appKey, key) {
365
+ const prefix = `im:${appKey}:`;
366
+ if (!key.startsWith(prefix)) return null;
367
+ switch (key.slice(prefix.length)) {
368
+ case "session":
369
+ return "session";
370
+ case "pending_logout":
371
+ return "pending_logout";
372
+ case "dbs":
373
+ return "users";
374
+ default:
375
+ return null;
376
+ }
377
+ }
378
+ function hintKey(appKey) {
379
+ return `drim:${appKey}:session_hint`;
380
+ }
381
+ function localStorageSafe() {
382
+ try {
383
+ return globalThis.localStorage ?? null;
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+ var EngineKv = class {
389
+ constructor(bridge, appKey, onError) {
390
+ this.bridge = bridge;
391
+ this.appKey = appKey;
392
+ this.onError = onError;
393
+ }
394
+ bridge;
395
+ appKey;
396
+ onError;
397
+ #cache = /* @__PURE__ */ new Map();
398
+ #queue = Promise.resolve();
399
+ async load() {
400
+ const r = await this.bridge.call("secure_load", { app_key: this.appKey });
401
+ const set = (key, value) => value === null ? this.#cache.delete(key) : this.#cache.set(key, value);
402
+ set(`im:${this.appKey}:session`, r.session);
403
+ set(`im:${this.appKey}:pending_logout`, r.pending_logout);
404
+ set(`im:${this.appKey}:dbs`, r.users);
405
+ this.#cache.set(DEVICE_ID_KEY, r.device_id);
406
+ this.#writeHint(r.session);
407
+ }
408
+ get(key) {
409
+ return this.#cache.get(key) ?? null;
410
+ }
411
+ set(key, value) {
412
+ if (key === DEVICE_ID_KEY) return;
413
+ const target = parse(this.appKey, key);
414
+ let stored = value;
415
+ if (target === "session") stored = this.#withDevice(value);
416
+ this.#cache.set(key, stored);
417
+ if (target) this.#persist(target, stored);
418
+ if (target === "session") this.#writeHint(stored);
419
+ }
420
+ remove(key) {
421
+ if (key === DEVICE_ID_KEY) return;
422
+ const target = parse(this.appKey, key);
423
+ this.#cache.delete(key);
424
+ if (target) this.#persist(target, null);
425
+ if (target === "session") this.#writeHint(null);
426
+ }
427
+ onChange() {
428
+ return () => {
429
+ };
430
+ }
431
+ /** 等全部写入交给插件(退出前)。 */
432
+ flush() {
433
+ return this.#queue;
434
+ }
435
+ /** 会话中记下登录时的设备标识(《1.本地存储》第 2.3 节):重装、复制到另一台电脑后据此丢弃。 */
436
+ #withDevice(value) {
437
+ const deviceId = this.#cache.get(DEVICE_ID_KEY);
438
+ if (!deviceId) return value;
439
+ try {
440
+ const session = JSON.parse(value);
441
+ return JSON.stringify({ ...session, device_id: deviceId });
442
+ } catch {
443
+ return value;
444
+ }
445
+ }
446
+ #persist(key, value) {
447
+ this.#queue = this.#queue.then(() => this.bridge.call("secure_set", { app_key: this.appKey, key, value })).catch((err) => this.onError(err));
448
+ }
449
+ #writeHint(session) {
450
+ const storage = localStorageSafe();
451
+ if (!storage) return;
452
+ try {
453
+ const parsed = session ? JSON.parse(session) : null;
454
+ if (parsed && typeof parsed.username === "string" && typeof parsed.session_id === "string") {
455
+ storage.setItem(hintKey(this.appKey), JSON.stringify({ username: parsed.username, session_id: parsed.session_id }));
456
+ } else {
457
+ storage.removeItem(hintKey(this.appKey));
458
+ }
459
+ } catch {
460
+ }
461
+ }
462
+ };
463
+ var WindowKv = class {
464
+ constructor(bridge, appKey) {
465
+ this.bridge = bridge;
466
+ this.appKey = appKey;
467
+ this.#sessionKey = `im:${appKey}:session`;
468
+ this.#hint = readHint(appKey);
469
+ bridge.on("kv", (event) => {
470
+ if (event.app_key !== this.appKey || event.key !== "session") return;
471
+ this.#update(event.hint ?? null);
472
+ });
473
+ void this.refresh().catch(() => {
474
+ });
475
+ }
476
+ bridge;
477
+ appKey;
478
+ #hint;
479
+ #listeners = /* @__PURE__ */ new Set();
480
+ #sessionKey;
481
+ get(key) {
482
+ if (key === DEVICE_ID_KEY) return this.bridge.info?.device_id ?? null;
483
+ if (key !== this.#sessionKey || !this.#hint) return null;
484
+ return JSON.stringify({ v: 1, username: this.#hint.username, session_id: this.#hint.session_id, access_token: "", access_expires_at: null, refresh_token: "", refresh_expires_at: null, login_method: "password" });
485
+ }
486
+ set() {
487
+ }
488
+ remove() {
489
+ }
490
+ onChange(listener) {
491
+ this.#listeners.add(listener);
492
+ return () => this.#listeners.delete(listener);
493
+ }
494
+ async refresh() {
495
+ const hint = await this.bridge.call("session_hint", { app_key: this.appKey });
496
+ this.#update(hint);
497
+ }
498
+ #update(hint) {
499
+ const same = hint === null ? this.#hint === null : this.#hint !== null && this.#hint.username === hint.username && this.#hint.session_id === hint.session_id;
500
+ if (same) return;
501
+ this.#hint = hint ? { username: hint.username, session_id: hint.session_id } : null;
502
+ for (const listener of [...this.#listeners]) listener(this.#sessionKey);
503
+ }
504
+ };
505
+ function readHint(appKey) {
506
+ const storage = localStorageSafe();
507
+ if (!storage) return null;
508
+ try {
509
+ const raw = storage.getItem(hintKey(appKey));
510
+ if (!raw) return null;
511
+ const hint = JSON.parse(raw);
512
+ return typeof hint.username === "string" && typeof hint.session_id === "string" ? { username: hint.username, session_id: hint.session_id } : null;
513
+ } catch {
514
+ return null;
515
+ }
516
+ }
517
+ function memoryKv() {
518
+ const data = /* @__PURE__ */ new Map();
519
+ return {
520
+ get: (key) => data.get(key) ?? null,
521
+ set: (key, value) => void data.set(key, value),
522
+ remove: (key) => void data.delete(key),
523
+ onChange: () => () => {
524
+ }
525
+ };
526
+ }
527
+
528
+ // src/adapter/locks.ts
529
+ function abortError() {
530
+ return Object.assign(new Error("aborted"), { name: "AbortError" });
531
+ }
532
+ var PluginLocks = class {
533
+ constructor(bridge) {
534
+ this.bridge = bridge;
535
+ bridge.on("lock_stolen", (event) => {
536
+ const reject = this.#stolen.get(String(event.id));
537
+ if (reject) {
538
+ this.#stolen.delete(String(event.id));
539
+ reject(abortError());
540
+ }
541
+ });
542
+ }
543
+ bridge;
544
+ native = true;
545
+ #stolen = /* @__PURE__ */ new Map();
546
+ #seq = 0;
547
+ async request(name, opts, fn) {
548
+ if (opts.signal?.aborted) throw opts.signal.reason ?? abortError();
549
+ const id = `${this.bridge.label}:${++this.#seq}:${Math.random().toString(36).slice(2)}`;
550
+ const onAbort = () => void this.bridge.call("lock_cancel", { id }).catch(() => {
551
+ });
552
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
553
+ let granted;
554
+ try {
555
+ granted = await this.bridge.call("lock_request", { name, id, steal: opts.steal === true, if_available: opts.ifAvailable === true });
556
+ } catch (err) {
557
+ if (opts.signal?.aborted) throw opts.signal.reason ?? abortError();
558
+ throw err;
559
+ } finally {
560
+ opts.signal?.removeEventListener("abort", onAbort);
561
+ }
562
+ if (!granted) return fn(false);
563
+ let stolen = false;
564
+ const stolenPromise = new Promise((_, reject) => {
565
+ this.#stolen.set(id, (err) => {
566
+ stolen = true;
567
+ reject(err);
568
+ });
569
+ });
570
+ stolenPromise.catch(() => {
571
+ });
572
+ try {
573
+ return await Promise.race([fn(true), stolenPromise]);
574
+ } finally {
575
+ this.#stolen.delete(id);
576
+ if (!stolen) void this.bridge.call("lock_release", { id }).catch(() => {
577
+ });
578
+ }
579
+ }
580
+ };
581
+
582
+ // src/adapter/visibility.ts
583
+ function engineVisibility() {
584
+ return { visible: () => false, focused: () => false, onChange: () => () => {
585
+ } };
586
+ }
587
+ function windowVisibility(bridge) {
588
+ const doc = typeof document !== "undefined" ? document : void 0;
589
+ const pageVisible = () => doc ? doc.visibilityState !== "hidden" : true;
590
+ const pageFocused = () => doc && typeof doc.hasFocus === "function" ? doc.hasFocus() : true;
591
+ let plugin = { visible: pageVisible(), focused: pageFocused() };
592
+ let last = current();
593
+ const listeners = /* @__PURE__ */ new Set();
594
+ function current() {
595
+ const visible = pageVisible() && plugin.visible;
596
+ return { visible, focused: visible && plugin.focused };
597
+ }
598
+ function fire() {
599
+ const next = current();
600
+ if (next.visible === last.visible && next.focused === last.focused) return;
601
+ last = next;
602
+ for (const listener of [...listeners]) listener(next);
603
+ }
604
+ function apply(state) {
605
+ if (!state) return;
606
+ plugin = { visible: state.visible !== false, focused: state.focused === true };
607
+ fire();
608
+ }
609
+ void bridge.ready.then((info) => apply(info.window), () => {
610
+ });
611
+ bridge.on("window", (event) => apply(event));
612
+ bridge.on("screen_lock", () => void refresh());
613
+ const refresh = async () => {
614
+ try {
615
+ apply(await bridge.call("window_state"));
616
+ } catch {
617
+ fire();
618
+ }
619
+ };
620
+ if (doc && typeof window !== "undefined") {
621
+ const onFocus = () => {
622
+ plugin = { ...plugin, focused: true, visible: true };
623
+ fire();
624
+ void refresh();
625
+ };
626
+ const onBlur = () => {
627
+ plugin = { ...plugin, focused: false };
628
+ fire();
629
+ void refresh();
630
+ };
631
+ doc.addEventListener("visibilitychange", () => {
632
+ fire();
633
+ void refresh();
634
+ });
635
+ window.addEventListener("focus", onFocus);
636
+ window.addEventListener("blur", onBlur);
637
+ }
638
+ return {
639
+ visible: () => current().visible,
640
+ focused: () => current().focused,
641
+ onChange: (listener) => {
642
+ listeners.add(listener);
643
+ return () => listeners.delete(listener);
644
+ }
645
+ };
646
+ }
647
+ function pluginNetwork(bridge) {
648
+ let pluginOnline = null;
649
+ const listeners = /* @__PURE__ */ new Set();
650
+ const navOnline = () => typeof navigator !== "undefined" && typeof navigator.onLine === "boolean" ? navigator.onLine : true;
651
+ const online = () => pluginOnline ?? navOnline();
652
+ const fire = (value) => {
653
+ for (const listener of [...listeners]) listener(value);
654
+ };
655
+ bridge.on("network", (event) => {
656
+ pluginOnline = event.online !== false;
657
+ fire(online());
658
+ });
659
+ if (typeof window !== "undefined") {
660
+ window.addEventListener("online", () => fire(true));
661
+ window.addEventListener("offline", () => {
662
+ if (pluginOnline !== true) fire(false);
663
+ });
664
+ }
665
+ return {
666
+ online,
667
+ onChange: (listener) => {
668
+ listeners.add(listener);
669
+ return () => listeners.delete(listener);
670
+ }
671
+ };
672
+ }
673
+
674
+ // src/adapter/index.ts
675
+ function createTauriPlatform(opts) {
676
+ const { bridge, appKey, role } = opts;
677
+ const engineKv = role === "leader" ? new EngineKv(bridge, appKey, (err) => opts.onWarn("写入安全存储失败", err)) : null;
678
+ const windowKv = role === "follower" ? new WindowKv(bridge, appKey) : null;
679
+ const files = pluginFiles(bridge);
680
+ let workerTimers = null;
681
+ const precise = () => {
682
+ if (!workerTimers) workerTimers = new WorkerTimers((reason) => opts.onWarn(`计时器 Worker 不可用,改用页面的计时器:${reason}`));
683
+ return workerTimers;
684
+ };
685
+ const g = globalThis;
686
+ const clock = {
687
+ now: () => Date.now(),
688
+ monotonic: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
689
+ setTimeout: (fn, ms) => g.setTimeout(fn, ms),
690
+ clearTimeout: (id) => g.clearTimeout(id),
691
+ // 存活信号、心跳、续期计划在专用 Worker 中计时,不受隐藏页面的节流(《2.功能概述文档》第 4.5 节)
692
+ setPreciseTimeout: (fn, ms) => precise().setTimeout(fn, ms),
693
+ clearPreciseTimeout: (id) => precise().clearTimeout(id)
694
+ };
695
+ let engineState = bridge.info?.engine_state ?? "starting";
696
+ let host = null;
697
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
698
+ const platform = {
699
+ fetch: (input, init) => g.fetch(input, init),
700
+ createWebSocket: (url, protocols) => {
701
+ if (role !== "leader") throw new DRError({ code: "invalid_state", message: "界面窗口不建立长连接", details: { reason: "not_engine_window" }, source: "local" });
702
+ return wrapWebSocket(new WebSocket(url, protocols));
703
+ },
704
+ kv: engineKv ?? windowKv,
705
+ sessionKv: memoryKv(),
706
+ openDatabase: (name, version, upgrade) => TauriDatabase.open(bridge, appKey, name, version, upgrade, opts.encrypt === true),
707
+ deleteDatabase: async (name) => {
708
+ if (role !== "leader") return;
709
+ const prefix = `im:${appKey}:`;
710
+ if (!name.startsWith(prefix)) return;
711
+ await bridge.call("db_delete", { app_key: appKey, username: name.slice(prefix.length) });
712
+ },
713
+ locks: new PluginLocks(bridge),
714
+ channel: (name) => pluginChannel(bridge, name, (err) => opts.onWarn(`频道消息发送失败:${name}`, err)),
715
+ clock,
716
+ visibility: role === "leader" ? engineVisibility() : windowVisibility(bridge),
717
+ lifecycle: pageLifecycle(),
718
+ network: pluginNetwork(bridge),
719
+ random: cryptoRandom(),
720
+ environment: {
721
+ userAgent: ua,
722
+ osVersion: async () => (await bridge.ready).os_version,
723
+ secureContext: true,
724
+ browser: true,
725
+ missingCapabilities: () => {
726
+ const missing = [];
727
+ const any = globalThis;
728
+ for (const name of ["WebSocket", "fetch", "TextEncoder", "queueMicrotask", "AbortController", "Worker"]) if (typeof any[name] === "undefined") missing.push(name);
729
+ if (typeof globalThis.crypto?.randomUUID !== "function") missing.push("crypto.randomUUID");
730
+ if (typeof any.OffscreenCanvas === "undefined") missing.push("OffscreenCanvas");
731
+ return missing;
732
+ },
733
+ deviceName: () => bridge.info?.computer_name || null
734
+ },
735
+ createWorker: (source) => {
736
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
737
+ const worker = new Worker(url);
738
+ const wrapper = {
739
+ postMessage: (msg, transfer) => worker.postMessage(msg, transfer ?? []),
740
+ onmessage: null,
741
+ terminate: () => {
742
+ worker.terminate();
743
+ URL.revokeObjectURL(url);
744
+ }
745
+ };
746
+ worker.onmessage = (event) => wrapper.onmessage?.(event.data);
747
+ return wrapper;
748
+ },
749
+ role,
750
+ rtcChannelsSupported: () => role === "follower" && typeof RTCPeerConnection === "function",
751
+ serverCallsInLeader: true,
752
+ viewingRequiresFocus: true,
753
+ files,
754
+ leaderUnavailable: () => bridge.info?.another_instance ? { code: "invalid_state", details: { reason: "another_instance" } } : { code: "engine_unavailable", details: { reason: engineState === "ready" ? "starting" : engineState } },
755
+ prepareForward: async (_method, args) => await blobsToPaths(args, (blob, name) => files.writeBlobTemp(blob, name)),
756
+ extendClient: (h) => {
757
+ host = h;
758
+ return opts.extend(h);
759
+ }
760
+ };
761
+ return {
762
+ platform,
763
+ engineKv,
764
+ windowKv,
765
+ files,
766
+ host: () => {
767
+ if (!host) throw new Error("客户端还没有创建");
768
+ return host;
769
+ },
770
+ setEngineState: (state) => {
771
+ engineState = state;
772
+ }
773
+ };
774
+ }
775
+ function pageLifecycle() {
776
+ const win = typeof window !== "undefined" ? window : void 0;
777
+ return {
778
+ onPageHide: (listener) => {
779
+ if (!win) return () => {
780
+ };
781
+ const handler = () => listener(false);
782
+ win.addEventListener("pagehide", handler);
783
+ return () => win.removeEventListener("pagehide", handler);
784
+ },
785
+ onPageShow: () => () => {
786
+ },
787
+ onFreeze: () => () => {
788
+ },
789
+ onResume: () => () => {
790
+ }
791
+ };
792
+ }
793
+ function cryptoRandom() {
794
+ const c = globalThis.crypto;
795
+ return {
796
+ uuid: () => c.randomUUID(),
797
+ bytes: (n) => {
798
+ const out = new Uint8Array(n);
799
+ c.getRandomValues(out);
800
+ return out;
801
+ },
802
+ float: () => Math.random()
803
+ };
804
+ }
805
+ function wrapWebSocket(ws) {
806
+ const wrapper = {
807
+ get readyState() {
808
+ return ws.readyState;
809
+ },
810
+ send: (data) => ws.send(data),
811
+ close: (code, reason) => ws.close(code, reason),
812
+ onopen: null,
813
+ onmessage: null,
814
+ onclose: null,
815
+ onerror: null
816
+ };
817
+ ws.onopen = () => wrapper.onopen?.();
818
+ ws.onmessage = (event) => {
819
+ if (typeof event.data === "string") wrapper.onmessage?.(event.data);
820
+ };
821
+ ws.onclose = (event) => wrapper.onclose?.(event.code, event.reason);
822
+ ws.onerror = () => wrapper.onerror?.();
823
+ return wrapper;
824
+ }
825
+
826
+ // src/version.ts
827
+ var TAURI_SDK_VERSION = "1.0.0-alpha" ;
828
+ var PROTOCOL_VERSION = 1;
829
+ var ENGINE_LABEL = "deeprespond-im-engine";
830
+ var PLUGIN = "plugin:deeprespond-im|";
831
+ var tauriTransport = {
832
+ invoke: (cmd, args, options) => invoke(cmd, args, options),
833
+ channel: (onEvent) => {
834
+ const channel = new Channel();
835
+ channel.onmessage = onEvent;
836
+ return channel;
837
+ }
838
+ };
839
+ function currentLabel() {
840
+ if (!isTauri()) return null;
841
+ const internals = globalThis.__TAURI_INTERNALS__;
842
+ return internals?.metadata?.currentWebview?.label ?? internals?.metadata?.currentWindow?.label ?? null;
843
+ }
844
+ function toError(err, command) {
845
+ if (err instanceof DRError) return err;
846
+ if (err && typeof err === "object" && typeof err.code === "string") {
847
+ const e = err;
848
+ const details = {};
849
+ if (e.reason) details.reason = e.reason;
850
+ return new DRError({ code: e.code, message: e.message ?? e.code, details, source: "local" });
851
+ }
852
+ const text = typeof err === "string" ? err : err instanceof Error ? err.message : String(err);
853
+ if (/not allowed|denied|ACL|capabilit/i.test(text)) {
854
+ return new DRError({ code: "permission_denied", message: text, details: { reason: "capability_missing", command }, source: "local" });
855
+ }
856
+ return new DRError({ code: "invalid_state", message: text, details: { reason: "plugin", command }, source: "local" });
857
+ }
858
+ var Bridge = class _Bridge {
859
+ static #instance = null;
860
+ static get() {
861
+ if (!_Bridge.#instance) _Bridge.#instance = new _Bridge(currentLabel() ?? "");
862
+ return _Bridge.#instance;
863
+ }
864
+ /** 测试用:指定本页面的连接(假的插件)。 */
865
+ static use(bridge) {
866
+ _Bridge.#instance = bridge;
867
+ }
868
+ label;
869
+ isEngine;
870
+ ready;
871
+ #info = null;
872
+ #listeners = /* @__PURE__ */ new Map();
873
+ #transport;
874
+ constructor(label, transport = tauriTransport) {
875
+ this.label = label;
876
+ this.isEngine = label === ENGINE_LABEL;
877
+ this.#transport = transport;
878
+ const channel = transport.channel((event) => this.#dispatch(event));
879
+ this.ready = (async () => {
880
+ let info;
881
+ try {
882
+ info = await transport.invoke(`${PLUGIN}hello`, { js_version: TAURI_SDK_VERSION, protocol: PROTOCOL_VERSION, channel });
883
+ } catch (err) {
884
+ throw toError(err, "hello");
885
+ }
886
+ if (info.plugin_version !== TAURI_SDK_VERSION || info.protocol !== PROTOCOL_VERSION) {
887
+ throw new DRError({
888
+ code: "unsupported",
889
+ message: `@deeprespond/im-tauri ${TAURI_SDK_VERSION} 与 tauri-plugin-deeprespond-im ${info.plugin_version} 的版本不同`,
890
+ details: { reason: "version_mismatch", js_version: TAURI_SDK_VERSION, plugin_version: info.plugin_version },
891
+ source: "local"
892
+ });
893
+ }
894
+ this.#info = info;
895
+ return info;
896
+ })();
897
+ this.ready.catch(() => {
898
+ });
899
+ }
900
+ /** hello 之后的信息;之前为 null。 */
901
+ get info() {
902
+ return this.#info;
903
+ }
904
+ /** 调用插件命令(等 hello 完成)。 */
905
+ async call(command, args) {
906
+ await this.ready;
907
+ try {
908
+ return await this.#transport.invoke(`${PLUGIN}${command}`, args);
909
+ } catch (err) {
910
+ throw toError(err, command);
911
+ }
912
+ }
913
+ /** 原始字节的请求体(Blob 落盘),参数放在请求头中。 */
914
+ async callRaw(command, body, headers) {
915
+ await this.ready;
916
+ try {
917
+ return await this.#transport.invoke(`${PLUGIN}${command}`, body, { headers });
918
+ } catch (err) {
919
+ throw toError(err, command);
920
+ }
921
+ }
922
+ on(type, listener) {
923
+ let set = this.#listeners.get(type);
924
+ if (!set) {
925
+ set = /* @__PURE__ */ new Set();
926
+ this.#listeners.set(type, set);
927
+ }
928
+ set.add(listener);
929
+ return () => set?.delete(listener);
930
+ }
931
+ #dispatch(event) {
932
+ if (!event || typeof event.t !== "string") return;
933
+ for (const listener of [...this.#listeners.get(event.t) ?? []]) {
934
+ try {
935
+ listener(event);
936
+ } catch (err) {
937
+ console.warn(`[im] 处理插件事件出错:${event.t}`, err);
938
+ }
939
+ }
940
+ for (const listener of [...this.#listeners.get("*") ?? []]) listener(event);
941
+ }
942
+ };
943
+ function makeLogger(bridge, option) {
944
+ const pending = [];
945
+ let timer = null;
946
+ const flush = () => {
947
+ timer = null;
948
+ const entries = pending.splice(0);
949
+ if (entries.length > 0) void bridge.call("log_write", { entries }).catch(() => {
950
+ });
951
+ };
952
+ return {
953
+ level: option?.level,
954
+ sink: (entry) => {
955
+ if (option?.sink) {
956
+ try {
957
+ option.sink(entry);
958
+ } catch {
959
+ }
960
+ } else if (entry.level === "error" || entry.level === "warn") {
961
+ const print = entry.level === "error" ? console.error : console.warn;
962
+ print(`[im] ${entry.module}: ${entry.message}`, entry.code ?? "", entry.reason ?? "");
963
+ }
964
+ if (!bridge.info?.log_file) return;
965
+ pending.push(entry);
966
+ if (pending.length > 500) pending.splice(0, pending.length - 500);
967
+ if (timer === null) timer = setTimeout(flush, 1e3);
968
+ }
969
+ };
970
+ }
971
+ function clickEvent(bridge, event) {
972
+ const clickId = String(event.click_id ?? "");
973
+ let claimed = false;
974
+ const out = {
975
+ id: String(event.id ?? ""),
976
+ kind: event.kind ?? "custom",
977
+ launched: event.launched === true,
978
+ claim: () => {
979
+ if (claimed || !clickId) return;
980
+ claimed = true;
981
+ void bridge.call("notify_claim", { click_id: clickId }).catch(() => {
982
+ });
983
+ }
984
+ };
985
+ if (typeof event.conversation_id === "string") out.conversation_id = event.conversation_id;
986
+ if (typeof event.seq === "number") out.seq = event.seq;
987
+ if (typeof event.action === "string") out.action = event.action;
988
+ if (event.data && typeof event.data === "object") out.data = event.data;
989
+ return Object.freeze(out);
990
+ }
991
+ function unsupported(reason, message) {
992
+ return new DRError({ code: "unsupported", message, details: { reason }, source: "local" });
993
+ }
994
+ function invalidState(reason) {
995
+ return new DRError({ code: "invalid_state", message: reason, details: { reason }, source: "local" });
996
+ }
997
+ function validation(reason, field) {
998
+ return new DRError({ code: "local_validation", message: reason, details: { reason, field }, source: "local" });
999
+ }
1000
+ var Desktop = class {
1001
+ constructor(bridge, client) {
1002
+ this.bridge = bridge;
1003
+ this.#client = client;
1004
+ Object.preventExtensions(this);
1005
+ }
1006
+ bridge;
1007
+ #client;
1008
+ #seq = 0;
1009
+ async notify(n) {
1010
+ if (!n || typeof n.title !== "string" || typeof n.body !== "string") throw validation("required", "title");
1011
+ if (n.id !== void 0 && (typeof n.id !== "string" || n.id.startsWith("dr:"))) throw validation("reserved_id", "id");
1012
+ if (n.data !== void 0) {
1013
+ if (typeof n.data !== "object" || Object.values(n.data).some((v) => typeof v !== "string")) throw validation("invalid_type", "data");
1014
+ if (new TextEncoder().encode(JSON.stringify(n.data)).length > 1024) throw validation("too_large", "data");
1015
+ }
1016
+ if (n.actions && (n.actions.length > 2 || n.actions.some((a) => typeof a?.id !== "string" || typeof a.title !== "string"))) throw validation("invalid_value", "actions");
1017
+ await this.show({ ...n, id: n.id ?? `app:${Date.now().toString(36)}:${++this.#seq}`, kind: "custom" });
1018
+ }
1019
+ /** SDK 自己显示的通知(id 以 dr: 开头)。 */
1020
+ async show(n) {
1021
+ await this.bridge.call("notify_show", {
1022
+ id: n.id,
1023
+ title: n.title,
1024
+ body: n.body,
1025
+ kind: n.kind,
1026
+ conversation_id: n.conversation_id ?? null,
1027
+ seq: n.seq ?? null,
1028
+ data: n.data ?? null,
1029
+ sound: n.sound !== false,
1030
+ actions: n.actions ?? null,
1031
+ group: n.group ?? n.conversation_id ?? null
1032
+ });
1033
+ }
1034
+ async clearNotifications(p = {}) {
1035
+ await this.bridge.call("notify_clear", p.conversation_id ? { conversation_id: p.conversation_id } : { all: true });
1036
+ }
1037
+ /** 清除一条(来电被处理后)。 */
1038
+ async clear(id) {
1039
+ await this.bridge.call("notify_clear", { id });
1040
+ }
1041
+ notificationPermission() {
1042
+ return this.bridge.call("notify_permission");
1043
+ }
1044
+ requestNotificationPermission() {
1045
+ return this.bridge.call("notify_request_permission");
1046
+ }
1047
+ async setBadge(count) {
1048
+ if (count !== null && (!Number.isInteger(count) || count < 0)) throw validation("invalid_value", "count");
1049
+ await this.bridge.call("badge_set", { count });
1050
+ }
1051
+ async requestAttention(p = {}) {
1052
+ await this.bridge.call("attention_request", { critical: p.critical === true, label: this.bridge.isEngine ? null : this.bridge.label });
1053
+ }
1054
+ async trayBlink(p) {
1055
+ if (p === null) {
1056
+ await this.bridge.call("tray_blink", { blink: null });
1057
+ return;
1058
+ }
1059
+ if (typeof p.trayId !== "string" || !Array.isArray(p.icons) || p.icons.length !== 2) throw validation("invalid_value", "icons");
1060
+ await this.bridge.call("tray_blink", { blink: { tray_id: p.trayId, icons: p.icons, interval_ms: p.intervalMs ?? 500 } });
1061
+ }
1062
+ pickFiles(p = {}) {
1063
+ return this.bridge.call("files_pick", { multiple: p.multiple === true, filters: p.filters ?? null });
1064
+ }
1065
+ pickSavePath(p = {}) {
1066
+ return this.bridge.call("files_pick_save", { default_name: p.defaultName ?? null });
1067
+ }
1068
+ fileUrl(path) {
1069
+ if (typeof path !== "string" || path === "") throw validation("required", "path");
1070
+ return fileUrl(path, this.info.os);
1071
+ }
1072
+ /**
1073
+ * 由 Rust 下载到磁盘(《2.功能概述文档》第 9.4 节):文件地址先换取下载地址(界面窗口中经引擎);
1074
+ * 地址过期(403)时重新换取后续传。
1075
+ */
1076
+ download(source, p = {}) {
1077
+ const task = newTask("down");
1078
+ const controller = new AbortController();
1079
+ if (p.signal) {
1080
+ if (p.signal.aborted) controller.abort();
1081
+ else p.signal.addEventListener("abort", () => controller.abort(), { once: true });
1082
+ }
1083
+ const run = async () => {
1084
+ let url;
1085
+ let name;
1086
+ if (typeof source === "string") {
1087
+ url = source;
1088
+ } else {
1089
+ const body = source?.message?.body ?? {};
1090
+ if (typeof body.url !== "string" || body.url === "") throw validation("required", "message.body.url");
1091
+ url = body.url;
1092
+ if (typeof body.name === "string") name = body.name;
1093
+ }
1094
+ const client = this.#client();
1095
+ const isFile = client.files.isFileUrl(url);
1096
+ const resolve = async () => isFile ? (await client.files.resolveUrl(url)).download_url : url;
1097
+ const off = this.bridge.on("download_progress", (event) => {
1098
+ if (event.task === task && typeof event.loaded === "number") p.onProgress?.(event.loaded, typeof event.total === "number" ? event.total : null);
1099
+ });
1100
+ const onAbort = () => void this.bridge.call("files_cancel", { task }).catch(() => {
1101
+ });
1102
+ controller.signal.addEventListener("abort", onAbort, { once: true });
1103
+ try {
1104
+ let target = await resolve();
1105
+ for (let attempt = 0; ; attempt++) {
1106
+ if (controller.signal.aborted) throw new DRError({ code: "aborted", message: "aborted", source: "local" });
1107
+ try {
1108
+ return await this.bridge.call("files_download", { task, url: target, dest: p.saveTo ?? null, conflict: p.conflict ?? "rename", name: name ?? null });
1109
+ } catch (err) {
1110
+ if (err instanceof DRError && err.code === "url_expired" && isFile && attempt < 2) {
1111
+ target = await resolve();
1112
+ continue;
1113
+ }
1114
+ throw err;
1115
+ }
1116
+ }
1117
+ } finally {
1118
+ off();
1119
+ controller.signal.removeEventListener("abort", onAbort);
1120
+ }
1121
+ };
1122
+ const promise = run();
1123
+ promise.cancel = () => controller.abort();
1124
+ return promise;
1125
+ }
1126
+ async openPath(path) {
1127
+ await this.bridge.call("files_open", { path });
1128
+ }
1129
+ async revealPath(path) {
1130
+ await this.bridge.call("files_reveal", { path });
1131
+ }
1132
+ async exportDiagnostics(p = {}) {
1133
+ const client = this.#client();
1134
+ let diagnostics = null;
1135
+ try {
1136
+ diagnostics = client.connection.getDiagnostics();
1137
+ } catch {
1138
+ }
1139
+ return this.bridge.call("diagnostics_export", { save_to: p.saveTo ?? null, info: { label: this.bridge.label, sdk_version: TAURI_SDK_VERSION, desktop: this.info, diagnostics } });
1140
+ }
1141
+ get info() {
1142
+ const info = this.bridge.info;
1143
+ let calls = false;
1144
+ try {
1145
+ calls = this.#client().calls.supported;
1146
+ } catch {
1147
+ calls = false;
1148
+ }
1149
+ return Object.freeze({
1150
+ os: info?.os ?? osFromUserAgent(typeof navigator !== "undefined" ? navigator.userAgent : ""),
1151
+ os_version: info?.os_version ?? "",
1152
+ webview_version: info?.webview_version ?? "",
1153
+ sdk_version: TAURI_SDK_VERSION,
1154
+ calls_supported: calls
1155
+ });
1156
+ }
1157
+ };
1158
+
1159
+ export { Bridge, Desktop, ENGINE_LABEL, TAURI_SDK_VERSION, clickEvent, createTauriPlatform, currentLabel, invalidState, makeLogger, osFromUserAgent, unsupported };
1160
+ //# sourceMappingURL=chunk-DMEEI2YZ.js.map
1161
+ //# sourceMappingURL=chunk-DMEEI2YZ.js.map