@onekeyfe/react-native-native-list 3.0.105 → 3.0.107
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/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
- package/ios/HybridNativeList.swift +8 -2
- package/ios/NativeListCell.swift +630 -31
- package/ios/NativeListDesignAssets.swift +24 -1
- package/ios/RNCNativeListView.swift +216 -42
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
- package/lib/module/NativeList.js +102 -8
- package/lib/module/NativeList.web.js +20 -2
- package/lib/module/avatarPrefetch.js +174 -0
- package/lib/module/validation.js +45 -2
- package/lib/module/web/NativeListAvatarWorker.js +542 -0
- package/lib/module/web/NativeListWebAvatarCache.js +267 -0
- package/lib/module/web/NativeListWebEngine.js +958 -47
- package/lib/typescript/src/NativeList.web.d.ts +2 -2
- package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
- package/lib/typescript/src/models.d.ts +72 -4
- package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
- package/package.json +2 -2
- package/src/NativeList.tsx +149 -13
- package/src/NativeList.web.tsx +30 -3
- package/src/avatarPrefetch.ts +236 -0
- package/src/models.ts +98 -7
- package/src/validation.ts +131 -2
- package/src/web/NativeListAvatarWorker.js +567 -0
- package/src/web/NativeListWebAvatarCache.ts +314 -0
- package/src/web/NativeListWebEngine.ts +1409 -60
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/* global indexedDB, OffscreenCanvas, createImageBitmap, postMessage, onmessage: writable */
|
|
2
|
+
// OneKey patch: avatar generation and PNG bytes stay in this external worker.
|
|
3
|
+
// Algorithm ported from ethereum-blockies-base64 1.0.2 by MyCrypto (MIT):
|
|
4
|
+
// https://github.com/MyCryptoHQ/ethereum-blockies-base64
|
|
5
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
// in the Software without restriction, including without limitation the rights
|
|
8
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
// furnished to do so, subject to the following conditions:
|
|
11
|
+
// The above copyright notice and this permission notice shall be included in
|
|
12
|
+
// all copies or substantial portions of the Software.
|
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
// THE SOFTWARE.
|
|
20
|
+
|
|
21
|
+
const PREFIX = 'onekey-avatar://blockie/v1/';
|
|
22
|
+
const DATABASE = 'onekey-native-list-avatar-v1';
|
|
23
|
+
const MAX_DISK_BYTES = 32 * 1024 * 1024;
|
|
24
|
+
const MAX_DISK_ENTRIES = 2048;
|
|
25
|
+
const MAX_CONCURRENT_LOADS = 2;
|
|
26
|
+
const requests = new Map();
|
|
27
|
+
const images = new Map();
|
|
28
|
+
const jobs = new Map();
|
|
29
|
+
const queue = [];
|
|
30
|
+
let activeLoads = 0;
|
|
31
|
+
let databasePromise;
|
|
32
|
+
// OneKey patch: persistence never occupies either display-load slot. Pending bytes
|
|
33
|
+
// bridge the last-lease/reacquire window until a bounded background write finishes.
|
|
34
|
+
const MAX_PENDING_WRITES = 32;
|
|
35
|
+
const MAX_PENDING_BYTES = 4 * 1024 * 1024;
|
|
36
|
+
const WRITE_DEADLINE_MS = 32;
|
|
37
|
+
const READ_BUDGET_MS = 8;
|
|
38
|
+
const MAX_PENDING_READS = 2;
|
|
39
|
+
const DISK_IDLE_MS = 100;
|
|
40
|
+
const pendingBlobs = new Map();
|
|
41
|
+
const diskQueue = [];
|
|
42
|
+
const touches = new Map();
|
|
43
|
+
const priorities = new Map();
|
|
44
|
+
let pendingBytes = 0;
|
|
45
|
+
let writing = false;
|
|
46
|
+
let diskTimer;
|
|
47
|
+
let lastAcquire = 0;
|
|
48
|
+
let pendingReads = 0;
|
|
49
|
+
|
|
50
|
+
function diskIsIdle() {
|
|
51
|
+
return (
|
|
52
|
+
!activeLoads &&
|
|
53
|
+
!queue.length &&
|
|
54
|
+
!pendingReads &&
|
|
55
|
+
Date.now() - lastAcquire >= DISK_IDLE_MS
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scheduleDiskWork() {
|
|
60
|
+
if (
|
|
61
|
+
writing ||
|
|
62
|
+
diskTimer !== undefined ||
|
|
63
|
+
activeLoads ||
|
|
64
|
+
queue.length ||
|
|
65
|
+
pendingReads ||
|
|
66
|
+
(!diskQueue.length && !touches.size)
|
|
67
|
+
)
|
|
68
|
+
return;
|
|
69
|
+
const delay = Math.max(0, DISK_IDLE_MS - (Date.now() - lastAcquire));
|
|
70
|
+
diskTimer = setTimeout(() => {
|
|
71
|
+
diskTimer = undefined;
|
|
72
|
+
void drainDiskQueue();
|
|
73
|
+
}, delay);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function persistLater(uri, blob) {
|
|
77
|
+
if (pendingBlobs.has(uri) || blob.size > MAX_PENDING_BYTES) return;
|
|
78
|
+
// OneKey patch: a long scroll retains the most recent bounded write window,
|
|
79
|
+
// rather than filling it once and dropping every later result. Never evict I/O in flight.
|
|
80
|
+
// if (pendingBlobs.size >= MAX_PENDING_WRITES || pendingBytes + blob.size > MAX_PENDING_BYTES) return;
|
|
81
|
+
while (
|
|
82
|
+
pendingBlobs.size >= MAX_PENDING_WRITES ||
|
|
83
|
+
pendingBytes + blob.size > MAX_PENDING_BYTES
|
|
84
|
+
) {
|
|
85
|
+
const oldest = diskQueue.shift();
|
|
86
|
+
if (!oldest) return;
|
|
87
|
+
if (pendingBlobs.get(oldest.uri) === oldest.blob)
|
|
88
|
+
pendingBlobs.delete(oldest.uri);
|
|
89
|
+
pendingBytes -= oldest.blob.size;
|
|
90
|
+
}
|
|
91
|
+
pendingBlobs.set(uri, blob);
|
|
92
|
+
pendingBytes += blob.size;
|
|
93
|
+
diskQueue.push({ uri, blob });
|
|
94
|
+
scheduleDiskWork();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function drainDiskQueue() {
|
|
98
|
+
// OneKey patch: a momentary empty load queue is not a scroll-idle window.
|
|
99
|
+
// if (writing || activeLoads || queue.length) return;
|
|
100
|
+
if (writing) return;
|
|
101
|
+
if (!diskIsIdle()) {
|
|
102
|
+
scheduleDiskWork();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
writing = true;
|
|
106
|
+
try {
|
|
107
|
+
if (diskQueue.length) {
|
|
108
|
+
const { uri, blob } = diskQueue.shift();
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = await writeDisk(uri, blob);
|
|
112
|
+
} catch {
|
|
113
|
+
/* Persistence remains best-effort. */
|
|
114
|
+
}
|
|
115
|
+
if (result === 'deferred') {
|
|
116
|
+
diskQueue.unshift({ uri, blob });
|
|
117
|
+
} else {
|
|
118
|
+
if (pendingBlobs.get(uri) === blob) pendingBlobs.delete(uri);
|
|
119
|
+
pendingBytes -= blob.size;
|
|
120
|
+
const image = images.get(uri);
|
|
121
|
+
if (result === true && image?.blob === blob) image.persisted = true;
|
|
122
|
+
}
|
|
123
|
+
} else if (touches.size) {
|
|
124
|
+
await flushTouches();
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
writing = false;
|
|
128
|
+
scheduleDiskWork();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// OneKey patch: abort is a request, not a bounded browser store-lock release.
|
|
133
|
+
// Display reads have their own budget below, even if this transaction is committing.
|
|
134
|
+
function cacheWriteDeadline(transaction, resolve) {
|
|
135
|
+
let timer;
|
|
136
|
+
const check = () => {
|
|
137
|
+
if (
|
|
138
|
+
!activeLoads &&
|
|
139
|
+
!queue.length &&
|
|
140
|
+
Date.now() - lastAcquire >= DISK_IDLE_MS
|
|
141
|
+
) {
|
|
142
|
+
timer = setTimeout(check, WRITE_DEADLINE_MS);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
transaction.abort();
|
|
147
|
+
} catch {
|
|
148
|
+
/* The browser already started committing. */
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
timer = setTimeout(check, WRITE_DEADLINE_MS);
|
|
152
|
+
const finish = (success) => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
resolve(success);
|
|
155
|
+
};
|
|
156
|
+
transaction.oncomplete = () => finish(true);
|
|
157
|
+
transaction.onabort = () => finish(false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function touchLater(uri) {
|
|
161
|
+
touches.delete(uri);
|
|
162
|
+
touches.set(uri, Date.now());
|
|
163
|
+
if (touches.size > 128) touches.delete(touches.keys().next().value);
|
|
164
|
+
// OneKey patch: touches share the same idle gate and writer as persistence.
|
|
165
|
+
// if (touchTimer === undefined) touchTimer = setTimeout(flushTouches, 250);
|
|
166
|
+
scheduleDiskWork();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function flushTouches() {
|
|
170
|
+
const database = await openDatabase();
|
|
171
|
+
// Opening the database can yield across a new acquire; recheck before starting I/O.
|
|
172
|
+
if (!diskIsIdle()) return;
|
|
173
|
+
const batch = [...touches];
|
|
174
|
+
touches.clear();
|
|
175
|
+
if (database && batch.length)
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
try {
|
|
178
|
+
const transaction = database.transaction('images', 'readwrite');
|
|
179
|
+
cacheWriteDeadline(transaction, resolve);
|
|
180
|
+
const store = transaction.objectStore('images');
|
|
181
|
+
batch.forEach(([uri, accessed]) => {
|
|
182
|
+
const request = store.get(uri);
|
|
183
|
+
request.onsuccess = () => {
|
|
184
|
+
if (request.result)
|
|
185
|
+
store.put({
|
|
186
|
+
...request.result,
|
|
187
|
+
accessed: Math.max(request.result.accessed || 0, accessed),
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
resolve(false);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function jobPriority(job) {
|
|
198
|
+
let priority = 2;
|
|
199
|
+
job.ids.forEach((id) => {
|
|
200
|
+
priority = Math.min(priority, priorities.get(id) ?? 2);
|
|
201
|
+
});
|
|
202
|
+
return priority;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function openDatabase() {
|
|
206
|
+
if (!databasePromise) {
|
|
207
|
+
databasePromise = new Promise((resolve, reject) => {
|
|
208
|
+
const request = indexedDB.open(DATABASE, 1);
|
|
209
|
+
request.onupgradeneeded = () => {
|
|
210
|
+
const store = request.result.createObjectStore('images', {
|
|
211
|
+
keyPath: 'uri',
|
|
212
|
+
});
|
|
213
|
+
store.createIndex('accessed', 'accessed');
|
|
214
|
+
request.result.createObjectStore('metadata');
|
|
215
|
+
};
|
|
216
|
+
let blocked = false;
|
|
217
|
+
request.onsuccess = () => {
|
|
218
|
+
const database = request.result;
|
|
219
|
+
if (blocked) {
|
|
220
|
+
database.close();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
database.onversionchange = () => {
|
|
224
|
+
database.close();
|
|
225
|
+
databasePromise = undefined;
|
|
226
|
+
};
|
|
227
|
+
resolve(database);
|
|
228
|
+
};
|
|
229
|
+
request.onerror = () => reject(request.error);
|
|
230
|
+
request.onblocked = () => {
|
|
231
|
+
blocked = true;
|
|
232
|
+
reject(new Error('Avatar cache database is blocked'));
|
|
233
|
+
};
|
|
234
|
+
}).catch(() => undefined);
|
|
235
|
+
}
|
|
236
|
+
return databasePromise;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function readDisk(uri) {
|
|
240
|
+
// OneKey patch: the cache is optional. A slow read must not retain a display
|
|
241
|
+
// load slot while abort/commit waits on browser I/O. Keep unfinished reads
|
|
242
|
+
// separately bounded, including an unresolved shared database open.
|
|
243
|
+
if (pendingReads >= MAX_PENDING_READS) return undefined;
|
|
244
|
+
pendingReads += 1;
|
|
245
|
+
return new Promise((resolve) => {
|
|
246
|
+
let transaction;
|
|
247
|
+
let blob;
|
|
248
|
+
let settled = false;
|
|
249
|
+
let finished = false;
|
|
250
|
+
let abortRequested = false;
|
|
251
|
+
const finish = (success) => {
|
|
252
|
+
if (finished) return;
|
|
253
|
+
finished = true;
|
|
254
|
+
pendingReads -= 1;
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
if (!settled) {
|
|
257
|
+
settled = true;
|
|
258
|
+
if (success && blob) touchLater(uri);
|
|
259
|
+
resolve(success ? blob : undefined);
|
|
260
|
+
}
|
|
261
|
+
scheduleDiskWork();
|
|
262
|
+
};
|
|
263
|
+
const timeout = () => {
|
|
264
|
+
if (finished) return;
|
|
265
|
+
if (!settled) {
|
|
266
|
+
settled = true;
|
|
267
|
+
resolve(undefined);
|
|
268
|
+
}
|
|
269
|
+
// Do not release pendingReads here: an aborted IDB transaction can still
|
|
270
|
+
// hold its store lock for hundreds of milliseconds before onabort arrives.
|
|
271
|
+
if (transaction && !abortRequested) {
|
|
272
|
+
abortRequested = true;
|
|
273
|
+
try {
|
|
274
|
+
transaction.abort();
|
|
275
|
+
} catch {
|
|
276
|
+
/* Already committing; ignore late callbacks. */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const timer = setTimeout(timeout, READ_BUDGET_MS);
|
|
281
|
+
Promise.resolve()
|
|
282
|
+
.then(openDatabase)
|
|
283
|
+
.then((database) => {
|
|
284
|
+
if (settled || !database) {
|
|
285
|
+
finish(false);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
transaction = database.transaction('images', 'readonly');
|
|
290
|
+
transaction.oncomplete = () => finish(true);
|
|
291
|
+
transaction.onabort = () => finish(false);
|
|
292
|
+
transaction.onerror = timeout;
|
|
293
|
+
const request = transaction.objectStore('images').get(uri);
|
|
294
|
+
request.onsuccess = () => {
|
|
295
|
+
if (settled) return;
|
|
296
|
+
const record = request.result;
|
|
297
|
+
if (
|
|
298
|
+
record?.blob instanceof Blob &&
|
|
299
|
+
record.blob.type === 'image/png' &&
|
|
300
|
+
record.blob.size <= MAX_DISK_BYTES
|
|
301
|
+
)
|
|
302
|
+
blob = record.blob;
|
|
303
|
+
// A completed readonly get can serve display before the transaction ends.
|
|
304
|
+
// Its physical permit still belongs to oncomplete/onabort.
|
|
305
|
+
settled = true;
|
|
306
|
+
if (blob) touchLater(uri);
|
|
307
|
+
resolve(blob);
|
|
308
|
+
};
|
|
309
|
+
} catch {
|
|
310
|
+
if (transaction) timeout();
|
|
311
|
+
else finish(false);
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
.catch(() => finish(false));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function writeDisk(uri, blob) {
|
|
319
|
+
const database = await openDatabase();
|
|
320
|
+
if (!database || blob.size > MAX_DISK_BYTES) return false;
|
|
321
|
+
if (!diskIsIdle()) return 'deferred';
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
try {
|
|
324
|
+
const transaction = database.transaction(
|
|
325
|
+
['images', 'metadata'],
|
|
326
|
+
'readwrite'
|
|
327
|
+
);
|
|
328
|
+
cacheWriteDeadline(transaction, resolve);
|
|
329
|
+
const store = transaction.objectStore('images');
|
|
330
|
+
const metadata = transaction.objectStore('metadata');
|
|
331
|
+
const previous = store.get(uri);
|
|
332
|
+
previous.onsuccess = () => {
|
|
333
|
+
const counter = metadata.get('size');
|
|
334
|
+
counter.onsuccess = () => {
|
|
335
|
+
const size = counter.result || { bytes: 0, count: 0 };
|
|
336
|
+
size.bytes += blob.size - (previous.result?.blob?.size || 0);
|
|
337
|
+
size.count += previous.result ? 0 : 1;
|
|
338
|
+
store.put({ uri, blob, accessed: Date.now() });
|
|
339
|
+
const save = () => metadata.put(size, 'size');
|
|
340
|
+
if (size.bytes <= MAX_DISK_BYTES && size.count <= MAX_DISK_ENTRIES) {
|
|
341
|
+
save();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const cursor = store.index('accessed').openCursor();
|
|
345
|
+
cursor.onsuccess = () => {
|
|
346
|
+
const item = cursor.result;
|
|
347
|
+
if (
|
|
348
|
+
!item ||
|
|
349
|
+
(size.bytes <= MAX_DISK_BYTES && size.count <= MAX_DISK_ENTRIES)
|
|
350
|
+
) {
|
|
351
|
+
save();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (item.value.uri !== uri) {
|
|
355
|
+
size.bytes -= item.value.blob.size;
|
|
356
|
+
size.count -= 1;
|
|
357
|
+
item.delete();
|
|
358
|
+
}
|
|
359
|
+
item.continue();
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
// OneKey patch: completion/abort clears the deadline before releasing pending bytes.
|
|
364
|
+
// transaction.oncomplete = transaction.onerror = transaction.onabort = () => resolve();
|
|
365
|
+
} catch {
|
|
366
|
+
resolve();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// PRNG and HSL conversion adapted from ethereum-blockies-base64 1.0.2 (MIT), MyCrypto.
|
|
372
|
+
// https://github.com/MyCryptoHQ/ethereum-blockies-base64/blob/master/src/main.js
|
|
373
|
+
// https://github.com/MyCryptoHQ/ethereum-blockies-base64/blob/master/src/hsl2rgb.js
|
|
374
|
+
// Preserve signed shifts, color order, and RGB rounding to match V1's decoded pixels.
|
|
375
|
+
async function generateBlob(seed) {
|
|
376
|
+
const state = [0, 0, 0, 0];
|
|
377
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
378
|
+
state[i % 4] = (state[i % 4] << 5) - state[i % 4] + seed.charCodeAt(i);
|
|
379
|
+
}
|
|
380
|
+
const rand = () => {
|
|
381
|
+
const t = state[0] ^ (state[0] << 11);
|
|
382
|
+
state[0] = state[1];
|
|
383
|
+
state[1] = state[2];
|
|
384
|
+
state[2] = state[3];
|
|
385
|
+
state[3] = state[3] ^ (state[3] >> 19) ^ t ^ (t >> 8);
|
|
386
|
+
return (state[3] >>> 0) / ((1 << 31) >>> 0);
|
|
387
|
+
};
|
|
388
|
+
const hue = (p, q, value) => {
|
|
389
|
+
let t = value;
|
|
390
|
+
if (t < 0) t += 1;
|
|
391
|
+
if (t > 1) t -= 1;
|
|
392
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
393
|
+
if (t < 1 / 2) return q;
|
|
394
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
395
|
+
return p;
|
|
396
|
+
};
|
|
397
|
+
const color = () => {
|
|
398
|
+
const h = Math.floor(rand() * 360) / 360;
|
|
399
|
+
const s = (rand() * 60 + 40) / 100;
|
|
400
|
+
const l = ((rand() + rand() + rand() + rand()) * 25) / 100;
|
|
401
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
402
|
+
const p = 2 * l - q;
|
|
403
|
+
const rgb =
|
|
404
|
+
s === 0
|
|
405
|
+
? [l, l, l]
|
|
406
|
+
: [hue(p, q, h + 1 / 3), hue(p, q, h), hue(p, q, h - 1 / 3)];
|
|
407
|
+
return `rgb(${rgb.map((value) => Math.round(value * 255)).join(',')})`;
|
|
408
|
+
};
|
|
409
|
+
const foreground = color();
|
|
410
|
+
const background = color();
|
|
411
|
+
const spot = color();
|
|
412
|
+
const canvas = new OffscreenCanvas(128, 128);
|
|
413
|
+
const context = canvas.getContext('2d');
|
|
414
|
+
if (!context) throw new Error('Avatar canvas is unavailable');
|
|
415
|
+
context.fillStyle = background;
|
|
416
|
+
context.fillRect(0, 0, 128, 128);
|
|
417
|
+
for (let row = 0; row < 8; row += 1) {
|
|
418
|
+
for (let column = 0; column < 4; column += 1) {
|
|
419
|
+
const value = Math.floor(rand() * 2.3);
|
|
420
|
+
if (value === 0) continue;
|
|
421
|
+
context.fillStyle = value === 1 ? foreground : spot;
|
|
422
|
+
context.fillRect(column * 16, row * 16, 16, 16);
|
|
423
|
+
context.fillRect((7 - column) * 16, row * 16, 16, 16);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return canvas.convertToBlob({ type: 'image/png' });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function release(id) {
|
|
430
|
+
const uri = requests.get(id);
|
|
431
|
+
requests.delete(id);
|
|
432
|
+
priorities.delete(id);
|
|
433
|
+
const image = images.get(uri);
|
|
434
|
+
image?.references.delete(id);
|
|
435
|
+
if (image && image.references.size === 0) {
|
|
436
|
+
URL.revokeObjectURL(image.url);
|
|
437
|
+
images.delete(uri);
|
|
438
|
+
}
|
|
439
|
+
const job = jobs.get(uri);
|
|
440
|
+
job?.ids.delete(id);
|
|
441
|
+
if (job && job.ids.size === 0 && !job.active) {
|
|
442
|
+
jobs.delete(uri);
|
|
443
|
+
const index = queue.indexOf(job);
|
|
444
|
+
if (index !== -1) queue.splice(index, 1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function runJob(job) {
|
|
449
|
+
// OneKey patch: reuse generated bytes even if the last UI lease was released
|
|
450
|
+
// while persistence is still in flight; these bytes already passed generation.
|
|
451
|
+
const pending = pendingBlobs.get(job.uri);
|
|
452
|
+
let blob = pending ?? (await readDisk(job.uri));
|
|
453
|
+
let persisted = !!blob && !pending;
|
|
454
|
+
if (blob && !pending) {
|
|
455
|
+
try {
|
|
456
|
+
const bitmap = await createImageBitmap(blob);
|
|
457
|
+
const valid = bitmap.width === 128 && bitmap.height === 128;
|
|
458
|
+
bitmap.close();
|
|
459
|
+
if (!valid) blob = undefined;
|
|
460
|
+
} catch {
|
|
461
|
+
blob = undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!blob && job.ids.size) {
|
|
465
|
+
persisted = false;
|
|
466
|
+
blob = await generateBlob(job.seed);
|
|
467
|
+
// OneKey patch: notify clients and free the load slot without awaiting I/O.
|
|
468
|
+
// await writeDisk(job.uri, blob);
|
|
469
|
+
persistLater(job.uri, blob);
|
|
470
|
+
}
|
|
471
|
+
if (!blob || !job.ids.size) return;
|
|
472
|
+
const image = {
|
|
473
|
+
url: URL.createObjectURL(blob),
|
|
474
|
+
blob,
|
|
475
|
+
persisted,
|
|
476
|
+
references: new Set(),
|
|
477
|
+
};
|
|
478
|
+
images.set(job.uri, image);
|
|
479
|
+
job.ids.forEach((id) => {
|
|
480
|
+
if (requests.get(id) !== job.uri) return;
|
|
481
|
+
image.references.add(id);
|
|
482
|
+
postMessage({ type: 'resolved', id, url: image.url });
|
|
483
|
+
});
|
|
484
|
+
if (!image.references.size) {
|
|
485
|
+
URL.revokeObjectURL(image.url);
|
|
486
|
+
images.delete(job.uri);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function pump() {
|
|
491
|
+
while (activeLoads < MAX_CONCURRENT_LOADS && queue.length) {
|
|
492
|
+
// OneKey patch: queued leases can be promoted without restarting their load.
|
|
493
|
+
// const job = queue.shift();
|
|
494
|
+
let next = 0;
|
|
495
|
+
for (let index = 1; index < queue.length; index += 1) {
|
|
496
|
+
if (jobPriority(queue[index]) < jobPriority(queue[next])) next = index;
|
|
497
|
+
}
|
|
498
|
+
const [job] = queue.splice(next, 1);
|
|
499
|
+
if (jobs.get(job.uri) !== job || !job.ids.size) continue;
|
|
500
|
+
job.active = true;
|
|
501
|
+
activeLoads += 1;
|
|
502
|
+
runJob(job)
|
|
503
|
+
.catch(() => {
|
|
504
|
+
job.ids.forEach((id) => {
|
|
505
|
+
if (requests.get(id) === job.uri) postMessage({ type: 'error', id });
|
|
506
|
+
});
|
|
507
|
+
})
|
|
508
|
+
.finally(() => {
|
|
509
|
+
if (jobs.get(job.uri) === job) jobs.delete(job.uri);
|
|
510
|
+
activeLoads -= 1;
|
|
511
|
+
pump();
|
|
512
|
+
void drainDiskQueue();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
onmessage = ({ data }) => {
|
|
518
|
+
if (!data || !Number.isSafeInteger(data.id)) return;
|
|
519
|
+
if (data.type === 'release') {
|
|
520
|
+
release(data.id);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (data.type === 'priority') {
|
|
524
|
+
if (requests.has(data.id))
|
|
525
|
+
priorities.set(
|
|
526
|
+
data.id,
|
|
527
|
+
data.priority === 0 ? 0 : data.priority === 1 ? 1 : 2
|
|
528
|
+
);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (data.type !== 'acquire') return;
|
|
532
|
+
release(data.id);
|
|
533
|
+
lastAcquire = Date.now();
|
|
534
|
+
scheduleDiskWork();
|
|
535
|
+
try {
|
|
536
|
+
if (typeof data.uri !== 'string' || !data.uri.startsWith(PREFIX))
|
|
537
|
+
throw new Error('Invalid avatar URI');
|
|
538
|
+
const seed = decodeURIComponent(
|
|
539
|
+
data.uri.slice(PREFIX.length)
|
|
540
|
+
).toLowerCase();
|
|
541
|
+
if (!seed) throw new Error('Empty avatar seed');
|
|
542
|
+
const uri = PREFIX + encodeURIComponent(seed);
|
|
543
|
+
requests.set(data.id, uri);
|
|
544
|
+
priorities.set(
|
|
545
|
+
data.id,
|
|
546
|
+
data.priority === 0 ? 0 : data.priority === 1 ? 1 : 2
|
|
547
|
+
);
|
|
548
|
+
const image = images.get(uri);
|
|
549
|
+
if (image) {
|
|
550
|
+
// Reacquiring a live Blob can retry best-effort persistence after queue pressure.
|
|
551
|
+
if (!image.persisted) persistLater(uri, image.blob);
|
|
552
|
+
image.references.add(data.id);
|
|
553
|
+
postMessage({ type: 'resolved', id: data.id, url: image.url });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
let job = jobs.get(uri);
|
|
557
|
+
if (!job) {
|
|
558
|
+
job = { uri, seed, ids: new Set(), active: false };
|
|
559
|
+
jobs.set(uri, job);
|
|
560
|
+
queue.push(job);
|
|
561
|
+
}
|
|
562
|
+
job.ids.add(data.id);
|
|
563
|
+
pump();
|
|
564
|
+
} catch {
|
|
565
|
+
postMessage({ type: 'error', id: data.id });
|
|
566
|
+
}
|
|
567
|
+
};
|