You want promises? I bring you commitments.

This commit is contained in:
flash 2025-04-12 23:33:57 +00:00
parent fcccccb6bc
commit ab95a803b4
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
9 changed files with 379 additions and 276 deletions

68
src/ami.js/commitment.js Normal file
View file

@ -0,0 +1,68 @@
const Commitment = function(run) {
if(typeof run !== 'function')
throw 'run must be a function';
let success;
let fail = ex => { console.error(ex); };
let always;
const pub = {
success(handler) {
if(typeof handler !== 'function')
throw 'success handler must be a function';
success = handler;
return pub;
},
fail(handler) {
if(typeof handler !== 'function')
throw 'fail handler must be a function';
fail = handler;
return pub;
},
always(handler) {
if(typeof handler !== 'function')
throw 'always handler must be a function';
always = handler;
return pub;
},
};
const successHandler = (...args) => {
try {
if(typeof success === 'function')
success(...args);
} catch(ex) {
fail(ex);
} finally {
if(typeof always === 'function')
always();
}
};
const failHandler = (...args) => {
try {
fail(...args);
} catch(ex) {
fail(ex);
} finally {
if(typeof always === 'function')
always();
}
};
pub.run = () => {
try {
run(successHandler, failHandler);
} catch(ex) {
fail(ex);
} finally {
if(typeof always === 'function')
always();
}
};
return Object.freeze(pub);
};

View file

@ -1,3 +1,6 @@
#include commitment.js
#include xhr.js
var FutamiCommon = function(vars) {
vars = vars || {};
@ -7,57 +10,41 @@ var FutamiCommon = function(vars) {
return {
get: get,
getJson: function(name, onload, onerror, noCache) {
if(typeof onload !== 'function')
throw 'onload must be specified';
getJson: function(name, noCache) {
return new Commitment((success, fail) => {
const options = { type: 'json' };
if(noCache)
options.headers = { 'Cache-Control': 'no-cache' };
var xhr = new XMLHttpRequest;
xhr.onload = function() {
onload(JSON.parse(xhr.responseText));
};
if(typeof onerror === 'function')
xhr.onerror = function() { onerror(); };
xhr.open('GET', get(name));
if(noCache)
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.send();
$xhr.get(get(name), options)
.success(({ body }) => { success(body); })
.fail(fail)
.run();
});
},
getApiJson: function(path, onload, onerror, noCache) {
if(typeof onload !== 'function')
throw 'onload must be specified';
getApiJson: function(path, noCache) {
return new Commitment((success, fail) => {
const options = { type: 'json' };
if(noCache)
options.headers = { 'Cache-Control': 'no-cache' };
var xhr = new XMLHttpRequest;
xhr.onload = function() {
onload(JSON.parse(xhr.responseText));
};
if(typeof onerror === 'function')
xhr.onerror = function() { onerror(); };
xhr.open('GET', get('api') + path);
if(noCache)
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.send();
$xhr.get(get('api') + path, options)
.success(({ body }) => { success(body); })
.fail(fail)
.run();
});
},
};
};
FutamiCommon.load = function(callback, url) {
if(typeof callback !== 'function')
throw 'there must be a callback';
if(typeof url !== 'string' && 'FUTAMI_URL' in window)
url = window.FUTAMI_URL + '?t=' + Date.now().toString();
FutamiCommon.load = function(url) {
return new Commitment((success, fail) => {
if(typeof url !== 'string' && 'FUTAMI_URL' in window)
url = window.FUTAMI_URL + '?t=' + Date.now().toString();
var xhr = new XMLHttpRequest;
xhr.onload = function() {
try {
callback(new FutamiCommon(JSON.parse(xhr.responseText)));
} catch(ex) {
callback(false);
}
};
xhr.onerror = function() {
callback(false);
};
xhr.open('GET', url);
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.send();
$xhr.get(url, { type: 'json', headers: { 'Cache-Control': 'no-cache' } })
.success(({ body }) => { success(new FutamiCommon(body)); })
.fail(fail)
.run();
});
};

View file

@ -122,9 +122,9 @@ var AmiContext = function(title, auth, loading) {
pub.textTriggers = textTriggers;
settings.watch('runJokeTriggers', function(value) {
if(!textTriggers.hasTriggers())
futami.getJson('texttriggers', function(trigInfos) {
textTriggers.addTriggers(trigInfos);
});
futami.getJson('texttriggers')
.success(textTriggers.addTriggers)
.run();
});
var styles = new AmiStyles;
@ -187,11 +187,11 @@ var AmiContext = function(title, auth, loading) {
return;
}
futami.getApiJson('/v1/emotes', function(emotes) {
futami.getApiJson('/v1/emotes').success(emotes => {
if(Array.isArray(emotes))
emoticons.load(emotes);
chat.emoticonList.render(emoticons);
});
}).run();
});
var reconnecter = new AmiReconnecter(chat);

View file

@ -1,3 +1,6 @@
#include commitment.js
#include xhr.js
var AmiEEPROM = function(endPoint, getAuthLine) {
if(typeof endPoint !== 'string')
throw 'endPoint must be a string';
@ -14,16 +17,6 @@ var AmiEEPROM = function(endPoint, getAuthLine) {
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,
});
};
return {
abort: () => {
@ -31,133 +24,75 @@ var AmiEEPROM = function(endPoint, getAuthLine) {
if(typeof abortHandler === 'function')
abortHandler();
},
onProgress: handler => {
if(typeof handler !== 'function')
throw 'handler must be a function';
progressHandler = handler;
},
start: (success, error) => {
const throwError = ex => {
if(typeof error === 'function')
error(ex);
else
console.error(ex);
};
try {
if(typeof success !== 'function')
throw 'success must be a callback function';
start: progress => {
return new Commitment((success, fail) => {
if(userAborted)
throw 'File upload was cancelled by the user, it cannot be restarted.';
try {
const formData = new FormData;
formData.append('src', appId);
formData.append('file', fileInput);
const xhr = new XMLHttpRequest;
abortHandler = () => { xhr.abort(); };
xhr.upload.onloadstart = reportProgress;
xhr.upload.onprogress = reportProgress;
xhr.upload.onloadend = reportProgress;
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== XMLHttpRequest.DONE)
return;
try {
const body = (() => {
try {
return JSON.parse(xhr.responseText);
} catch(ex) {
return null;
}
})();
// user cancel
if(xhr.status === 0)
throw '';
if(body === null)
throw "The upload server didn't return the metadata for some reason.";
if(xhr.status !== 201)
throw body.english ?? body.error ?? `Upload failed with status code ${xhr.status}`;
body.isImage = () => body.type.indexOf('image/') === 0;
body.isVideo = () => body.type.indexOf('video/') === 0;
body.isAudio = () => body.type.indexOf('audio/') === 0;
body.isMedia = () => body.isImage() || body.isAudio() || body.isVideo();
success(Object.freeze(body));
} catch(ex) {
throwError(ex);
}
const reportProgress = typeof progress !== 'function' ? undefined : ev => {
progress({
loaded: ev.loaded,
total: ev.total,
progress: ev.total <= 0 ? 0 : ev.loaded / ev.total,
});
xhr.open('POST', `${endPoint}/uploads`);
xhr.setRequestHeader('Authorization', getAuthLine());
xhr.send(formData);
} catch(ex) {
if(userAborted)
throw '';
};
console.error(ex);
throw ex;
}
} catch(ex) {
throwError(ex);
}
const formData = new FormData;
formData.append('src', appId);
formData.append('file', fileInput);
$xhr.post(`${endPoint}/uploads`, {
authed: true,
type: 'json',
upload: reportProgress,
abort: handler => abortHandler = handler
}, formData).fail(fail).success(({ status, body }) => {
try {
if(body === null)
throw "The upload server didn't return the metadata for some reason.";
if(status !== 201)
throw body.english ?? body.error ?? `Upload failed with status code ${status}`;
body.isImage = () => body.type.indexOf('image/') === 0;
body.isVideo = () => body.type.indexOf('video/') === 0;
body.isAudio = () => body.type.indexOf('audio/') === 0;
body.isMedia = () => body.isImage() || body.isAudio() || body.isVideo();
success(Object.freeze(body));
} catch(ex) {
if(userAborted)
throw '';
throw ex;
}
}).run();
});
},
};
},
delete: (fileInfo, success, error) => {
const throwError = ex => {
if(typeof error === 'function')
error(ex);
else
console.error(ex);
};
try {
delete: fileInfo => {
return new Commitment((success, fail) => {
if(typeof fileInfo !== 'object')
throw 'fileInfo must be an object';
if(typeof fileInfo.urlf !== 'string')
throw 'fileInfo.urlf must be a string';
const xhr = new XMLHttpRequest;
xhr.addEventListener('readystatechange', () => {
if(xhr.readyState !== XMLHttpRequest.DONE)
return;
try {
const body = (() => {
try {
return JSON.parse(xhr.responseText);
} catch(ex) {
return null;
}
})();
if(xhr.status !== 204) {
$xhr.delete(fileInfo.urlf, { authed: true, type: 'json' })
.success(({ status, body }) => {
if(status !== 204) {
if(body === null)
throw `Delete failed with status code ${xhr.status}`;
throw `Delete failed with status code ${status}`;
throw body.english ?? body.error ?? `Delete failed with status code ${xhr.status}`;
throw body.english ?? body.error ?? `Delete failed with status code ${status}`;
}
if(typeof success === 'function')
success(body);
} catch(ex) {
throwError(ex);
}
});
xhr.open('DELETE', fileInfo.urlf);
xhr.setRequestHeader('Authorization', getAuthLine());
xhr.send();
} catch(ex) {
throwError(ex);
}
success();
})
.fail(fail)
.run();
});
},
};
};

View file

@ -13,36 +13,37 @@
(function() {
var loading = new AmiLoadingOverlay(document.body, true);
FutamiCommon.load(function(futami) {
if(typeof futami !== 'object') {
alert('Failed to load environment settings!');
return;
}
FutamiCommon.load()
.success(futami => {
window.futami = futami;
window.futami = futami;
var auth = new AmiMisuzuAuth(futami.get('token'));
var refreshInfo = function(next) {
auth.refresh().success(token => {
if(token.ok === false) {
location.assign(futami.get('login') + '?legacy=1');
return;
}
var auth = new AmiMisuzuAuth(futami.get('token'));
var refreshInfo = function(next) {
auth.refresh(function(token) {
if(token.ok === false) {
location.assign(futami.get('login') + '?legacy=1');
return;
}
if(typeof next === 'function')
next(token.ok);
}).run();
};
if(typeof next === 'function')
next(token.ok);
var ami = new AmiContext(futami.get('title'), auth, loading);
window.ami = ami;
setInterval(refreshInfo, 600000);
refreshInfo(function() {
Chat.Main(auth);
ami.sockChat.open();
window.addEventListener('beforeunload', () => ami.sockChat.close());
});
};
var ami = new AmiContext(futami.get('title'), auth, loading);
window.ami = ami;
setInterval(refreshInfo, 600000);
refreshInfo(function() {
Chat.Main(auth);
ami.sockChat.open();
window.addEventListener('beforeunload', () => ami.sockChat.close());
});
});
})
.fail(ex => {
console.log(ex);
alert('Failed to load environment settings!');
})
.run();
})();

View file

@ -1,39 +1,36 @@
#include commitment.js
#include xhr.js
var AmiMisuzuAuth = function(refreshUrl) {
var userId = undefined,
authToken = undefined;
let userId = null;
let authMethod = 'Misuzu';
let authToken = null;
return {
getUserId: function() { return userId; },
getAuthToken: function() { return authToken; },
getInfo: function() {
hasInfo: () => userId !== null && authToken !== null,
getUserId: () => userId,
getAuthToken: () => authToken,
getLine: () => `${authMethod} ${authToken}`,
getInfo: () => {
return {
method: 'Misuzu',
method: authMethod,
token: authToken,
};
},
getHttpAuth: function() {
return 'Misuzu ' + authToken;
},
refresh: function(callback) {
var xhr = new XMLHttpRequest;
xhr.onload = function() {
if(xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
if(data.ok === true) {
userId = data.usr.toString();
authToken = data.tkn;
}
refresh: function() {
return new Commitment((success, fail) => {
$xhr.get(refreshUrl, { authed: true, type: 'json' })
.success(({ body }) => {
if(body.ok) {
userId = body.usr.toString();
authToken = body.tkn;
}
callback(data);
} else
callback({ok: null});
};
xhr.onerror = function() {
callback({ok: null});
};
xhr.open('GET', refreshUrl);
xhr.withCredentials = true;
xhr.send();
success(body);
})
.fail(fail)
.run();
});
},
};
};

View file

@ -451,40 +451,41 @@ var Chat = (function () {
ami.sound.clearPacks();
ami.sound.clearSoundLibrary();
futami.getJson('sounds2', function(sounds) {
if(typeof sounds !== 'object')
return;
futami.getJson('sounds2')
.success(function(sounds) {
if(typeof sounds !== 'object')
return;
if(Array.isArray(sounds.library))
for(var i in sounds.library)
ami.sound.registerLibrarySound(sounds.library[i]);
if(Array.isArray(sounds.library))
for(var i in sounds.library)
ami.sound.registerLibrarySound(sounds.library[i]);
if(Array.isArray(sounds.packs)) {
for(var i in sounds.packs)
ami.sound.registerSoundPack(sounds.packs[i]);
if(Array.isArray(sounds.packs)) {
for(var i in sounds.packs)
ami.sound.registerSoundPack(sounds.packs[i]);
if(sounds.packs.length > 0 && !ami.settings.has('soundPack'))
ami.settings.set('soundPack', sounds.packs[0].name);
}
if(sounds.packs.length > 0 && !ami.settings.has('soundPack'))
ami.settings.set('soundPack', sounds.packs[0].name);
}
var soundPackSetting = $id('chatSetting-spack');
if(soundPackSetting) {
while(soundPackSetting.options.length > 0)
soundPackSetting.options.remove(0);
var soundPackSetting = $id('chatSetting-spack');
if(soundPackSetting) {
while(soundPackSetting.options.length > 0)
soundPackSetting.options.remove(0);
ami.sound.forEachPack(function(pack) {
var opt = document.createElement('option');
opt.value = pack.getName();
opt.innerHTML = pack.getTitle();
soundPackSetting.appendChild(opt);
ami.sound.forEachPack(function(pack) {
var opt = document.createElement('option');
opt.value = pack.getName();
opt.innerHTML = pack.getTitle();
soundPackSetting.appendChild(opt);
if(ami.settings.get('soundPack') === '')
ami.settings.set('soundPack', pack.getName());
});
if(ami.settings.get('soundPack') === '')
ami.settings.set('soundPack', pack.getName());
});
soundPackSetting.value = ami.settings.get('soundPack');
}
});
soundPackSetting.value = ami.settings.get('soundPack');
}
}).run();
}
ami.sound.setMuted(value);

120
src/ami.js/xhr.js Normal file
View file

@ -0,0 +1,120 @@
#include commitment.js
const $xhr = (function() {
const send = function(method, url, options, body) {
if(options === undefined)
options = {};
else if(typeof options !== 'object')
throw 'options must be undefined or an object';
Object.freeze(options);
const xhr = new XMLHttpRequest;
const requestHeaders = new Map;
if('headers' in options && typeof options.headers === 'object')
for(const name in options.headers)
if(options.headers.hasOwnProperty(name))
requestHeaders.set(name.toLowerCase(), options.headers[name]);
if(typeof options.download === 'function') {
xhr.onloadstart = ev => options.download(ev);
xhr.onprogress = ev => options.download(ev);
xhr.onloadend = ev => options.download(ev);
}
if(typeof options.upload === 'function') {
xhr.upload.onloadstart = ev => options.upload(ev);
xhr.upload.onprogress = ev => options.upload(ev);
xhr.upload.onloadend = ev => options.upload(ev);
}
if(options.authed)
xhr.withCredentials = true;
if(typeof options.timeout === 'number')
xhr.timeout = options.timeout;
if(typeof options.abort === 'function')
options.abort(() => xhr.abort());
if(typeof options.xhr === 'function')
options.xhr(() => xhr);
if(typeof body === 'object') {
if(body instanceof FormData) {
// content-type is implicitly set
} 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')) {
const bodyParts = [];
for(const name in body)
if(body.hasOwnProperty(name))
bodyParts.push(encodeURIComponent(name) + '=' + encodeURIComponent(body[name]));
body = bodyParts.join('&');
requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
}
}
return new Commitment((success, fail) => {
xhr.onload = ev => {
const headers = (headersString => {
const headers = new Map;
const raw = headersString.trim().split(/[\r\n]+/);
for(const name in raw)
if(raw.hasOwnProperty(name)) {
const parts = raw[name].split(': ');
headers.set(parts.shift(), parts.join(': '));
}
return headers;
})(xhr.getAllResponseHeaders());
success({
get ev() { return ev; },
get xhr() { return xhr; },
get status() { return xhr.status; },
get headers() { return headers; },
get body() { return xhr.response; },
get text() { return xhr.responseText; },
});
};
xhr.onabort = ev => fail({
abort: true,
xhr: xhr,
ev: ev,
});
xhr.onerror = ev => fail({
abort: false,
xhr: xhr,
ev: ev,
});
xhr.open(method, url);
if(typeof options.type === 'string')
xhr.responseType = options.type;
requestHeaders.forEach((value, name) => {
xhr.setRequestHeader(name, requestHeaders[name]);
});
xhr.send(body);
});
};
return {
send: send,
get: (url, options, body) => send('GET', url, options, body),
post: (url, options, body) => send('POST', url, options, body),
delete: (url, options, body) => send('DELETE', url, options, body),
patch: (url, options, body) => send('PATCH', url, options, body),
put: (url, options, body) => send('PUT', url, options, body),
};
})();

View file

@ -1,13 +1,9 @@
#include common.js
#include eeprom.js
var eepromAppl = 1,
eepromSupportImageEmbed = true,
eepromSupportAudioEmbed = true,
eepromSupportVideoEmbed = true;
var eepromInitialise = function(auth) {
window.eepromHistory = {};
window.eepromClient = new AmiEEPROM(futami.get('eeprom2'), auth.getHttpAuth);
window.eepromClient = new AmiEEPROM(futami.get('eeprom2'), auth.getLine);
window.eepromUploadsTable = $id('uploadHistory');
$id('uploadSelector').onchange = function() {
@ -88,27 +84,25 @@ var eepromFileUpload = function(file) {
var uploadTask = window.eepromClient.create(file);
uploadTask.onProgress(function(progressInfo) {
progBar.style.width = `${progressInfo.progress * 100}%`;
});
name.onclick = function(ev) {
if(ev.shiftKey) {
ev.preventDefault();
var fileInfo = window.eepromHistory[name.getAttribute('data-eeprom-id') || ''];
if(fileInfo) {
window.eepromClient.delete(
fileInfo,
() => { uploadHistory.deleteRow(uploadEntryWrap.rowIndex); },
window.alert
);
window.eepromClient.delete(fileInfo)
.success(() => { window.eepromUploadsTable.deleteRow(uploadEntryWrap.rowIndex); })
.fail(ex => {
console.error(ex);
window.alert(ex);
})
.run();
} else uploadTask.abort();
}
};
uploadTask.start(
fileInfo => {
uploadTask.start(progressInfo => { progBar.style.width = `${progressInfo.progress * 100}%`; })
.success(fileInfo => {
window.eepromHistory[fileInfo.id] = fileInfo;
name.style.textDecoration = null;
name.setAttribute('data-eeprom-id', fileInfo.id);
@ -117,19 +111,19 @@ var eepromFileUpload = function(file) {
var insertText = location.protocol + fileInfo.url;
if(eepromSupportImageEmbed && fileInfo.isImage())
if(fileInfo.isImage())
insertText = '[img]' + fileInfo.url + '[/img]';
else if(eepromSupportAudioEmbed && fileInfo.isAudio())
else if(fileInfo.isAudio())
insertText = '[audio]' + fileInfo.url + '[/audio]';
else if(eepromSupportVideoEmbed && fileInfo.isVideo())
else if(fileInfo.isVideo())
insertText = '[video]' + fileInfo.url + '[/video]';
ami.chat.inputBox.insert(insertText);
},
ex => {
})
.fail(ex => {
if(ex !== '')
alert(ex);
uploadHistory.deleteRow(uploadEntryWrap.rowIndex);
},
);
window.eepromUploadsTable.deleteRow(uploadEntryWrap.rowIndex);
})
.run();
};