socio
Version:
A WebSocket Real-Time Communication (RTC) API framework.
709 lines (708 loc) • 31.7 kB
JavaScript
import pako from 'pako';
import * as diff_lib from 'recursive-diff';
import { LogHandler, E } from './logging.js';
import { socio_decode, socio_encode, clamp, ServerMessageKind, ClientMessageKind, initLifecycleHooks } from './utils.js';
import { ErrorOrigin } from './logging.js';
export class SocioClient extends LogHandler {
#ws = null;
#client_id = '';
#latency = 0;
#ready_state = new ReadyState();
#authenticated = false;
#queries = new Map();
#props = new Map();
static #key = 1;
config;
key_generator;
lifecycle_hooks;
rpc_dict = {};
constructor({ url, name, logging = { verbose: false, hard_crash: false }, keep_alive = true, reconnect_tries = 1, persistent = false, hooks = {}, allow_rpc = false, } = {}) {
super({ ...logging, prefix: name ? `SocioClient:${name}` : 'SocioClient' });
this.config = { url, name, logging, keep_alive, reconnect_tries, persistent, allow_rpc };
this.lifecycle_hooks = { ...initLifecycleHooks(), ...hooks };
if (url) {
this.Connect();
}
}
Connect({ url = this.config?.url, keep_alive = this.config?.keep_alive || false, reconnect_tries = this.config?.reconnect_tries || 0 } = {}) {
if (!url)
throw new E('Must provide a WebSocket URL to connect to! [#no-url]');
if (this.#ws && this.#ws.readyState === WebSocket.OPEN)
throw new E('Socio WebSocket is already connected! Please disconnect first before connecting again or create a new instance. [#already-connected]');
this.#latency = (new Date()).getTime();
this.#connect(url, keep_alive, this.verbose || false, reconnect_tries);
if (this.verbose) {
if (window && url.startsWith('ws://'))
this.HandleInfo('WARNING, UNSECURE WEBSOCKET URL CONNECTION! Please use wss:// and https:// protocols in production to protect against man-in-the-middle attacks. You need to host an https server with bought SCTs - Signed Certificate Timestamps (keys) - from an authority.');
}
return this.ready();
}
async #connect(url, keep_alive, verbose, reconnect_tries) {
this.#ws = new WebSocket(url);
this.#ws.binaryType = 'arraybuffer';
this.#ws.addEventListener('message', this.#message.bind(this));
if (keep_alive && reconnect_tries) {
this.#ws.addEventListener("close", (event) => { this.#RetryConn(url, keep_alive, verbose, reconnect_tries, event); });
this.#ws.addEventListener("error", (event) => { this.#RetryConn(url, keep_alive, verbose, reconnect_tries, event); });
}
else {
const notify = (event) => {
if (event instanceof CloseEvent)
this.HandleInfo('Connection closed.');
else
this.#HandleClientError(new E(`Socio failed to connect [${url}]`, event));
if (this.lifecycle_hooks.discon)
this.lifecycle_hooks.discon(this, url, keep_alive, verbose, reconnect_tries, event);
};
this.#ws.addEventListener("close", notify);
this.#ws.addEventListener("error", notify);
}
}
#RetryConn(url, keep_alive, verbose, reconnect_tries, event) {
this.#HandleClientError(new E(`"${this.config.name || ''}" WebSocket closed. Retrying... Event details:`, event));
this.#resetConn();
this.#connect(url, keep_alive, verbose, reconnect_tries - 1);
if (this.lifecycle_hooks.discon)
this.lifecycle_hooks.discon(this, url, keep_alive, verbose, reconnect_tries - 1, event);
}
#resetConn() {
this.#client_id = '';
this.#ws = null;
this.#latency = Infinity;
this.#ready_state = new ReadyState();
this.#authenticated = false;
this.#queries.clear();
this.#props.clear();
}
async #message(event) {
try {
const { kind, data } = socio_decode(new Uint8Array(event.data));
this.HandleInfo('recv:', ClientMessageKind[kind], data);
if (typeof data.id === 'number' && typeof SocioClient.#key === 'number' && data.id > SocioClient.#key)
SocioClient.#key = data.id;
if (this.lifecycle_hooks.msg)
if (await this.lifecycle_hooks.msg(this, kind, data))
return;
switch (kind) {
case ClientMessageKind.CON: {
this.#client_id = data;
this.#latency = (new Date()).getTime() - this.#latency;
let successful_recon = false;
if (this.config.persistent) {
successful_recon = await this.#TryReconnect();
await this.#GetReconToken();
}
this.#ready_state.resolve(true);
if (this.verbose)
this.done(`Socio WebSocket [${this.config?.name || this.#client_id || 'NAME'}] connected.`);
if (this.config?.name && successful_recon !== true)
this.IdentifySelf(this.config.name);
break;
}
case ClientMessageKind.UPD: {
this.#FindID(kind, data.id);
const q = this.#queries.get(data.id);
if (data.result.success === 1 && q.onUpdate.success) {
q.onUpdate.success((data.result.res));
}
else {
if (q.onUpdate?.error) {
q.onUpdate.error(data.result.error);
}
else {
throw new E('Subscription query doesnt handle incoming server error:', data.result.error);
}
}
break;
}
case ClientMessageKind.PONG: {
this.#FindID(kind, data?.id);
this.HandleInfo('pong', data?.id);
break;
}
case ClientMessageKind.AUTH: {
this.#FindID(kind, data?.id);
if (data?.result?.success !== 1)
this.HandleInfo(`AUTH returned FALSE, which means websocket has not authenticated.`);
this.#authenticated = data.result.success === 1;
this.#queries.get(data.id).res(this.#authenticated);
this.#queries.delete(data.id);
break;
}
case ClientMessageKind.GET_PERM: {
this.#FindID(kind, data?.id);
if (data?.result?.success !== 1)
this.HandleInfo(`Server rejected grant perm for ${data?.verb} on ${data?.table}.`);
else {
const q = this.#queries.get(data.id);
q.res(data?.result.success === 1 ? true : false);
}
this.#queries.delete(data.id);
break;
}
case ClientMessageKind.RES: {
this.#HandleBasicPromiseMessage(kind, data);
break;
}
case ClientMessageKind.PROP_UPD: {
if (data.hasOwnProperty('prop') && data.hasOwnProperty('id') && (data.hasOwnProperty('prop_val') || data.hasOwnProperty('prop_val_diff'))) {
const prop = this.#props.get(data.prop);
let prop_val;
if (prop) {
if (data.hasOwnProperty('prop_val')) {
prop_val = data.prop_val;
}
else if (data.hasOwnProperty('prop_val_diff')) {
prop_val = diff_lib.applyDiff(prop.val, data.prop_val_diff);
}
else
throw new E('Prop upd data didnt have either factual val or diff val to use. [#prop-upd-no-val]', { kind, data });
prop.val = prop_val;
for (const callback of Object.values(prop.subs)) {
if (callback !== null) {
callback(prop.val, data?.prop_val_diff || undefined);
}
}
}
else {
throw new E('Prop not found by name. [#prop-name-not-found]', { data, prop_name: data.prop });
}
}
else
throw new E('Not enough prop info sent from server to perform prop update.', { data: data });
break;
}
case ClientMessageKind.PROP_DROP: {
if (data?.prop && data.hasOwnProperty('id')) {
if (this.#props.has(data.prop)) {
const prop = this.#props.get(data.prop);
if (prop) {
prop.subs = {};
this.#props.delete(data.prop);
}
if (this.lifecycle_hooks.prop_drop)
this.lifecycle_hooks.prop_drop(this, data.prop, data.id);
}
else
throw new E('Cant drop unsubbed prop!', data);
}
else
throw new E('Not enough prop info sent from server to perform prop drop.', data);
break;
}
case ClientMessageKind.CMD: {
if (this.lifecycle_hooks.cmd)
this.lifecycle_hooks.cmd(data);
break;
}
case ClientMessageKind.RECON: {
if (data?.id) {
this.#FindID(kind, data.id);
this.#queries.get(data.id).res(data);
this.#queries.delete(data.id);
}
else
this.#Reconnect(data);
break;
}
case ClientMessageKind.RECV_FILES: {
this.#FindID(kind, data?.id);
let resolve_with = null;
let error = null;
if (data.result.success === 1) {
if (data?.files) {
resolve_with = ParseSocioFiles(data.files);
}
else {
resolve_with = null;
error = 'Received 0 files. Something must\'ve gone wrong, bcs success was true. [#recv-0-files]';
}
}
else {
resolve_with = null;
error = 'File receive bad result. [#recv-files-bad]';
}
;
this.#queries.get(data.id)?.res(resolve_with);
this.#queries.delete(data.id);
if (error) {
const file_count = Object.keys(data?.files || {}).length;
this.#HandleServerError(error, data?.result?.error, 'files received: ' + file_count);
throw new E(error, { err_msg: data.result.error, file_count });
}
break;
}
case ClientMessageKind.TIMEOUT: {
if (this.lifecycle_hooks.timeout)
this.lifecycle_hooks.timeout(this);
break;
}
case ClientMessageKind.RPC: {
if (this.config.allow_rpc !== true) {
this.HandleDebug('Received RPC, but the client hasnt enabled it. [#rpc-client-not-enabled]', data);
return;
}
if (this.lifecycle_hooks.rpc) {
const res = await this.lifecycle_hooks.rpc(this, data.origin_client, data.f_name, data.args);
if (res !== undefined) {
this.Send(ServerMessageKind.OK, { id: data.id, return: res });
return;
}
}
let result = undefined;
if (data.f_name in this.rpc_dict)
result = await this.rpc_dict[data.f_name](data.origin_client, ...data.args);
else if (data.target_client === null && data.f_name in this)
result = await this[data.f_name](...data.args);
else
this.HandleDebug('Received RPC, but the function name doesnt exist on this client. [#rpc-client-no-function]', data);
this.Send(ServerMessageKind.OK, { id: data.id, return: result });
break;
}
default: {
const exhaustiveCheck = kind;
throw new E(`Unrecognized message kind!`, { kind, data });
}
}
}
catch (e) {
this.#HandleClientError(e);
}
}
Send(kind, ...data) {
try {
if (data.length < 1)
throw new E('Not enough arguments to send data! kind;data:', kind, ...data);
this.#ws?.send(socio_encode(Object.assign({}, { kind, data: data[0] }, ...data.slice(1))));
this.HandleInfo('sent:', ServerMessageKind[kind], ...data);
}
catch (e) {
this.#HandleClientError(e);
}
}
SendFiles(files, other_data = undefined) {
const { id, prom } = this.CreateQueryPromise();
(async () => {
const proc_files = new Map();
for (const file of files) {
const meta = {
lastModified: file.lastModified,
size: file.size,
type: file.type
};
const file_bytes_buffer = await file.arrayBuffer();
proc_files.set(file.name, { meta, bin: pako.deflate(file_bytes_buffer) });
}
const socio_form_data = { id, files: Object.fromEntries(proc_files) };
if (other_data)
socio_form_data['data'] = other_data;
this.Send(ServerMessageKind.UP_FILES, socio_form_data);
this.#UpdateQueryPromisePayloadSize(id);
})();
return prom;
}
SendBinary(blob) {
if (this.#queries.get('BLOB'))
throw new E('BLOB already being uploaded. Wait until the last query completes!');
const start_buff = this.#ws?.bufferedAmount || 0;
this.#ws?.send(blob);
this.HandleInfo('sent: BLOB');
const prom = new Promise((res) => {
this.#queries.set('BLOB', { res, prom, start_buff, payload_size: (this.#ws?.bufferedAmount || 0) - start_buff, full_meta: false });
});
return prom;
}
CreateQueryPromise({ full_meta = false } = {}) {
const id = this.GenKey;
const prom = new Promise((res) => {
this.#queries.set(id, { res, prom: null, start_buff: this.#ws?.bufferedAmount || 0, full_meta });
});
this.#queries.get(id).prom = prom;
return { id, prom };
}
#UpdateQueryPromisePayloadSize(query_id) {
if (!this.#queries.has(query_id))
return;
this.#queries.get(query_id).payload_size = (this.#ws?.bufferedAmount || 0) - this.#queries.get(query_id)?.start_buff || 0;
}
Serv(data) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.SERV, { id, data });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
GetFiles(data) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.GET_FILES, { id, data });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
Ping(id_num = undefined) {
this.Send(ServerMessageKind.PING, { id: typeof id_num === 'number' ? id_num : this.GenKey });
}
async DiscoverSessions(by = 'ID') {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.DISCOVERY, { id });
let clients = await prom;
if (by === 'NAME') {
return Object.fromEntries(Object.entries(clients).map(([id, meta]) => [
meta?.name ?? id,
{ ...meta, id },
]));
}
else if (by === 'AS_ARRAY') {
return Object.entries(clients).map(([id, meta]) => ({ ...meta, id }));
}
else {
return clients;
}
}
UnsubscribeAll({ props = true, queries = true, force = false } = {}) {
if (props)
for (const p of [...this.#props.keys()])
this.UnsubscribeProp(p, force);
if (queries)
for (const q of [...this.#queries.keys()])
this.Unsubscribe(q, force);
}
IdentifySelf(name) {
if (!name)
throw new E('Must provide a unique string name to indetify this session globally.');
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.IDENTIFY, { id, name });
return prom;
}
Subscribe({ sql = undefined, endpoint = undefined, params = null } = {}, onUpdate = null, status_callbacks = {}, rate_limit = null) {
if (sql && endpoint)
throw new E('Can only subscribe to either literal SQL query string or endpoint keyname, not both!');
if (typeof onUpdate !== "function")
throw new E('Subscription onUpdate is not function, but has to be.');
if (status_callbacks?.error && typeof status_callbacks.error !== "function")
throw new E('Subscription error is not function, but has to be.');
try {
const id = this.GenKey;
const callbacks = { success: onUpdate, ...status_callbacks };
this.#queries.set(id, { sql, endpoint, params, onUpdate: callbacks });
this.Send(ServerMessageKind.SUB, { id, sql, endpoint, params, rate_limit });
return id;
}
catch (e) {
this.#HandleClientError(e);
return null;
}
}
async Unsubscribe(sub_id, force = false) {
try {
if (this.#queries.has(sub_id)) {
if (force)
this.#queries.delete(sub_id);
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.UNSUB, { id, unreg_id: sub_id });
const res = await prom;
if (res === 1)
this.#queries.delete(sub_id);
return res;
}
else
throw new E('Cannot unsubscribe query, because provided ID is not currently tracked.', sub_id);
}
catch (e) {
this.#HandleClientError(e);
return false;
}
}
Query(sql, params = null, { sql_is_endpoint = undefined, onUpdate, freq_ms = undefined } = {}) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.SQL, { id, sql, params, sql_is_endpoint });
this.#UpdateQueryPromisePayloadSize(id);
if (onUpdate)
this.TrackProgressOfQueryID(id, onUpdate, freq_ms);
return prom;
}
async SetProp(prop_name, new_val, prop_upd_as_diff) {
try {
const { id, prom } = this.CreateQueryPromise({ full_meta: true });
this.Send(ServerMessageKind.PROP_SET, { id, prop: prop_name, prop_val: new_val, prop_upd_as_diff });
this.#UpdateQueryPromisePayloadSize(id);
const res = await prom;
if (res?.result?.success === 1) {
const prop = this.#props.get(prop_name);
if (prop) {
prop.val = new_val;
}
}
return res;
}
catch (e) {
this.#HandleClientError(e);
return null;
}
}
GetProp(prop_name, local = false) {
if (local)
return { result: { success: 1, res: this.#props.get(prop_name)?.val } };
else {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.PROP_GET, { id, prop: prop_name });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
}
SubscribeProp(prop_name, onUpdate, { rate_limit = null, receive_initial_update = true } = {}) {
if (typeof onUpdate !== "function")
throw new E('Subscription onUpdate is not function, but has to be.');
try {
const prop = this.#props.get(prop_name);
if (prop) {
const id = this.GenKey;
prop.subs[id] = onUpdate;
return Promise.resolve({ id, result: { success: 1, res: prop.val } });
}
else {
const { id, prom } = this.CreateQueryPromise();
this.#props.set(prop_name, { val: undefined, subs: { [id]: onUpdate } });
this.Send(ServerMessageKind.PROP_SUB, { id, prop: prop_name, rate_limit, data: { receive_initial_update } });
return prom;
}
}
catch (e) {
this.#HandleClientError(e);
return Promise.resolve({ result: { success: 0 } });
}
}
async UnsubscribeProp(prop_name, force = false) {
try {
if (this.#props.get(prop_name)) {
if (force)
this.#props.delete(prop_name);
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.PROP_UNSUB, { id, prop: prop_name });
const res = await prom;
if (res === 1)
this.#props.delete(prop_name);
return res;
}
else
throw new E('Cannot unsubscribe query, because provided prop_name is not currently tracked.', prop_name);
}
catch (e) {
this.#HandleClientError(e);
return false;
}
}
RegisterProp(prop_name, initial_value = null, prop_reg_opts = {}) {
try {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.PROP_REG, { id, prop: prop_name, initial_value, opts: prop_reg_opts });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
catch (e) {
this.#HandleClientError(e);
return null;
}
}
async Prop(prop_name, { prop_sub_opts = {}, prop_upd_as_diff = false } = {}) {
const prop = await this.GetProp(prop_name, false);
if (prop === undefined) {
this.#HandleClientError(new E(`Couldnt retrieve server prop [${prop_name}]`, { prop_name, prop }));
return undefined;
}
if (typeof prop !== 'object') {
this.#HandleClientError(new E(`Can only proxy js objects, but [${prop_name}] is not an object.`, { prop_name, prop }));
return undefined;
}
let from_sub = false;
const LocalSetProp = this.SetProp.bind(this);
const prop_proxy = new Proxy(prop, {
get(p, property) {
return p[property];
},
set(p, property, new_val) {
p[property] = new_val;
if (from_sub !== true)
LocalSetProp(prop_name, p, prop_upd_as_diff);
return true;
}
});
await this.SubscribeProp(prop_name, (new_val) => {
from_sub = true;
for (const [key, val] of Object.entries(new_val))
prop_proxy[key] = val;
from_sub = false;
}, prop_sub_opts);
return prop_proxy;
}
Authenticate(params = {}) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.AUTH, { id, params });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
get authenticated() { return this.#authenticated === true; }
AskPermission(verb = '', table = '') {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.GET_PERM, { id, verb: verb, table: table });
this.#UpdateQueryPromisePayloadSize(id);
return prom;
}
async RPC(target_client, f_name, ...args) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.RPC, { ...{ target_client, origin_client: this.client_id, f_name, args }, id });
return await prom;
}
#FindID(kind, id) {
if (!this.#queries.has(id))
throw new E(`A received socio message [querry_id ${id}, ${ClientMessageKind[kind]}] is not currently in tracked queries!`);
}
#HandleBasicPromiseMessage(kind, data) {
this.#FindID(kind, data?.id);
const q = this.#queries.get(data.id);
if (q.hasOwnProperty('res')) {
q.res(q?.full_meta ? data : data?.result?.res);
if (data.result.success !== 1) {
this.#HandleServerError(data.result?.error);
}
}
else if (q.hasOwnProperty('onUpdate'))
if (data.result.success === 1) {
if (q.onUpdate?.success)
q.onUpdate.success(data.result.res);
}
else {
if (q.onUpdate?.error)
q.onUpdate.error(data.result.error);
else
this.#HandleServerError(``, data.result?.error);
}
this.#queries.delete(data.id);
}
#HandleServerError(...error_msgs) {
if (this.lifecycle_hooks.server_error)
this.lifecycle_hooks.server_error(this, error_msgs);
else
this.HandleError(new E(...error_msgs), ErrorOrigin.SERVER);
}
#HandleClientError(...error_msgs) {
this.HandleError(new E(...error_msgs), ErrorOrigin.CLIENT);
}
get GenKey() { return this?.key_generator ? this.key_generator() : ++SocioClient.#key; }
get client_id() { return this.#client_id; }
get web_socket() { return this.#ws; }
get client_address_info() { return { url: this.#ws?.url, protocol: this.#ws?.protocol, extensions: this.#ws?.extensions }; }
get latency() { return this.#latency; }
ready() { return this.#ready_state.promise; }
Close() { this.#ws?.close(); }
get is_ready() { return this.#ready_state.is_ready === true; }
async #GetReconToken(name = this.config.name) {
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.RECON, { id, type: 'GET' });
const token = await prom;
localStorage.setItem(`Socio_recon_token_${name}`, token);
}
RefreshReconToken(name = this.config.name) { return this.#GetReconToken(name); }
async #TryReconnect(name = this.config.name) {
const key = `Socio_recon_token_${name}`;
const token = localStorage.getItem(key);
if (token) {
localStorage.removeItem(key);
const { id, prom } = this.CreateQueryPromise();
this.Send(ServerMessageKind.RECON, { id, type: 'USE', token });
const res = await prom;
this.#Reconnect(res);
return res.result.success === 1;
}
else
return false;
}
#Reconnect(data) {
if (data.result.success === 1) {
this.#authenticated = data.auth;
this.config.name = data.name;
this.done(`${this.config.name} reconnected successfully. ${data.old_client_id} -> ${this.#client_id} (old client ID -> new/current client ID)`, data);
}
else {
const error = 'Failed to reconnect';
this.#HandleClientError(new E(error, data));
this.#HandleServerError(error, data.result?.error);
}
}
LogMaps() {
this.debug('queries', [...this.#queries.entries()]);
this.debug('props', [...this.#props.entries()]);
}
TrackProgressOfQueryPromise(prom, onUpdate, freq_ms = 33.34) {
for (const [id, q] of this.#queries) {
if (q?.prom == prom) {
return this.#CreateProgTrackingTimer(id, q.start_buff, q.payload_size || 0, onUpdate, freq_ms);
}
}
return null;
}
TrackProgressOfQueryID(query_id, onUpdate, freq_ms = 33.34) {
const q = this.#queries.get(query_id);
if (q)
return this.#CreateProgTrackingTimer(query_id, q.start_buff, q.payload_size || 0, onUpdate, freq_ms);
else
return null;
}
#CreateProgTrackingTimer(query_id, start_buff, payload_size, onUpdate, freq_ms = 33.34) {
let last_buff_size = this.#ws?.bufferedAmount || 0;
const intervalID = setInterval(() => {
if (!payload_size) {
payload_size = this.#queries.get(query_id)?.payload_size || 0;
if (!payload_size)
return;
last_buff_size = this.#ws?.bufferedAmount || 0;
}
const later_payload_ids = Array.from(this.#queries.keys()).filter(id => id > query_id);
const later_payloads_size = later_payload_ids.map(p_id => this.#queries.get(p_id)?.payload_size || 0).reduce((sum, payload) => sum += payload, 0);
const now_buff_size = (this.#ws?.bufferedAmount || 0) - later_payloads_size;
const delta_buff = (last_buff_size - now_buff_size) || 1_000;
last_buff_size = now_buff_size;
start_buff -= delta_buff;
const p = (start_buff * -100) / payload_size;
onUpdate(clamp(p, 0, 100));
if (p >= 100 || (this.#ws?.bufferedAmount || 0) === 0) {
onUpdate(100);
clearInterval(intervalID);
}
}, freq_ms);
return intervalID;
}
}
export function ParseSocioFiles(files) {
if (!files)
return [];
const files_array = [];
const entries = files instanceof Map ? files.entries() : Object.entries(files);
for (const [filename, file_data] of entries)
files_array.push(new File([pako.inflate(file_data.bin)], filename, { type: file_data.meta.type, lastModified: file_data.meta.lastModified }));
return files_array;
}
export function SocioFileBase64ToUint8Array(base64 = '') {
return pako.inflate(Uint8Array.from(window.atob(base64), (v) => v.charCodeAt(0)));
}
export function Uint8ArrayToSocioFileBase64(file_bin, chunkSize = 0x8000) {
const compressedData = pako.deflate(file_bin);
let binaryString = '';
for (let i = 0; i < compressedData.length; i += chunkSize) {
const chunk = compressedData.subarray(i, i + chunkSize);
binaryString += String.fromCharCode(...chunk);
}
return window.btoa(binaryString);
}
class ReadyState {
promise;
#resolve;
is_ready = false;
constructor() {
this.promise = new Promise(res => {
this.#resolve = res;
});
}
resolve(value) {
this.is_ready = true;
this.#resolve(value);
}
}