Experimental new EEPROM API implementation.
This commit is contained in:
parent
9a01d3a711
commit
b9d58652b7
6 changed files with 282 additions and 8 deletions
src/mami.js
36
src/mami.js/concurrent.js
Normal file
36
src/mami.js/concurrent.js
Normal 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();
|
||||
});
|
||||
};
|
162
src/mami.js/eeprom/eepromv2.js
Normal file
162
src/mami.js/eeprom/eepromv2.js
Normal 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
13
src/mami.js/hash.js
Normal 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;
|
||||
};
|
||||
})();
|
|
@ -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
53
src/mami.js/urib64.js
Normal 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;
|
||||
})();
|
|
@ -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')) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue