@vyer-technologies/offline-first
Version:
Forked from @wora Offline First
192 lines • 7.26 kB
JavaScript
import { Cache } from '@vyer-technologies/cache-persist';
import { NetInfo } from '@vyer-technologies/netinfo';
const isServer = typeof window === 'undefined';
const defaultOfflineOptions = {
manualExecution: false,
start: (_mutations) => Promise.resolve(_mutations),
finish: (_mutations, _error) => Promise.resolve(undefined),
onExecute: (mutation) => Promise.resolve(mutation),
onComplete: (_options) => Promise.resolve(true),
onDiscard: (_options) => Promise.resolve(true),
compare: (v1, v2) => v1.fetchTime - v2.fetchTime,
onPublish: (offlineRecord) => Promise.resolve(offlineRecord),
execute: (_offlineRecord) => Promise.reject(new Error('set execute offline options')),
};
export class OfflineFirst {
offlineStore;
busy;
offlineOptions = defaultOfflineOptions;
online = isServer;
rehydrated = isServer;
promisesRestore;
disposeListener;
constructor(persistOptions = {}) {
const persistOptionsStoreOffline = {
prefix: 'offline-first',
serialize: true,
...persistOptions,
};
this.offlineStore = new Cache(persistOptionsStoreOffline);
if (this.rehydrated) {
this.promisesRestore = Promise.resolve(true);
}
}
setOfflineOptions(offlineOptions) {
this.offlineOptions = {
...defaultOfflineOptions,
...offlineOptions,
};
}
purge() {
this.offlineStore.purge();
return this.offlineStore.flush().then(() => {
this.notify();
return true;
});
}
isManualExecution() {
const { manualExecution } = this.offlineOptions;
return manualExecution;
}
dispose() {
this.disposeListener && this.disposeListener();
this.promisesRestore = null;
this.rehydrated = false;
}
hydrate() {
if (!this.promisesRestore) {
this.promisesRestore = Promise.all([NetInfo.fetch(), this.offlineStore.restore()])
.then((result) => {
const finalize = () => {
this.disposeListener = NetInfo.addEventListener((state) => {
const { isConnected } = state;
if (this.online !== isConnected && isConnected && !this.isManualExecution()) {
this.process();
}
this.online = isConnected;
});
this.notify();
this.rehydrated = true;
return true;
};
const { isConnected } = result[0];
this.online = isConnected;
if (isConnected && !this.isManualExecution()) {
return this.process().then(finalize);
}
return finalize();
})
.catch((error) => {
this.rehydrated = false;
this.promisesRestore = null;
throw error;
});
}
return this.promisesRestore;
}
isOnline() {
return this.online;
}
subscribe(callback) {
return this.offlineStore.subscribe(callback);
}
notify() {
const { compare } = this.offlineOptions;
this.offlineStore.notify({ state: Object.values(this.getState()).sort(compare) });
}
getState() {
return this.offlineStore.getState();
}
remove(id) {
this.offlineStore.remove(id);
return this.offlineStore.flush().then(() => this.notify());
}
set(id, value) {
this.offlineStore.set(id, value);
return this.offlineStore.flush().then(() => this.notify());
}
getListMutation() {
const { compare } = this.offlineOptions;
const requests = Object.assign({}, this.getState());
return Object.values(requests).sort(compare);
}
process() {
if (!this.busy) {
const { start, finish, onExecute } = this.offlineOptions;
const listMutation = this.getListMutation();
let parallelPromises = [];
this.busy = start(listMutation).then(async (startMutations) => {
try {
for (const mutation of startMutations) {
const processMutation = await onExecute(mutation);
if (processMutation) {
if (!processMutation.serial) {
parallelPromises.push(this.executeMutation(processMutation));
}
else {
await Promise.all(parallelPromises)
.then(() => this.executeMutation(processMutation))
.catch((error) => {
throw error;
});
parallelPromises = [];
}
}
}
if (parallelPromises.length > 0) {
await Promise.all(parallelPromises);
}
return finish(this.getListMutation());
}
catch (error) {
return finish(this.getListMutation(), error);
}
finally {
this.busy = undefined;
}
});
}
return this.busy;
}
executeMutation(offlineRecord) {
const { execute, onComplete, onDiscard } = this.offlineOptions;
const { id } = offlineRecord;
offlineRecord.error = undefined;
offlineRecord.retry = offlineRecord.retry ? offlineRecord.retry + 1 : 0;
return this.set(id, { ...offlineRecord }).then(() => {
return execute(offlineRecord)
.then(async (response) => {
offlineRecord.error = undefined;
return onComplete({ offlineRecord, response }).then((complete) => {
if (complete) {
return this.remove(id);
}
return this.set(id, { ...offlineRecord });
});
})
.catch((error) => {
offlineRecord.error = error;
return onDiscard({ offlineRecord, error }).then((discard) => {
if (discard) {
return this.remove(id);
}
return this.set(id, { ...offlineRecord });
});
});
});
}
publish(options) {
const { onPublish } = this.offlineOptions;
const id = options.id ? options.id : `${this.offlineStore.getAllKeys().length}`;
const { request, serial } = options;
const fetchTime = Date.now();
return onPublish({ id, request, fetchTime, serial }).then((offlineRecord) => {
return this.set(offlineRecord.id, offlineRecord)
.then(() => offlineRecord)
.catch((error) => {
throw error;
});
});
}
}
//# sourceMappingURL=index.js.map