apical-store
Advanced tools
Comparing version 0.0.82 to 0.0.83
@@ -51,5 +51,9 @@ (function (global, factory) { | ||
: undefined); | ||
function findGrandParent(observable) { | ||
function findGrandParent(observable, visited = new Set()) { | ||
if (visited.has(observable)) { | ||
throw new Error("Circular reference detected in observable structure"); | ||
} | ||
visited.add(observable); | ||
if (observable.parent) | ||
return findGrandParent(observable.parent); | ||
return findGrandParent(observable.parent, visited); | ||
else | ||
@@ -882,3 +886,2 @@ return observable; | ||
constructor({ debounceRate, model, encode, decode, onSyncStart, onSyncEnd, localPersistence, remotePersistence, } = {}) { | ||
this.isOnline = true; | ||
this.deferredPresent = false; | ||
@@ -895,6 +898,2 @@ this.onSyncStart = () => { }; | ||
this.$$decode = (x) => x; | ||
/** | ||
* List of all items in the store (including deleted items) However, the list is not observable | ||
*/ | ||
this.copy = this.$$observableObject.copy; | ||
this.new = this.$$model.new; | ||
@@ -1001,3 +1000,3 @@ this.sync = debounce(this.$$sync.bind(this), this.$$debounceRate); | ||
ts: Date.now(), | ||
data: serializedLine, | ||
id: item.id, | ||
}); | ||
@@ -1026,4 +1025,6 @@ } | ||
*/ | ||
yield this.$$localPersistence.putDeferred(deferredArray.concat(...toDeffer)); | ||
this.deferredPresent = true; | ||
if (this.$$remotePersistence) { | ||
yield this.$$localPersistence.putDeferred(deferredArray.concat(...toDeffer)); | ||
this.deferredPresent = true; | ||
} | ||
this.onSyncEnd(); | ||
@@ -1101,2 +1102,3 @@ }); | ||
let deferredArray = yield this.$$localPersistence.getDeferred(); | ||
let conflicts = 0; | ||
if (localVersion === remoteVersion && deferredArray.length === 0) { | ||
@@ -1112,4 +1114,3 @@ return { | ||
var _a; | ||
let item = this.$$deserialize(x.data); | ||
const conflict = remoteUpdates.rows.findIndex((y) => y.id === item.id); | ||
const conflict = remoteUpdates.rows.findIndex((y) => y.id === x.id); | ||
// take row-specific version if available, otherwise rely on latest version | ||
@@ -1123,2 +1124,3 @@ const comparison = Number(((_a = remoteUpdates.rows[conflict]) === null || _a === void 0 ? void 0 : _a.ts) || remoteVersion); | ||
remoteUpdates.rows.splice(conflict, 1); | ||
conflicts++; | ||
return true; | ||
@@ -1128,2 +1130,3 @@ } | ||
// there's a conflict, and the remote change is newer | ||
conflicts++; | ||
return false; | ||
@@ -1137,5 +1140,4 @@ } | ||
const updatedRows = new Map(); | ||
for (const local of deferredArray) { | ||
let item = this.$$deserialize(local.data); | ||
updatedRows.set(item.id, local.data); | ||
for (const d of deferredArray) { | ||
updatedRows.set(d.id, yield this.$$localPersistence.getOne(d.id)); | ||
// latest deferred write wins since it would overwrite the previous one | ||
@@ -1163,3 +1165,3 @@ } | ||
let pulled = remoteUpdates.rows.length; | ||
return { pushed, pulled }; | ||
return { pushed, pulled, conflicts }; | ||
} | ||
@@ -1195,2 +1197,33 @@ catch (e) { | ||
} | ||
backup() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.$$localPersistence) { | ||
throw new Error("Local persistence not available"); | ||
} | ||
return JSON.stringify(yield this.$$localPersistence.dump()); | ||
}); | ||
} | ||
restoreBackup(input) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.$$remotePersistence) { | ||
yield this.$$remotePersistence.checkOnline(); | ||
if (!this.$$remotePersistence.isOnline) { | ||
throw new Error("Can not restore backup when the client is offline!"); | ||
} | ||
} | ||
const dump = JSON.parse(input); | ||
if (!this.$$localPersistence) { | ||
throw new Error("Local persistence not available"); | ||
} | ||
yield this.$$localPersistence.put(dump.data); | ||
yield this.$$localPersistence.putDeferred(dump.metadata.deferred); | ||
yield this.$$localPersistence.putVersion(dump.metadata.version); | ||
yield this.$$loadFromLocal(); | ||
if (this.$$remotePersistence) { | ||
yield this.$$remotePersistence.put(dump.data); | ||
return yield this.sync(); // to get latest version number | ||
} | ||
return []; | ||
}); | ||
} | ||
/** | ||
@@ -1205,2 +1238,8 @@ * Public methods, to be used by the application | ||
} | ||
/** | ||
* List of all items in the store (including deleted items) However, the list is not observable | ||
*/ | ||
get copy() { | ||
return this.$$observableObject.copy; | ||
} | ||
getByID(id) { | ||
@@ -1215,3 +1254,3 @@ return this.$$observableObject.target.find((x) => x.id === id); | ||
} | ||
restore(id) { | ||
restoreItem(id) { | ||
const item = this.$$observableObject.target.find((x) => x.id === id); | ||
@@ -1252,2 +1291,12 @@ if (!item) { | ||
} | ||
updateByID(id, item) { | ||
const index = this.$$observableObject.target.findIndex((x) => x.id === id); | ||
if (index === -1) { | ||
throw new Error("Item not found."); | ||
} | ||
if (this.$$observableObject.target[index].id !== item.id) { | ||
throw new Error("ID mismatch."); | ||
} | ||
this.updateByIndex(index, item); | ||
} | ||
isUpdated() { | ||
@@ -1273,2 +1322,7 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
get isOnline() { | ||
if (!this.$$remotePersistence) | ||
return false; | ||
return this.$$remotePersistence.isOnline; | ||
} | ||
} | ||
@@ -1345,2 +1399,5 @@ | ||
} | ||
getOne(id) { | ||
return this.store("readonly", (store) => this.pr(store.get(id))); | ||
} | ||
getVersion() { | ||
@@ -1396,2 +1453,24 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
dump() { | ||
return this.store("readonly", (store) => __awaiter(this, void 0, void 0, function* () { | ||
let data = []; | ||
if (store.getAll && store.getAllKeys) { | ||
const keys = yield this.pr(store.getAllKeys()); | ||
const values = yield this.pr(store.getAll()); | ||
data = keys.map((key, index) => [key, values[index]]); | ||
} | ||
else { | ||
yield this.eachCursor(store, (cursor) => { | ||
data.push([cursor.key, cursor.value]); | ||
}); | ||
} | ||
return { | ||
data, | ||
metadata: { | ||
version: yield this.getVersion(), | ||
deferred: yield this.getDeferred(), | ||
}, | ||
}; | ||
})); | ||
} | ||
} | ||
@@ -1401,6 +1480,30 @@ | ||
constructor({ endpoint, token, name, }) { | ||
this.isOnline = true; | ||
this.baseUrl = endpoint; | ||
this.token = token; | ||
this.table = name; | ||
this.checkOnline(); | ||
} | ||
checkOnline() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
yield fetch(this.baseUrl, { | ||
method: "HEAD", | ||
}); | ||
this.isOnline = true; | ||
} | ||
catch (e) { | ||
this.isOnline = false; | ||
this.retryConnection(); | ||
} | ||
}); | ||
} | ||
retryConnection() { | ||
let i = setInterval(() => { | ||
if (this.isOnline) | ||
clearInterval(i); | ||
else | ||
this.checkOnline(); | ||
}, 5000); | ||
} | ||
getSince() { | ||
@@ -1414,9 +1517,25 @@ return __awaiter(this, arguments, void 0, function* (version = 0) { | ||
const url = `${this.baseUrl}/${this.table}/${version}/${page}`; | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = yield response.json(); | ||
let res; | ||
try { | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = yield response.json(); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
break; | ||
} | ||
if (res.success === false) { | ||
result = []; | ||
version = 0; | ||
break; | ||
} | ||
const output = JSON.parse(res.output); | ||
@@ -1434,9 +1553,19 @@ nextPage = output.rows.length > 0 && version !== 0; | ||
const url = `${this.baseUrl}/${this.table}/0/Infinity`; | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = yield response.json(); | ||
let res; | ||
try { | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = yield response.json(); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
} | ||
if (res.success) | ||
@@ -1455,9 +1584,15 @@ return Number(JSON.parse(res.output).version); | ||
const url = `${this.baseUrl}/${this.table}`; | ||
yield fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
try { | ||
yield fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
throw e; | ||
} | ||
return; | ||
@@ -1464,0 +1599,0 @@ }); |
import { ObservableMeta } from "./meta"; | ||
export declare function findGrandParent<T extends object>(observable: ObservableMeta<T>): ObservableMeta<T>; | ||
export declare function findGrandParent<T extends object>(observable: ObservableMeta<T>, visited?: Set<unknown>): ObservableMeta<T>; | ||
export declare function copy<T>(obj: T): T; | ||
@@ -4,0 +4,0 @@ export declare function copyPropertiesTo(source: any, target: object): void; |
@@ -13,5 +13,9 @@ // polyfill | ||
: this); | ||
export function findGrandParent(observable) { | ||
export function findGrandParent(observable, visited = new Set()) { | ||
if (visited.has(observable)) { | ||
throw new Error("Circular reference detected in observable structure"); | ||
} | ||
visited.add(observable); | ||
if (observable.parent) | ||
return findGrandParent(observable.parent); | ||
return findGrandParent(observable.parent, visited); | ||
else | ||
@@ -18,0 +22,0 @@ return observable; |
import { Persistence } from "./type"; | ||
export type deferredArray = { | ||
ts: number; | ||
data: string; | ||
id: string; | ||
}[]; | ||
export interface Dump { | ||
data: [string, string][]; | ||
metadata: { | ||
version: number; | ||
deferred: deferredArray; | ||
}; | ||
} | ||
export interface LocalPersistence extends Persistence { | ||
getAll(): Promise<string[]>; | ||
getOne(id: string): Promise<string>; | ||
putVersion(number: number): Promise<void>; | ||
getDeferred(): Promise<deferredArray>; | ||
putDeferred(arr: deferredArray): Promise<void>; | ||
dump(): Promise<Dump>; | ||
} | ||
@@ -35,2 +44,3 @@ export declare class IDB implements LocalPersistence { | ||
getAll(): Promise<string[]>; | ||
getOne(id: string): Promise<string>; | ||
getVersion(): Promise<number>; | ||
@@ -53,2 +63,9 @@ putVersion(version: number): Promise<void>; | ||
clearMetadata(): Promise<void>; | ||
dump(): Promise<{ | ||
data: [string, string][]; | ||
metadata: { | ||
version: number; | ||
deferred: deferredArray; | ||
}; | ||
}>; | ||
} |
@@ -79,2 +79,5 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
} | ||
getOne(id) { | ||
return this.store("readonly", (store) => this.pr(store.get(id))); | ||
} | ||
getVersion() { | ||
@@ -130,2 +133,24 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
dump() { | ||
return this.store("readonly", (store) => __awaiter(this, void 0, void 0, function* () { | ||
let data = []; | ||
if (store.getAll && store.getAllKeys) { | ||
const keys = yield this.pr(store.getAllKeys()); | ||
const values = yield this.pr(store.getAll()); | ||
data = keys.map((key, index) => [key, values[index]]); | ||
} | ||
else { | ||
yield this.eachCursor(store, (cursor) => { | ||
data.push([cursor.key, cursor.value]); | ||
}); | ||
} | ||
return { | ||
data, | ||
metadata: { | ||
version: yield this.getVersion(), | ||
deferred: yield this.getDeferred(), | ||
}, | ||
}; | ||
})); | ||
} | ||
} |
@@ -11,7 +11,10 @@ import { Persistence } from "./type"; | ||
}>; | ||
isOnline: boolean; | ||
checkOnline: () => Promise<void>; | ||
} | ||
export declare class CloudFlareApexoDB implements Persistence { | ||
export declare class CloudFlareApexoDB implements RemotePersistence { | ||
private baseUrl; | ||
private token; | ||
private table; | ||
isOnline: boolean; | ||
constructor({ endpoint, token, name, }: { | ||
@@ -22,2 +25,4 @@ endpoint: string; | ||
}); | ||
checkOnline(): Promise<void>; | ||
retryConnection(): void; | ||
getSince(version?: number): Promise<{ | ||
@@ -24,0 +29,0 @@ version: number; |
@@ -12,6 +12,30 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
constructor({ endpoint, token, name, }) { | ||
this.isOnline = true; | ||
this.baseUrl = endpoint; | ||
this.token = token; | ||
this.table = name; | ||
this.checkOnline(); | ||
} | ||
checkOnline() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
yield fetch(this.baseUrl, { | ||
method: "HEAD", | ||
}); | ||
this.isOnline = true; | ||
} | ||
catch (e) { | ||
this.isOnline = false; | ||
this.retryConnection(); | ||
} | ||
}); | ||
} | ||
retryConnection() { | ||
let i = setInterval(() => { | ||
if (this.isOnline) | ||
clearInterval(i); | ||
else | ||
this.checkOnline(); | ||
}, 5000); | ||
} | ||
getSince() { | ||
@@ -25,9 +49,25 @@ return __awaiter(this, arguments, void 0, function* (version = 0) { | ||
const url = `${this.baseUrl}/${this.table}/${version}/${page}`; | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = yield response.json(); | ||
let res; | ||
try { | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = yield response.json(); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
break; | ||
} | ||
if (res.success === false) { | ||
result = []; | ||
version = 0; | ||
break; | ||
} | ||
const output = JSON.parse(res.output); | ||
@@ -45,9 +85,19 @@ nextPage = output.rows.length > 0 && version !== 0; | ||
const url = `${this.baseUrl}/${this.table}/0/Infinity`; | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = yield response.json(); | ||
let res; | ||
try { | ||
const response = yield fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = yield response.json(); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
} | ||
if (res.success) | ||
@@ -66,9 +116,15 @@ return Number(JSON.parse(res.output).version); | ||
const url = `${this.baseUrl}/${this.table}`; | ||
const response = yield fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
try { | ||
yield fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
} | ||
catch (e) { | ||
this.checkOnline(); | ||
throw e; | ||
} | ||
return; | ||
@@ -75,0 +131,0 @@ }); |
@@ -5,3 +5,2 @@ import { LocalPersistence } from "./persistence/local"; | ||
export declare class Store<T extends Document> { | ||
isOnline: boolean; | ||
deferredPresent: boolean; | ||
@@ -83,2 +82,9 @@ onSyncStart: () => void; | ||
private $$sync; | ||
backup(): Promise<string>; | ||
restoreBackup(input: string): Promise<{ | ||
pushed?: number; | ||
pulled?: number; | ||
conflicts?: number; | ||
exception?: string; | ||
}[]>; | ||
/** | ||
@@ -94,3 +100,3 @@ * Public methods, to be used by the application | ||
*/ | ||
copy: T[]; | ||
get copy(): T[]; | ||
getByID(id: string): T | undefined; | ||
@@ -101,3 +107,3 @@ add(item: T): void; | ||
}>(this: new () => T_1, data?: import("./model").RecursivePartial<T_1>) => T_1; | ||
restore(id: string): void; | ||
restoreItem(id: string): void; | ||
delete(item: T): void; | ||
@@ -107,2 +113,3 @@ deleteByIndex(index: number): void; | ||
updateByIndex(index: number, item: T): void; | ||
updateByID(id: string, item: T): void; | ||
sync: () => Promise<ReturnType<() => Promise<{ | ||
@@ -115,2 +122,3 @@ exception?: string; | ||
get loaded(): Promise<void>; | ||
get isOnline(): boolean; | ||
} |
@@ -15,3 +15,2 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
constructor({ debounceRate, model, encode, decode, onSyncStart, onSyncEnd, localPersistence, remotePersistence, } = {}) { | ||
this.isOnline = true; | ||
this.deferredPresent = false; | ||
@@ -28,6 +27,2 @@ this.onSyncStart = () => { }; | ||
this.$$decode = (x) => x; | ||
/** | ||
* List of all items in the store (including deleted items) However, the list is not observable | ||
*/ | ||
this.copy = this.$$observableObject.copy; | ||
this.new = this.$$model.new; | ||
@@ -134,3 +129,3 @@ this.sync = debounce(this.$$sync.bind(this), this.$$debounceRate); | ||
ts: Date.now(), | ||
data: serializedLine, | ||
id: item.id, | ||
}); | ||
@@ -159,4 +154,6 @@ } | ||
*/ | ||
yield this.$$localPersistence.putDeferred(deferredArray.concat(...toDeffer)); | ||
this.deferredPresent = true; | ||
if (this.$$remotePersistence) { | ||
yield this.$$localPersistence.putDeferred(deferredArray.concat(...toDeffer)); | ||
this.deferredPresent = true; | ||
} | ||
this.onSyncEnd(); | ||
@@ -234,2 +231,3 @@ }); | ||
let deferredArray = yield this.$$localPersistence.getDeferred(); | ||
let conflicts = 0; | ||
if (localVersion === remoteVersion && deferredArray.length === 0) { | ||
@@ -245,4 +243,3 @@ return { | ||
var _a; | ||
let item = this.$$deserialize(x.data); | ||
const conflict = remoteUpdates.rows.findIndex((y) => y.id === item.id); | ||
const conflict = remoteUpdates.rows.findIndex((y) => y.id === x.id); | ||
// take row-specific version if available, otherwise rely on latest version | ||
@@ -256,2 +253,3 @@ const comparison = Number(((_a = remoteUpdates.rows[conflict]) === null || _a === void 0 ? void 0 : _a.ts) || remoteVersion); | ||
remoteUpdates.rows.splice(conflict, 1); | ||
conflicts++; | ||
return true; | ||
@@ -261,2 +259,3 @@ } | ||
// there's a conflict, and the remote change is newer | ||
conflicts++; | ||
return false; | ||
@@ -270,5 +269,4 @@ } | ||
const updatedRows = new Map(); | ||
for (const local of deferredArray) { | ||
let item = this.$$deserialize(local.data); | ||
updatedRows.set(item.id, local.data); | ||
for (const d of deferredArray) { | ||
updatedRows.set(d.id, yield this.$$localPersistence.getOne(d.id)); | ||
// latest deferred write wins since it would overwrite the previous one | ||
@@ -296,3 +294,3 @@ } | ||
let pulled = remoteUpdates.rows.length; | ||
return { pushed, pulled }; | ||
return { pushed, pulled, conflicts }; | ||
} | ||
@@ -328,2 +326,33 @@ catch (e) { | ||
} | ||
backup() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.$$localPersistence) { | ||
throw new Error("Local persistence not available"); | ||
} | ||
return JSON.stringify(yield this.$$localPersistence.dump()); | ||
}); | ||
} | ||
restoreBackup(input) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.$$remotePersistence) { | ||
yield this.$$remotePersistence.checkOnline(); | ||
if (!this.$$remotePersistence.isOnline) { | ||
throw new Error("Can not restore backup when the client is offline!"); | ||
} | ||
} | ||
const dump = JSON.parse(input); | ||
if (!this.$$localPersistence) { | ||
throw new Error("Local persistence not available"); | ||
} | ||
yield this.$$localPersistence.put(dump.data); | ||
yield this.$$localPersistence.putDeferred(dump.metadata.deferred); | ||
yield this.$$localPersistence.putVersion(dump.metadata.version); | ||
yield this.$$loadFromLocal(); | ||
if (this.$$remotePersistence) { | ||
yield this.$$remotePersistence.put(dump.data); | ||
return yield this.sync(); // to get latest version number | ||
} | ||
return []; | ||
}); | ||
} | ||
/** | ||
@@ -338,2 +367,8 @@ * Public methods, to be used by the application | ||
} | ||
/** | ||
* List of all items in the store (including deleted items) However, the list is not observable | ||
*/ | ||
get copy() { | ||
return this.$$observableObject.copy; | ||
} | ||
getByID(id) { | ||
@@ -348,3 +383,3 @@ return this.$$observableObject.target.find((x) => x.id === id); | ||
} | ||
restore(id) { | ||
restoreItem(id) { | ||
const item = this.$$observableObject.target.find((x) => x.id === id); | ||
@@ -385,2 +420,12 @@ if (!item) { | ||
} | ||
updateByID(id, item) { | ||
const index = this.$$observableObject.target.findIndex((x) => x.id === id); | ||
if (index === -1) { | ||
throw new Error("Item not found."); | ||
} | ||
if (this.$$observableObject.target[index].id !== item.id) { | ||
throw new Error("ID mismatch."); | ||
} | ||
this.updateByIndex(index, item); | ||
} | ||
isUpdated() { | ||
@@ -406,2 +451,7 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
get isOnline() { | ||
if (!this.$$remotePersistence) | ||
return false; | ||
return this.$$remotePersistence.isOnline; | ||
} | ||
} |
{ | ||
"name": "apical-store", | ||
"version": "0.0.82", | ||
"version": "0.0.83", | ||
"description": "reactive-syncable-datastore", | ||
@@ -5,0 +5,0 @@ "main": "dist/bundle.js", |
import { Persistence } from "./type"; | ||
export interface RemotePersistence extends Persistence { | ||
getSince(version?: number): Promise<{version: number, rows: { | ||
id: string, | ||
data: string, | ||
ts?: string | ||
}[]}> | ||
getSince(version?: number): Promise<{ | ||
version: number; | ||
rows: { | ||
id: string; | ||
data: string; | ||
ts?: string; | ||
}[]; | ||
}>; | ||
isOnline: boolean; | ||
checkOnline: () => Promise<void>; | ||
} | ||
export class CloudFlareApexoDB implements Persistence { | ||
export class CloudFlareApexoDB implements RemotePersistence { | ||
private baseUrl: string; | ||
private token: string; | ||
private table: string; | ||
isOnline: boolean = true; | ||
@@ -28,4 +35,24 @@ constructor({ | ||
this.table = name; | ||
this.checkOnline(); | ||
} | ||
async checkOnline() { | ||
try { | ||
await fetch(this.baseUrl, { | ||
method: "HEAD", | ||
}); | ||
this.isOnline = true; | ||
} catch (e) { | ||
this.isOnline = false; | ||
this.retryConnection(); | ||
} | ||
} | ||
retryConnection() { | ||
let i = setInterval(() => { | ||
if (this.isOnline) clearInterval(i); | ||
else this.checkOnline(); | ||
}, 5000); | ||
} | ||
async getSince(version: number = 0) { | ||
@@ -38,12 +65,29 @@ let page = 0; | ||
const url = `${this.baseUrl}/${this.table}/${version}/${page}`; | ||
const response = await fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = await response.json(); | ||
let res: { success: boolean; output: string }; | ||
try { | ||
const response = await fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = await response.json(); | ||
} catch (e: any) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
break; | ||
} | ||
if (res.success === false) { | ||
result = []; | ||
version = 0; | ||
break; | ||
} | ||
const output = JSON.parse(res.output) as { | ||
version: number; | ||
rows: { id: string; data: string; ts?:string }[]; | ||
rows: { id: string; data: string; ts?: string }[]; | ||
}; | ||
@@ -60,9 +104,20 @@ nextPage = output.rows.length > 0 && version !== 0; | ||
const url = `${this.baseUrl}/${this.table}/0/Infinity`; | ||
const response = await fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
const res = await response.json(); | ||
let res: { success: boolean; output: string }; | ||
try { | ||
const response = await fetch(url, { | ||
method: "GET", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
}); | ||
res = await response.json(); | ||
} catch (e) { | ||
this.checkOnline(); | ||
res = { | ||
success: false, | ||
output: ``, | ||
}; | ||
} | ||
if (res.success) return Number(JSON.parse(res.output).version); | ||
@@ -78,11 +133,16 @@ else return 0; | ||
const url = `${this.baseUrl}/${this.table}`; | ||
const response = await fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
try { | ||
await fetch(url, { | ||
method: "PUT", | ||
headers: { | ||
Authorization: `Bearer ${this.token}`, | ||
}, | ||
body: JSON.stringify(reqBody), | ||
}); | ||
} catch (e) { | ||
this.checkOnline(); | ||
throw e; | ||
} | ||
return; | ||
} | ||
} |
@@ -8,3 +8,2 @@ import { Change, Observable } from "./observable"; | ||
export class Store<T extends Document> { | ||
public isOnline = true; | ||
public deferredPresent: boolean = false; | ||
@@ -238,2 +237,3 @@ public onSyncStart: () => void = () => {}; | ||
pulled?: number; | ||
conflicts?: number; | ||
exception?: string; | ||
@@ -260,2 +260,3 @@ }> { | ||
let deferredArray = await this.$$localPersistence.getDeferred(); | ||
let conflicts = 0; | ||
@@ -289,5 +290,7 @@ if (localVersion === remoteVersion && deferredArray.length === 0) { | ||
remoteUpdates.rows.splice(conflict, 1); | ||
conflicts++; | ||
return true; | ||
} else { | ||
// there's a conflict, and the remote change is newer | ||
conflicts++; | ||
return false; | ||
@@ -336,3 +339,3 @@ } | ||
let pulled = remoteUpdates.rows.length; | ||
return { pushed, pulled }; | ||
return { pushed, pulled, conflicts }; | ||
} catch (e) { | ||
@@ -372,3 +375,18 @@ console.error(e); | ||
async restoreBackup(input: string) { | ||
async restoreBackup( | ||
input: string | ||
): Promise< | ||
{ | ||
pushed?: number; | ||
pulled?: number; | ||
conflicts?: number; | ||
exception?: string; | ||
}[] | ||
> { | ||
if (this.$$remotePersistence) { | ||
await this.$$remotePersistence.checkOnline(); | ||
if (!this.$$remotePersistence.isOnline) { | ||
throw new Error("Can not restore backup when the client is offline!"); | ||
} | ||
} | ||
const dump = JSON.parse(input) as Dump; | ||
@@ -384,4 +402,5 @@ if (!this.$$localPersistence) { | ||
await this.$$remotePersistence.put(dump.data); | ||
await this.sync(); // to get latest version number | ||
return await this.sync(); // to get latest version number | ||
} | ||
return []; | ||
} | ||
@@ -495,2 +514,7 @@ | ||
} | ||
get isOnline() { | ||
if (!this.$$remotePersistence) return false; | ||
return this.$$remotePersistence.isOnline; | ||
} | ||
} |
@@ -18,2 +18,3 @@ import { Store } from "../src/store"; | ||
const token = "token"; | ||
let mf: Miniflare; | ||
@@ -32,3 +33,3 @@ beforeEach(async () => { | ||
const mf = new Miniflare({ | ||
mf = new Miniflare({ | ||
modules: true, | ||
@@ -585,3 +586,5 @@ scriptPath: `./worker.js`, | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.add({ id: "2", name: "john" }); | ||
@@ -607,3 +610,4 @@ await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
@@ -660,3 +664,5 @@ { | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.updateByIndex(0, { id: "1", name: "john" }); | ||
@@ -690,3 +696,4 @@ await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
@@ -766,3 +773,6 @@ { | ||
}); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
await store.sync(); | ||
{ | ||
@@ -835,6 +845,11 @@ const tries = await store.sync(); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.add({ id: "1", name: "alex" }); | ||
await new Promise((r) => setTimeout(r, 150)); | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
const version = Number( | ||
@@ -856,3 +871,2 @@ ( | ||
store.isOnline = true; | ||
{ | ||
@@ -907,6 +921,9 @@ const tries = await store.sync(); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.add({ id: "1", name: "alex" }); | ||
await new Promise((r) => setTimeout(r, 150)); | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
// this is more recent | ||
@@ -929,3 +946,2 @@ const version = Number( | ||
store.isOnline = true; | ||
{ | ||
@@ -984,4 +1000,5 @@ const tries = await store.sync(); | ||
store.isOnline = false; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
const version = Number( | ||
@@ -1003,8 +1020,10 @@ ( | ||
await new Promise((r) => setTimeout(r, 1300)); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
// this is more recent | ||
store.add({ id: "1", name: "alex" }); | ||
await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
{ | ||
@@ -1193,3 +1212,5 @@ const tries = await store.sync(); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.updateByIndex(0, { id: "1", name: "john" }); | ||
@@ -1226,4 +1247,4 @@ await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
{ | ||
@@ -1283,3 +1304,5 @@ const tries = await store.sync(); // now it should send the deferred changes | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.updateByIndex(0, { id: "1", name: "john" }); | ||
@@ -1313,3 +1336,4 @@ await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
@@ -1384,3 +1408,5 @@ store.updateByIndex(0, { id: "1", name: "mark" }); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
store.updateByIndex(0, { id: "1", name: "mathew" }); | ||
@@ -1417,3 +1443,4 @@ await new Promise((r) => setTimeout(r, 150)); | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
@@ -1539,3 +1566,5 @@ const keys = (await env.CACHE.list()).keys.map((x) => x.name); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
@@ -1611,3 +1640,3 @@ store.add({ id: "1", name: "alex" }); | ||
it("should restore deferred", async ()=>{ | ||
it("should restore deferred", async () => { | ||
{ | ||
@@ -1645,3 +1674,5 @@ // clearing local database before starting | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
await new Promise((r) => setTimeout(r, 100)); | ||
@@ -1651,3 +1682,2 @@ | ||
store.updateByID("2", { name: "ron", id: "2" }); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
@@ -1658,22 +1688,27 @@ expect(store.copy).toEqual([ | ||
]); | ||
const deferred = (await (store as any).$$localPersistence?.getDeferred()); | ||
const deferred = await (store as any).$$localPersistence?.getDeferred(); | ||
expect(store.deferredPresent).toBe(true); | ||
const backup = await store.backup(); | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
store.isOnline = true; | ||
await store.sync(); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual([]); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual( | ||
[] | ||
); | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
await expect(async () => await store.restoreBackup(backup)).to.rejects.toThrow(); | ||
await store.restoreBackup(backup); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual(deferred); | ||
global.fetch = mf.dispatchFetch as any; | ||
const sync = await store.restoreBackup(backup); | ||
expect(sync.length).toBe(2); | ||
expect(sync[0].conflicts).toBe(2); | ||
}); | ||
it("should restore and process deferred", async ()=>{ | ||
it("should restore and process deferred", async () => { | ||
{ | ||
@@ -1711,3 +1746,5 @@ // clearing local database before starting | ||
store.isOnline = false; | ||
global.fetch = () => { | ||
throw new Error("Mock connectivity error"); | ||
}; | ||
await new Promise((r) => setTimeout(r, 100)); | ||
@@ -1723,3 +1760,3 @@ | ||
]); | ||
const deferred = (await (store as any).$$localPersistence?.getDeferred()); | ||
const deferred = await (store as any).$$localPersistence?.getDeferred(); | ||
expect(store.deferredPresent).toBe(true); | ||
@@ -1729,15 +1766,20 @@ | ||
store.isOnline = true; | ||
global.fetch = mf.dispatchFetch as any; | ||
(store as any).$$remotePersistence.isOnline = true; | ||
await store.sync(); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual([]); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual( | ||
[] | ||
); | ||
await store.restoreBackup(backup); | ||
await new Promise((r) => setTimeout(r, 100)); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual([]); | ||
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual( | ||
[] | ||
); | ||
}); | ||
it("should get a new version after restore is completed", async ()=>{ | ||
it("should get a new version after restore is completed", async () => { | ||
{ | ||
@@ -1774,6 +1816,6 @@ // clearing local database before starting | ||
await new Promise((r) => setTimeout(r, 100)); | ||
await store.sync(); | ||
const version1 = (await (store as any).$$localPersistence?.getVersion()); | ||
const version2 = (await (store as any).$$remotePersistence?.getVersion()); | ||
const version1 = await (store as any).$$localPersistence?.getVersion(); | ||
const version2 = await (store as any).$$remotePersistence?.getVersion(); | ||
@@ -1784,10 +1826,9 @@ expect(version1).toBe(version2); | ||
await store.restoreBackup(backup); | ||
const version3 = (await (store as any).$$localPersistence?.getVersion()); | ||
const version4 = (await (store as any).$$remotePersistence?.getVersion()); | ||
const version3 = await (store as any).$$localPersistence?.getVersion(); | ||
const version4 = await (store as any).$$remotePersistence?.getVersion(); | ||
expect(version3).toBe(version4); | ||
expect(version3).greaterThan(version1); | ||
}); | ||
}); | ||
}); |
286059
7845
8