Experimental new EEPROM API implementation.

This commit is contained in:
flash 2025-01-14 01:43:49 +00:00
parent 9a01d3a711
commit b9d58652b7
6 changed files with 282 additions and 8 deletions

36
src/mami.js/concurrent.js Normal file
View file

@ -0,0 +1,36 @@
const MamiRunConcurrent = (tasks, concurrency=4) => {
return new Promise((resolve, reject) => {
let index = 0;
let active = 0;
const results = [];
let rejected = false;
const next = () => {
if(rejected)
return;
if(index >= tasks.length && active < 1) {
resolve(results);
return;
}
while(active < concurrency && index < tasks.length) {
const current = index++;
++active;
Promise.resolve(tasks[current]())
.then(result => {
results[current] = result;
--active;
next();
})
.catch((...args) => {
rejected = true;
reject(...args);
});
}
};
next();
});
};

View file

@ -0,0 +1,162 @@
#include concurrent.js
#include hash.js
#include xhr.js
const MamiEEPROMv2 = function(baseUrl, getAuthLine, poolName) {
if(typeof baseUrl !== 'string')
throw 'baseUrl must be a string';
if(typeof getAuthLine !== 'function')
throw 'getAuthLine must be a function';
if(typeof poolName !== 'string')
throw 'poolName must be a string';
const CHUNK_SIZE = 4 * 1024 * 1024;
return {
create: fileInput => {
if(!(fileInput instanceof File))
throw 'fileInput must be an instance of window.File';
let userAborted = false;
let abortHandler;
let progressHandler;
const reportProgress = ev => {
if(progressHandler !== undefined)
progressHandler({
loaded: ev.loaded,
total: ev.total,
progress: ev.total <= 0 ? 0 : ev.loaded / ev.total,
});
};
const createTask = async (buffer, name, type, size) => {
const params = new URLSearchParams;
params.append('size', size);
params.append('type', type);
params.append('name', name);
params.append('hash', await MamiHash(buffer));
const { status, body } = await $x.post(`${baseUrl}/v1/storage/pools/${poolName}/uploads`, {
type: 'json',
headers: {
Authorization: getAuthLine(),
},
abort: handler => abortHandler = handler,
}, params);
if(body === null)
throw "Could not create upload task for some reason.";
if(status !== 200 && status !== 201)
throw body.english ?? body.error ?? `Upload failed with status code ${status}`;
return body;
};
const putChunk = async (taskInfo, buffer, index, size) => {
const offset = size * index;
buffer = buffer.subarray(offset, Math.min(buffer.length, offset + size));
let taskUrl = taskInfo.task_url;
if(offset > 0)
taskUrl += `?offset=${offset}`;
const { status, body } = await $x.put(taskUrl, { type: 'json' }, buffer);
if(status === 202)
return;
if(body === null)
throw `Upload of chunk #${offset} failed with status code ${status}`;
throw body.english ?? body.error ?? `Upload of chunk #${offset} failed with status code ${status}`;
};
const finishTask = async (taskInfo) => {
const { status, body } = await $x.post(`${baseUrl}/v1/storage/tasks/${taskInfo.task_id}`, { type: 'json' });
if(body === null)
throw "Could not complete upload task for some reason.";
if(status !== 201)
throw body.english ?? body.error ?? `Upload failed with status code ${status}`;
return body;
};
return {
abort: () => {
userAborted = true;
if(typeof abortHandler === 'function')
abortHandler();
},
onProgress: handler => {
if(typeof handler !== 'function')
throw 'handler must be a function';
progressHandler = handler;
},
start: async () => {
if(userAborted)
throw 'File upload was cancelled by the user, it cannot be restarted.';
try {
const buffer = new Uint8Array(await fileInput.arrayBuffer());
let upload;
const task = await createTask(buffer, fileInput.name, fileInput.type, fileInput.size);
if(task.state === 'complete') {
upload = task.upload;
} else if(task.state === 'pending') {
await MamiRunConcurrent((() => {
const promises = [];
for(let i = 0; i < buffer.length / CHUNK_SIZE; ++i)
promises.push(async () => await putChunk(task, buffer, i, CHUNK_SIZE));
return promises;
})());
const result = await finishTask(task);
if(result.state !== 'complete')
throw `Upload task finished with unsupported state: ${result.state}`;
upload = result.upload;
} else
throw `Upload task is in unsupported state: ${task.state}`;
upload.thumb = `${upload.url}.t`;
upload.isImage = () => upload.type.startsWith('image/');
upload.isVideo = () => upload.type.startsWith('video/');
upload.isAudio = () => upload.type.startsWith('audio/');
upload.isMedia = () => upload.isImage() || upload.isAudio() || upload.isVideo();
return Object.freeze(upload);
} catch(ex) {
if(userAborted)
throw '';
console.error(ex);
throw ex;
}
},
};
},
delete: async fileInfo => {
if(typeof fileInfo !== 'object')
throw 'fileInfo must be an object';
if(typeof fileInfo.id !== 'string')
throw 'fileInfo.id must be a string';
const { status, body } = await $x.delete(`${baseUrl}/v1/storage/uploads/${fileInfo.id}`, {
type: 'json',
headers: {
Authorization: getAuthLine(),
},
});
if(status === 204)
return;
if(body === null)
throw `Delete failed with status code ${status}`;
throw body.english ?? body.error ?? `Delete failed with status code ${status}`;
},
};
};

13
src/mami.js/hash.js Normal file
View file

@ -0,0 +1,13 @@
#include urib64.js
const MamiHash = (() => {
const textEncoder = new TextEncoder('utf-8');
return async (data, method='SHA-256', asString=true) => {
if(typeof data === 'string')
data = textEncoder.encode(data);
const hash = new Uint8Array(await crypto.subtle.digest(method, data));
return asString ? MamiUriBase64.encode(hash) : hash;
};
})();

View file

@ -25,6 +25,7 @@ window.Umi = { UI: {} };
#include controls/ping.jsx
#include controls/views.js
#include eeprom/eeprom.js
#include eeprom/eepromv2.js
#include emotes/picker.jsx
#include notices/baka.jsx
#include notices/youare.jsx
@ -151,6 +152,7 @@ const MamiInit = async args => {
settings.define('keepEmotePickerOpen').default(true).create();
settings.define('doNotMarkLinksAsVisited').default(false).create();
settings.define('showMarkupSelector').type(['always', 'focus', 'never']).default('always').create();
settings.define('dbgEEPROMv2').default(false).create();
const noNotifSupport = !('Notification' in window);
settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create();
@ -639,7 +641,8 @@ const MamiInit = async args => {
category.warning("Only touch these settings if you're ABSOLUTELY sure you know what you're doing, you're on your own if you break something.");
category.setting('dumpPackets').title('Dump packets to console').done();
category.setting('dumpEvents').title('Dump events to console').done();
category.setting('marqueeAllNames').title('Apply marquee on everyone').done();
category.setting('dbgEEPROMv2').title('Use EEPROM v2 API (requires reload)').done();
category.setting('marqueeAllNames').title('Apply marquee on everyone (requires reload)').done();
category.setting('dbgAnimDurationMulti').title('Animation multiplier').type('range').done();
category.button('Test kick/ban notice', async button => {
button.disabled = true;
@ -716,7 +719,9 @@ const MamiInit = async args => {
});
ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
ctx.eeprom = settings.get('dbgEEPROMv2')
? new MamiEEPROMv2(futami.get('api'), MamiMisuzuAuth.getLine, 'chat')
: new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
sbUploads.addOption({
name: 'view',
@ -732,14 +737,18 @@ const MamiInit = async args => {
const upload = entry.uploadInfo;
let text;
let url = upload.url;
if(upload.isImage()) {
text = `[img]${upload.url}[/img]`;
text = `[img]${url}[/img]`;
} else if(upload.isAudio()) {
text = `[audio]${upload.url}[/audio]`;
text = `[audio]${url}[/audio]`;
} else if(upload.isVideo()) {
text = `[video]${upload.url}[/video]`;
} else
text = location.protocol + upload.url;
text = `[video]${url}[/video]`;
} else {
if(url.startsWith('//'))
url = location.protocol + url;
text = url;
}
chatForm.input.insertAtCursor(text);
},

53
src/mami.js/urib64.js Normal file
View file

@ -0,0 +1,53 @@
const MamiUriBase64 = (() => {
const public = {};
const textEncoder = new TextEncoder('utf-8');
const textDecoder = new TextDecoder('utf-8');
if(typeof Uint8Array.prototype.toBase64 === 'function')
public.encode = data => {
if(typeof data === 'string')
data = textEncoder.encode(data);
else if(!(data instanceof Uint8Array))
throw 'data must be a string or a Uint8Array';
return data.toBase64({
alphabet: 'base64url',
omitPadding: true,
});
};
else // toBase64 is very new, compatible impl
public.encode = data => {
if(typeof data === 'string')
data = textEncoder.encode(data);
else if(!(data instanceof Uint8Array))
throw 'data must be a string or a Uint8Array';
return btoa(Array.from(data, byte => String.fromCodePoint(byte)).join(''))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};
if(typeof Uint8Array.fromBase64 === 'function')
public.decode = (data, asString=false) => {
if(typeof data !== 'string')
throw 'data must be string';
const buffer = Uint8Array.fromBase64(data, {
alphabet: 'base64url',
});
return asString ? textDecoder.decode(buffer) : buffer;
};
else // fromBase64 is very new, compatible impl
public.decode = (data, asString=false) => {
if(typeof data !== 'string')
throw 'data must be string';
const buffer = Uint8Array.from(atob(data), str => str.codePointAt(0));
return asString ? textDecoder.decode(buffer) : buffer;
};
return public;
})();

View file

@ -47,7 +47,8 @@ const $x = (function() {
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
} else if(body instanceof FormData) {
// content-type is implicitly set
} else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
} else if(body instanceof Blob || body instanceof ArrayBuffer
|| body instanceof DataView || body instanceof Uint8Array) {
if(!requestHeaders.has('content-type'))
requestHeaders.set('content-type', 'application/octet-stream');
} else if(!requestHeaders.has('content-type')) {