react-native-mosquito-transport
Version:
React native javascript sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)
935 lines (800 loc) • 34.9 kB
JavaScript
import { io } from "socket.io-client";
import EngineApi from "../../helpers/engine_api";
import { DatabaseRecordsListener } from "../../helpers/listeners";
import { deserializeE2E, serializeE2E } from "../../helpers/peripherals";
import { awaitReachableServer, awaitStore, buildFetchInterface, buildFetchResult, getReachableServer, updateCacheStore } from "../../helpers/utils";
import { CacheStore, Scoped } from "../../helpers/variables";
import { addPendingWrites, generateRecordID, getCountQuery, getRecord, insertCountQuery, insertRecord, listenQueryEntry, removePendingWrite, validateWriteValue } from "./accessor";
import { validateCollectionName, validateFilter, validateFindConfig, validateFindObject, validateListenFindConfig } from "./validator";
import { awaitRefreshToken, ensureActiveToken, listenTokenReady } from "../auth/accessor";
import { DELIVERY, RETRIEVAL } from "../../helpers/values";
import { ObjectId } from "../../vendor/bson";
import { guardObject, Validator } from "guard-object";
import { simplifyCaughtError } from "simplify-error";
import { deserializeBSON, serializeToBase64 } from "./bson";
import { basicClone } from "../../helpers/basic_clone";
import { AppState } from "react-native";
export class MTCollection {
constructor(config) {
this.builder = { ...config };
}
find = (find = {}) => ({
get: (config) => findObject({ ...this.builder, command: { find } }, config),
listen: (callback, error, config) => listenDocument(callback, error, { ...this.builder, command: { find } }, config),
count: (config) => countCollection({ ...this.builder, command: { find } }, config),
limit: (limit) => ({
get: (config) => findObject({ ...this.builder, command: { find, limit } }, config),
random: (config) => findObject({ ...this.builder, command: { find, limit, random: true } }, config),
listen: (callback, error, config) => listenDocument(callback, error, { ...this.builder, command: { find, limit } }, config),
sort: (sort, direction) => ({
get: (config) => findObject({ ...this.builder, command: { find, limit, sort, direction } }, config),
listen: (callback, error, config) => listenDocument(callback, error, {
...this.builder,
command: { find, limit, sort, direction }
}, config)
})
}),
sort: (sort, direction) => ({
get: (config) => findObject({ ...this.builder, command: { find, sort, direction } }, config),
listen: (callback, error, config) => listenDocument(callback, error, {
...this.builder,
command: { find, sort, direction }
}, config),
limit: (limit) => ({
get: (config) => findObject({ ...this.builder, command: { find, sort, direction, limit } }, config),
listen: (callback, error, config) => listenDocument(callback, error, {
...this.builder,
command: { find, sort, direction, limit }
}, config)
})
})
});
sort = (sort, direction) => this.find().sort(sort, direction);
limit = (limit) => this.find().limit(limit);
count = (config) => this.find().count(config);
get = (config) => this.find().get(config);
listen = (callback, error, config) => this.find().listen(callback, error, config);
findOne = (findOne = {}) => ({
listen: (callback, error, config) => listenDocument(callback, error, { ...this.builder, command: { findOne } }, config),
get: (config) => findObject({ ...this.builder, command: { findOne } }, config)
});
setOne = (value, config) => commitData(this.builder, value, 'setOne', config);
setMany = (value, config) => commitData(this.builder, value, 'setMany', config);
addOne = (value, config) => commitData(
this.builder,
Validator.OBJECT(value) ? { ...value, _id: new ObjectId() } : value,
'setOne',
config
);
addMany = (value, config) => commitData(
this.builder,
value.map(v => Validator.OBJECT(v) ? ({ ...v, _id: new ObjectId() }) : v),
'setMany',
config
);
updateOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'updateOne', config);
updateMany = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'updateMany', config);
mergeOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'mergeOne', config);
mergeMany = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'mergeMany', config);
replaceOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'replaceOne', config);
putOne = (find = {}, value, config) => commitData({ ...this.builder, find }, value, 'putOne', config);
deleteOne = (find = {}, config) => commitData({ ...this.builder, find }, undefined, 'deleteOne', config);
deleteMany = (find = {}, config) => commitData({ ...this.builder, find }, undefined, 'deleteMany', config);
};
export const onCollectionConnect = (builder) => ({
...collectionIO(data => ({
...initCollectionIO({ connectData: data, builder }),
onDisconnect: () => collectionIO(data2 =>
initCollectionIO({ connectData: data, disconnectData: data2, builder })
)
})),
onDisconnect: () => collectionIO(data =>
initCollectionIO({ disconnectData: data, builder })
)
});
const collectionIO = (caller) => ({
batchWrite: (map, config) => caller({ value: map, config })
});
const initCollectionIO = (data) => ({
start: () => initOnDisconnectionTask(data)
});
export const batchWrite = (builder, map, config) => commitData({ ...builder }, map, 'batchWrite', config);
const {
_listenCollection,
_listenDocument,
_startDisconnectWriteTask,
_cancelDisconnectWriteTask,
_documentCount,
_readDocument,
_queryCollection,
_writeDocument,
_writeMapDocument
} = EngineApi;
const listenDocument = (callback, onError, builder, config) => {
builder = basicClone(builder);
config = basicClone(config);
const { projectUrl, wsPrefix, serverE2E_PublicKey, baseUrl, dbUrl, dbName, path, disableCache, command, uglify, extraHeaders, castBSON } = builder;
const { find, findOne, sort, direction, limit } = command;
const { disableAuth, episode } = config || {};
const shouldCache = !disableCache;
const processId = `${++Scoped.AnyProcessIte}`;
let accessId;
validateListenFindConfig(config);
validateFilter(findOne || find);
validateCollectionName(path);
/**
* @type {import('socket.io-client').Socket}
*/
let socket;
let hasCancelled,
hasRespond,
cacheListener,
lastInitRef = 0,
lastSnapshot;
const dispatchSnapshot = s => {
const thisSnapshotId = serializeToBase64({ _: s });
if (thisSnapshotId === lastSnapshot) return;
lastSnapshot = thisSnapshotId;
callback?.(basicClone(transformBSON(s, castBSON)));
};
if (shouldCache) {
accessId = generateRecordID(builder, config, true).then(hash => {
if (hasCancelled) return hash;
cacheListener = listenQueryEntry(snapshot => {
if (!Scoped.IS_CONNECTED[projectUrl]) dispatchSnapshot(snapshot);
}, { accessId: hash, builder, config, processId });
return hash;
});
awaitStore().then(() => {
if (hasCancelled) return;
getReachableServer(projectUrl).then(connected => {
if (!connected && !hasRespond && !hasCancelled && shouldCache)
DatabaseRecordsListener.dispatch('d', processId);
});
});
}
let foregroundListener;
const clearForegroundListener = () => {
if (!foregroundListener) return;
foregroundListener.remove();
foregroundListener = undefined;
}
const clearSocket = () => {
if (socket) {
socket.close();
socket = undefined;
}
}
const init = async () => {
clearForegroundListener();
const processID = ++lastInitRef;
if (!disableAuth) await awaitRefreshToken(projectUrl);
if (hasCancelled || processID !== lastInitRef) return;
const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
const pureConfig = stripRequestConfig(config);
const authObj = {
commands: stripUndefined({
config: pureConfig && serializeToBase64(pureConfig),
path,
find: serializeToBase64(findOne || find),
sort,
direction,
limit
}),
...dbName ? { dbName } : undefined,
...dbUrl ? { dbUrl } : undefined
};
const [encPlate, [privateKey]] = uglify ? await serializeE2E({ _body: authObj }, mtoken, serverE2E_PublicKey) : ['', []];
if (hasCancelled || processID !== lastInitRef) return;
socket = io(`${wsPrefix}://${baseUrl}`, {
transports: ['websocket', 'polling', 'flashsocket'],
extraHeaders,
auth: {
...uglify ? { e2e: encPlate.toString('base64') } : {
_body: authObj,
...mtoken ? { mtoken } : {}
},
_m_internal: true,
_m_route: (findOne ? _listenDocument : _listenCollection)(uglify)
},
reconnection: false
});
socket.on('mSnapshot', async ([err, snapshot]) => {
hasRespond = true;
if (err) {
if (typeof onError === 'function') {
onError(simplifyCaughtError(err).simpleError);
} else console.error('unhandled listen for:', { path, find }, ' error:', err);
} else {
if (uglify) snapshot = await deserializeE2E(snapshot, serverE2E_PublicKey, privateKey);
snapshot = hydrateForeignDoc(deserializeBSON(snapshot)._);
dispatchSnapshot(snapshot);
if (shouldCache) insertRecord(builder, config, await accessId, snapshot, episode);
}
});
const reconnect = (timeout) => {
if (processID !== lastInitRef || hasCancelled) return;
const reloadIntance = async () => {
if (processID === lastInitRef && !hasCancelled) remountInit();
}
if (AppState.currentState === 'active') {
awaitReachableServer(projectUrl, timeout).then(reloadIntance);
} else {
foregroundListener = AppState.addEventListener('change', s => {
if (s === 'active') {
clearForegroundListener();
reloadIntance();
}
});
}
}
let wasHandled;
socket.on('connect_error', () => {
if (processID !== lastInitRef || wasHandled) return;
wasHandled = true;
clearSocket();
reconnect(3000);
});
socket.on('disconnect', r => {
if (processID !== lastInitRef || wasHandled) return;
wasHandled = true;
clearSocket();
if (r === 'io client disconnect' || r === 'io server disconnect') {
canceller();
} else reconnect(true);
});
};
const remountInit = () => {
if (socket) {
++lastInitRef;
clearSocket();
}
init();
}
let lastTokenStatus;
let tokenListener;
if (disableAuth) {
init();
} else {
tokenListener = listenTokenReady(ready => {
if (lastTokenStatus === (ready || false)) return;
if (ready) {
remountInit();
} else {
++lastInitRef;
clearForegroundListener();
clearSocket();
}
lastTokenStatus = ready || false;
}, projectUrl);
}
const canceller = () => {
if (hasCancelled) return;
hasCancelled = true;
cacheListener?.();
tokenListener?.();
clearForegroundListener();
clearSocket();
}
return canceller;
};
const initOnDisconnectionTask = ({ builder, connectData, disconnectData }) => {
connectData = basicClone(connectData);
disconnectData = basicClone(disconnectData);
builder = basicClone(builder);
const { projectUrl, wsPrefix, baseUrl, serverE2E_PublicKey, dbUrl, dbName, extraHeaders, uglify } = builder;
const disableAuth = false;
[connectData, disconnectData].forEach((e) => {
if (e) {
if (e.config !== undefined)
guardObject({
stepping: t => t === undefined || Validator.BOOLEAN(t)
}).validate(e.config);
cleanBatchWrite(e.value).forEach(e => {
const { scope, find, value, path } = e;
validateCollectionName(path);
validateWriteValue({ find, value, type: scope });
});
}
});
/**
* @type {import('socket.io-client').Socket}
*/
let socket,
hasCancelled,
lastInitRef = 0;
let foregroundListener;
const clearForegroundListener = () => {
if (!foregroundListener) return;
foregroundListener.remove();
foregroundListener = undefined;
}
const clearSocket = () => {
if (socket) {
socket.close();
socket = undefined;
}
}
const init = async () => {
const processID = ++lastInitRef;
const mtoken = disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl];
const makeObj = (d) => ({
...d?.config,
value: serializeToBase64({ _: cleanBatchWrite(d.value) })
});
const authObj = {
commands: {
...connectData ? { connectTask: makeObj(connectData) } : {},
...disconnectData ? { disconnectTask: makeObj(disconnectData) } : {}
},
...dbName ? { dbName } : undefined,
...dbUrl ? { dbUrl } : undefined
};
const uglifyData = uglify && (await serializeE2E({ _body: authObj }, mtoken, serverE2E_PublicKey))[0].toString('base64');
if (hasCancelled || processID !== lastInitRef) return;
socket = io(`${wsPrefix}://${baseUrl}`, {
transports: ['websocket', 'polling', 'flashsocket'],
extraHeaders,
auth: {
...uglify ? { e2e: uglifyData } : {
...mtoken ? { mtoken } : {},
_body: authObj
},
_m_internal: true,
_m_route: _startDisconnectWriteTask(uglify)
},
reconnection: false
});
const reconnect = (timeout) => {
if (processID !== lastInitRef || hasCancelled) return;
const reloadIntance = async () => {
if (!disableAuth) await awaitRefreshToken(projectUrl);
if (processID === lastInitRef && !hasCancelled) remountInit();
}
if (AppState.currentState === 'active') {
awaitReachableServer(projectUrl, timeout).then(reloadIntance);
} else {
foregroundListener = AppState.addEventListener('change', s => {
if (s === 'active') {
clearForegroundListener();
reloadIntance();
}
});
}
}
let wasHandled;
socket.on('connect_error', () => {
if (processID !== lastInitRef || wasHandled) return;
wasHandled = true;
clearSocket();
reconnect(3000);
});
socket.on('disconnect', r => {
if (processID !== lastInitRef || wasHandled) return;
wasHandled = true;
clearSocket();
if (r === 'io client disconnect' || r === 'io server disconnect') {
canceller();
} else reconnect(true);
});
};
const remountInit = () => {
if (socket) {
++lastInitRef;
clearSocket();
}
init();
}
let lastTokenStatus;
let tokenListener;
if (disableAuth) {
init();
} else {
tokenListener = listenTokenReady(ready => {
if (lastTokenStatus === (ready || false)) return;
if (ready) {
remountInit();
} else {
++lastInitRef;
clearForegroundListener();
clearSocket();
}
lastTokenStatus = ready || false;
}, projectUrl);
}
const canceller = () => {
if (hasCancelled) return;
hasCancelled = true;
tokenListener?.();
clearForegroundListener();
if (socket) {
const thisSocket = socket;
try {
thisSocket.timeout(5000).emitWithAck(_cancelDisconnectWriteTask(uglify)).finally(() => {
thisSocket.close();
});
} catch (error) {
console.warn('socket closure error:', error);
}
}
};
return canceller;
};
const countCollection = async (builder, config) => {
builder = basicClone(builder);
config = basicClone(config);
const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, maxRetries = 1, uglify, extraHeaders, path, disableCache, command = {} } = builder;
const { find } = command;
const { disableAuth } = config || {};
const accessId = await generateRecordID({ ...builder, countDoc: true }, config);
await awaitStore();
if (config !== undefined)
guardObject({
disableAuth: t => t === undefined || Validator.BOOLEAN(t)
}).validate(config);
validateFilter(find);
validateCollectionName(path);
let retries = 0;
const readValue = () => new Promise(async (resolve, reject) => {
++retries;
const finalize = (a, b) => {
if (Validator.NUMBER(a)) {
resolve(a);
} else reject(b);
};
try {
if (!disableAuth) await ensureActiveToken(projectUrl);
const [reqBuilder, [privateKey]] = await buildFetchInterface({
body: {
commands: { path, find: serializeToBase64(find) },
...dbName ? { dbName } : undefined,
...dbUrl ? { dbUrl } : undefined
},
...disableAuth ? {} : { authToken: Scoped.AuthJWTToken[projectUrl] },
serverE2E_PublicKey,
uglify,
extraHeaders
});
const data = await buildFetchResult(await fetch(_documentCount(projectUrl, uglify), reqBuilder), uglify);
const f = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
finalize(f.result);
if (!disableCache) insertCountQuery(builder, accessId, f.result);
} catch (e) {
const b4Data = await getCountQuery(builder, accessId).catch(() => null);
if (e?.simpleError) {
e.simpleError.ack = true;
finalize(undefined, e.simpleError);
} else if (!disableCache && Validator.NUMBER(b4Data)) {
finalize(b4Data);
} else if (retries > maxRetries) {
finalize(undefined, { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` });
} else {
awaitReachableServer(projectUrl, true).then(() => {
readValue().then(
e => { finalize(e); },
e => { finalize(undefined, e); }
);
});
}
}
});
return await readValue();
};
const stripRequestConfig = (config) => {
const known_fields = ['extraction', 'returnOnly', 'excludeFields'];
const requestConfig = Object.entries({ ...config }).map(([k, v]) =>
known_fields.includes(k) ? [k, v] : null
).filter(v => v);
return requestConfig.length ? Object.fromEntries(requestConfig) : undefined;
};
const stripUndefined = o => Object.fromEntries(
Object.entries(o).filter(v => v[1] !== undefined)
);
const hydrateForeignDoc = ({ data, doc_holder }) => {
const isList = Array.isArray(data);
const filled = (isList ? data : [data]).map(v => {
if (v?._foreign_doc) {
v._foreign_doc = Array.isArray(v._foreign_doc)
? v._foreign_doc.map(k => doc_holder[k])
: doc_holder[v._foreign_doc];
}
return v;
});
return isList ? filled : filled[0];
}
const transformBSON = (d, castBSON) => {
if (castBSON) return d && deserializeBSON(serializeToBase64({ _: d }), true)._;
return basicClone(d);
};
const findObject = async (builder, initConfig) => {
builder = basicClone(builder);
const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, maxRetries = 1, path, disableCache = false, uglify, extraHeaders, command, castBSON } = builder;
const pureConfig = stripRequestConfig(initConfig);
validateFindObject(command);
validateFindConfig(initConfig);
validateCollectionName(path);
let { onWaiting, ...config } = basicClone(initConfig) || {};
const { find, findOne, sort, direction, limit, random } = command;
const { retrieval = RETRIEVAL.DEFAULT, episode = 0, disableAuth, disableMinimizer } = config || {};
const enableMinimizer = !disableMinimizer;
const accessId = await generateRecordID(builder, config, true);
const processAccessId = `${accessId}_${limit}_${episode}_${projectUrl}_${dbUrl}_${dbName}_${retrieval}_${disableCache}`;
const getRecordData = () => getRecord(builder, accessId, episode);
const shouldCache = (retrieval !== RETRIEVAL.DEFAULT || !disableCache) &&
![RETRIEVAL.NO_CACHE_NO_AWAIT, RETRIEVAL.NO_CACHE_AWAIT].includes(retrieval);
await awaitStore();
let intruder = {};
let retries = 0, hasFinalize;
const readValue = () => new Promise(async (resolve, reject) => {
const retryProcess = ++retries,
instantProcess = retryProcess === 1;
const finalize = (a, b) => {
const res = (instantProcess && a) ? intruder ? transformBSON(a[0] || undefined, castBSON) : a[0] : a;
const doClone = v => intruder ? basicClone(v) : v;
if (a) {
resolve(instantProcess ? doClone(res) : a);
} else reject(instantProcess ? doClone(b) : b);
if (hasFinalize || !instantProcess) return;
hasFinalize = true;
if (enableMinimizer) {
const resolutionList = (Scoped.PendingDbReadCollective[processAccessId] || []).slice(0);
if (Scoped.PendingDbReadCollective[processAccessId])
delete Scoped.PendingDbReadCollective[processAccessId];
resolutionList.forEach(e => {
e(a ? { result: doClone(res) } : undefined, doClone(b));
});
}
};
try {
if (instantProcess) {
if (enableMinimizer) {
if (Scoped.PendingDbReadCollective[processAccessId]) {
Scoped.PendingDbReadCollective[processAccessId].push((a, b) => {
if (a) resolve(a.result);
else reject(b);
});
return;
}
Scoped.PendingDbReadCollective[processAccessId] = [];
}
let staleData;
if (
retrieval.startsWith('sticky') &&
(staleData = await getRecordData())
) {
finalize(staleData);
if (retrieval !== RETRIEVAL.STICKY_RELOAD) return;
}
}
if (!disableAuth) await ensureActiveToken(projectUrl);
const [reqBuilder, [privateKey]] = await buildFetchInterface({
body: {
commands: stripUndefined({
config: pureConfig && serializeToBase64(pureConfig),
path,
find: serializeToBase64(findOne || find),
sort,
direction,
limit,
random
}),
...dbName ? { dbName } : undefined,
...dbUrl ? { dbUrl } : undefined
},
authToken: disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
serverE2E_PublicKey,
uglify,
extraHeaders
});
const data = await buildFetchResult(await fetch((findOne ? _readDocument : _queryCollection)(projectUrl, uglify), reqBuilder), uglify);
const result = hydrateForeignDoc(
deserializeBSON((uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data).result)._
);
if (shouldCache) insertRecord(builder, config, accessId, result, episode);
finalize([result]);
} catch (e) {
let thisRecord;
const getThisRecord = async () => thisRecord ? thisRecord[0] :
(thisRecord = [await getRecordData()])[0];
if (e?.simpleError) {
e.simpleError.ack = true;
finalize(undefined, e?.simpleError);
} else if (
(retrieval === RETRIEVAL.CACHE_NO_AWAIT && !(await getThisRecord())) ||
retrieval === RETRIEVAL.STICKY_NO_AWAIT ||
retrieval === RETRIEVAL.NO_CACHE_NO_AWAIT
) {
finalize(undefined, simplifyCaughtError(e).simpleError);
} else if (
shouldCache &&
[
RETRIEVAL.DEFAULT,
RETRIEVAL.CACHE_NO_AWAIT,
RETRIEVAL.CACHE_AWAIT
].includes(retrieval) &&
await getThisRecord()
) {
finalize(await getThisRecord());
} else if (retries > maxRetries) {
finalize(undefined, { error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` });
} else {
awaitReachableServer(projectUrl, true).then(() => {
if (intruder) {
intruder.resolve = undefined;
intruder.reject = undefined;
readValue().then(
e => { finalize(e); },
e => { finalize(undefined, e); }
);
}
});
const cleanseIntruder = () => {
intruder = undefined;
}
intruder.resolve = (data) => {
cleanseIntruder();
finalize([data]);
};
intruder.reject = (err) => {
cleanseIntruder();
finalize(undefined, err);
};
onWaiting?.(intruder);
onWaiting = undefined;
}
}
});
return (await readValue());
};
const transformNullRecursively = obj => Object.fromEntries(
Object.entries(obj).map(([k, v]) =>
[k, [undefined, Infinity, NaN].includes(v) ? null : Validator.OBJECT(v) ? transformNullRecursively(v) : v]
)
);
const cleanBatchWrite = (value) => basicClone(value).map(v => {
if (Validator.OBJECT(v?.value)) {
v.value = transformNullRecursively(v.value);
} else if (Array.isArray(v?.value)) {
v.value = v.value.map(e =>
Validator.OBJECT(e) ? transformNullRecursively(e) : e
);
}
return v;
});
const commitData = async (builder, value, type, config) => {
builder = basicClone(builder);
config = basicClone(config);
// transform undefined
if (Validator.OBJECT(value)) {
value = value && deserializeBSON(serializeToBase64({ _: transformNullRecursively(value) }))._;
} else if (type === 'batchWrite' && Array.isArray(value)) {
value = deserializeBSON(
serializeToBase64({
_: cleanBatchWrite(value)
})
)._;
}
const { projectUrl, serverE2E_PublicKey, dbUrl, dbName, maxRetries = 1, path, find, disableCache, uglify, extraHeaders } = builder;
const { disableAuth, delivery = DELIVERY.DEFAULT, stepping } = config || {};
const writeId = `${Date.now() + ++Scoped.PendingIte}`;
const isBatchWrite = type === 'batchWrite';
const shouldCache = (delivery !== DELIVERY.DEFAULT || !disableCache) &&
![DELIVERY.NO_CACHE_AWAIT, DELIVERY.NO_CACHE_NO_AWAIT].includes(delivery);
await awaitStore();
if (shouldCache) {
await addPendingWrites(builder, writeId, { value, type, config: stripUndefined({ disableAuth, stepping }) });
Scoped.OutgoingWrites[writeId] = true;
if (Scoped.dispatchingWritesPromise[projectUrl])
await Scoped.dispatchingWritesPromise[projectUrl];
}
let retries = 0, hasFinalize;
const sendValue = () => new Promise(async (resolve, reject) => {
const retryProcess = ++retries,
instantProcess = retryProcess === 1;
const finalize = (a, b, c) => {
const { removeCache, revertCache } = c || {};
if (!instantProcess) {
if (a) a = { a, c };
if (b) b = { b, c };
}
if (a) {
resolve(a);
} else reject(b);
if (hasFinalize || !instantProcess) return;
hasFinalize = true;
if (Scoped.OutgoingWrites[writeId])
delete Scoped.OutgoingWrites[writeId];
if (shouldCache) {
if (removeCache) removePendingWrite(builder, writeId, revertCache);
}
};
try {
if (!disableAuth) await ensureActiveToken(projectUrl);
const [reqBuilder, [privateKey]] = await buildFetchInterface({
body: {
commands: stripUndefined({
value: value && serializeToBase64({ _: value }),
...isBatchWrite ? { stepping } : {
path,
scope: type,
find: find && serializeToBase64(find)
}
}),
...dbName ? { dbName } : undefined,
...dbUrl ? { dbUrl } : undefined
},
serverE2E_PublicKey,
authToken: disableAuth ? undefined : Scoped.AuthJWTToken[projectUrl],
uglify,
extraHeaders
});
const data = await buildFetchResult(await fetch((isBatchWrite ? _writeMapDocument : _writeDocument)(projectUrl, uglify), reqBuilder), uglify);
const f = uglify ? await deserializeE2E(data, serverE2E_PublicKey, privateKey) : data;
finalize({ ...f.statusData }, undefined, { removeCache: true });
} catch (e) {
if (e?.simpleError) {
console.error(`${type} error (${path}), ${e.simpleError?.message}`);
e.simpleError.ack = true;
finalize(undefined, e.simpleError, { removeCache: true, revertCache: true });
} else if (delivery === DELIVERY.NO_CACHE_NO_AWAIT) {
finalize(undefined, simplifyCaughtError(e).simpleError);
} else if (retries > maxRetries) {
finalize(
undefined,
{ error: 'retry_limit_exceeded', message: `retry exceed limit(${maxRetries})` },
{ removeCache: true, revertCache: true }
);
} else {
if (delivery === DELIVERY.NO_CACHE_AWAIT) {
awaitReachableServer(projectUrl, true).then(() => {
sendValue().then(
e => { finalize(e.a, undefined, e.c); },
e => { finalize(undefined, e.b, e.c); }
);
});
} else if (shouldCache) finalize({ status: 'queued' });
else finalize(undefined, simplifyCaughtError(e).simpleError);
}
}
});
return (await sendValue());
};
export const trySendPendingWrite = async (projectUrl) => {
if (Scoped.dispatchingWritesPromise[projectUrl]) return;
let resolveCallback;
Scoped.dispatchingWritesPromise[projectUrl] = new Promise(async resolve => {
resolveCallback = resolve;
});
const sortedWrite = Object.entries(CacheStore.PendingWrites[projectUrl] || {})
.filter(([k]) => !Scoped.OutgoingWrites[k])
.sort((a, b) => a[1].addedOn - b[1].addedOn);
let resolveCounts = 0;
for (const [writeId, { snapshot, builder, attempts = 1 }] of sortedWrite) {
try {
await commitData(
{ ...Scoped.InitializedProject[projectUrl], ...builder, find: deserializeBSON(builder.find, true)._ },
deserializeBSON(snapshot.value, true)._,
snapshot.type,
{ ...snapshot.config, delivery: DELIVERY.NO_CACHE_NO_AWAIT }
);
delete CacheStore.PendingWrites[projectUrl][writeId];
++resolveCounts;
} catch (err) {
const { maxRetries } = builder;
if (err?.ack || !maxRetries || attempts >= maxRetries) {
delete CacheStore.PendingWrites[projectUrl][writeId];
++resolveCounts;
} else if (CacheStore.PendingWrites[projectUrl]?.[writeId]) {
CacheStore.PendingWrites[projectUrl][writeId].attempts = attempts + 1;
}
}
}
resolveCallback();
if (Scoped.dispatchingWritesPromise[projectUrl])
delete Scoped.dispatchingWritesPromise[projectUrl];
updateCacheStore(['PendingWrites']);
if (
(sortedWrite.length - resolveCounts) &&
await getReachableServer(projectUrl)
) trySendPendingWrite(projectUrl);
};