Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

apical-store

Package Overview
Dependencies
Maintainers
0
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

apical-store - npm Package Compare versions

Comparing version 0.0.81 to 0.0.82

2

package.json
{
"name": "apical-store",
"version": "0.0.81",
"version": "0.0.82",
"description": "reactive-syncable-datastore",

@@ -5,0 +5,0 @@ "main": "dist/bundle.js",

@@ -18,7 +18,9 @@ import { ObservableMeta } from "./meta";

export function findGrandParent<T extends object>(
observable: ObservableMeta<T>
): ObservableMeta<T> {
if (observable.parent) return findGrandParent(observable.parent);
else return observable;
export function findGrandParent<T extends object>(observable: ObservableMeta<T>, visited = new Set()): ObservableMeta<T> {
if (visited.has(observable)) {
throw new Error("Circular reference detected in observable structure");
}
visited.add(observable);
if (observable.parent) return findGrandParent(observable.parent, visited);
else return observable;
}

@@ -25,0 +27,0 @@

import { Persistence } from "./type";
export type deferredArray = { ts: number; data: string }[];
export type deferredArray = { ts: number; id: string }[];
export interface Dump {
data: [string, string][];
metadata: {
version: number;
deferred: deferredArray;
};
}
export interface LocalPersistence extends Persistence {
getAll(): Promise<string[]>
getAll(): Promise<string[]>;
getOne(id: string): Promise<string>;
putVersion(number: number): Promise<void>;
getDeferred(): Promise<deferredArray>;
putDeferred(arr: deferredArray): Promise<void>;
dump(): Promise<Dump>;
}

@@ -21,3 +31,3 @@

constructor({name}:{name: string}) {
constructor({ name }: { name: string }) {
const request = indexedDB.open(name);

@@ -104,6 +114,12 @@ request.onupgradeneeded = function (event) {

}
return rows
return rows;
});
}
getOne(id: string): Promise<string> {
return this.store("readonly", (store) =>
this.pr(store.get(id) as IDBRequest<string>)
);
}
async getVersion() {

@@ -158,2 +174,28 @@ return Number((await this.getMetadata("version")) || 0);

}
dump() {
return this.store("readonly", async (store) => {
let data: [string, string][] = [];
if (store.getAll && store.getAllKeys) {
const keys: string[] = await this.pr(
store.getAllKeys() as IDBRequest<string[]>
);
const values: string[] = await this.pr(
store.getAll() as IDBRequest<string[]>
);
data = keys.map((key, index) => [key, values[index]]);
} else {
await this.eachCursor(store, (cursor) => {
data.push([cursor.key as string, cursor.value as string]);
});
}
return {
data,
metadata: {
version: await this.getVersion(),
deferred: await this.getDeferred(),
},
};
});
}
}
import { Change, Observable } from "./observable";
import { deferredArray, LocalPersistence } from "./persistence/local";
import { deferredArray, Dump, LocalPersistence } from "./persistence/local";
import { debounce } from "./debounce";

@@ -142,3 +142,3 @@ import { Document } from "./model";

ts: Date.now(),
data: serializedLine,
id: item.id,
});

@@ -170,6 +170,8 @@ }

*/
await this.$$localPersistence.putDeferred(
deferredArray.concat(...toDeffer)
);
this.deferredPresent = true;
if (this.$$remotePersistence) {
await this.$$localPersistence.putDeferred(
deferredArray.concat(...toDeffer)
);
this.deferredPresent = true;
}
this.onSyncEnd();

@@ -272,4 +274,3 @@ }

deferredArray = deferredArray.filter((x) => {
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

@@ -303,5 +304,4 @@ const comparison = Number(

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, await this.$$localPersistence.getOne(d.id));
// latest deferred write wins since it would overwrite the previous one

@@ -364,2 +364,24 @@ }

async backup() {
if (!this.$$localPersistence) {
throw new Error("Local persistence not available");
}
return JSON.stringify(await this.$$localPersistence.dump());
}
async restoreBackup(input: string) {
const dump = JSON.parse(input) as Dump;
if (!this.$$localPersistence) {
throw new Error("Local persistence not available");
}
await this.$$localPersistence.put(dump.data);
await this.$$localPersistence.putDeferred(dump.metadata.deferred);
await this.$$localPersistence.putVersion(dump.metadata.version);
await this.$$loadFromLocal();
if (this.$$remotePersistence) {
await this.$$remotePersistence.put(dump.data);
await this.sync(); // to get latest version number
}
}
/**

@@ -379,3 +401,5 @@ * Public methods, to be used by the application

*/
copy = this.$$observableObject.copy;
get copy() {
return this.$$observableObject.copy;
}

@@ -395,3 +419,3 @@ getByID(id: string) {

restore(id: string) {
restoreItem(id: string) {
const item = this.$$observableObject.target.find((x) => x.id === id);

@@ -439,2 +463,13 @@ if (!item) {

updateByID(id: string, item: T) {
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);
}
sync = debounce(this.$$sync.bind(this), this.$$debounceRate);

@@ -441,0 +476,0 @@

@@ -20,4 +20,3 @@ import { Store } from "../src/store";

beforeEach(async () => {
store = new Store({
});
store = new Store({});

@@ -237,3 +236,3 @@ let workerFile = readFileSync(

});
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -247,3 +246,3 @@ {

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -298,5 +297,5 @@ const rows = (await env.DB.prepare("SELECT * FROM staff").all()).results;

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.delete({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -348,5 +347,5 @@ const rows = (await env.DB.prepare("SELECT * FROM staff").all()).results;

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.updateByIndex(0, { id: "1", name: "john" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -458,3 +457,3 @@ const rows = (await env.DB.prepare("SELECT * FROM staff").all()).results;

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -525,3 +524,3 @@ const version = Number(

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -589,7 +588,7 @@ const version = Number(

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.isOnline = false;
store.add({ id: "2", name: "john" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -606,3 +605,3 @@ expect(store.list.length).toBe(2);

await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -664,7 +663,7 @@ expect(store.list.length).toBe(2);

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.isOnline = false;
store.updateByIndex(0, { id: "1", name: "john" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -685,3 +684,3 @@ expect(store.list.length).toBe(1);

await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -751,3 +750,3 @@ expect(store.list.length).toBe(1);

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -767,7 +766,7 @@ {

token: "any",
name: "staff"
name: "staff",
}),
localPersistence: new IDB({
name: "staff"
})
name: "staff",
}),
});

@@ -786,4 +785,4 @@ store.isOnline = false;

token: "any",
name: "staff"
})
name: "staff",
}),
});

@@ -845,3 +844,3 @@ {

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -916,3 +915,3 @@ const version = Number(

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -988,3 +987,3 @@ // this is more recent

}
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1012,3 +1011,3 @@ store.isOnline = false;

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.isOnline = true;

@@ -1197,10 +1196,10 @@

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.isOnline = false;
store.updateByIndex(0, { id: "1", name: "john" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.updateByIndex(0, { id: "1", name: "mark" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1221,3 +1220,3 @@ expect(store.list.length).toBe(1);

await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1287,7 +1286,7 @@ expect(store.list.length).toBe(1);

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));
store.isOnline = false;
store.updateByIndex(0, { id: "1", name: "john" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1308,3 +1307,3 @@ expect(store.list.length).toBe(1);

await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1323,3 +1322,3 @@ expect(store.list.length).toBe(1);

store.updateByIndex(0, { id: "1", name: "mark" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1382,3 +1381,3 @@ expect(

store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1394,3 +1393,3 @@ {

store.updateByIndex(0, { id: "1", name: "mathew" });
await new Promise((r) => setTimeout(r, 300));
await new Promise((r) => setTimeout(r, 150));

@@ -1410,3 +1409,5 @@ expect(store.deferredPresent).toBe(true);

);
const localVersion = Number(await (store as any).$$localPersistence.getVersion());
const localVersion = Number(
await (store as any).$$localPersistence.getVersion()
);
expect(deferredVersion).toBeGreaterThan(localVersion);

@@ -1443,2 +1444,348 @@ const remoteConflictVersion = (deferredVersion + localVersion) / 2;

});
describe("Backup and restore", () => {
it("should throw an error when local persistence is not available", async () => {
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store<{ name: string; id: string }>({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
});
(store as any).$$localPersistence = undefined;
await expect(() => store.backup()).rejects.toThrow(
"Local persistence not available"
);
});
it("should return a JSON string of the local persistence dump", async () => {
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store<{ name: string; id: string }>({
localPersistence: new IDB({
name: "staff",
}),
});
expect(await store.backup()).toBe(
`{"data":[],"metadata":{"version":0,"deferred":[]}}`
);
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
await new Promise((r) => setTimeout(r, 150));
expect(await store.backup()).toBe(
JSON.stringify({
data: [
["1", '{"id":"1","name":"alex"}'],
["2", '{"id":"2","name":"mark"}'],
],
metadata: {
version: 0,
deferred: [],
},
})
);
});
it("deferred updates should be present on backup dump", async () => {
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
store.add({ id: "0", name: "ali" });
await new Promise((r) => setTimeout(r, 100));
store.isOnline = false;
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
await new Promise((r) => setTimeout(r, 100));
const backup = JSON.parse(await store.backup());
expect(backup.data.length).toBe(3);
expect(backup.data[0][0]).toBe("0");
expect(backup.data[1][0]).toBe("1");
expect(backup.data[2][0]).toBe("2");
expect(backup.metadata.deferred.length).toBe(2);
expect(backup.metadata.deferred[0].id).toBe("1");
expect(backup.metadata.deferred[1].id).toBe("2");
});
it("should restore the local persistence from a JSON string", async () => {
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store<{ name: string; id: string }>({
localPersistence: new IDB({
name: "staff",
}),
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
});
await store.loaded;
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
await new Promise((r) => setTimeout(r, 150));
const backup = await store.backup();
store.updateByID("1", { name: "mathew", id: "1" });
store.updateByID("2", { name: "ron", id: "2" });
await new Promise((r) => setTimeout(r, 150));
expect(store.copy).toEqual([
{ id: "1", name: "mathew" },
{ id: "2", name: "ron" },
]);
await store.restoreBackup(backup);
await new Promise((r) => setTimeout(r, 150));
expect(store.copy).toEqual([
{ id: "1", name: "alex" },
{ id: "2", name: "mark" },
]);
});
it("should restore deferred", async ()=>{
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await store.loaded;
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
await new Promise((r) => setTimeout(r, 100));
store.isOnline = false;
await new Promise((r) => setTimeout(r, 100));
store.updateByID("1", { name: "mathew", id: "1" });
store.updateByID("2", { name: "ron", id: "2" });
await new Promise((r) => setTimeout(r, 100));
expect(store.copy).toEqual([
{ id: "1", name: "mathew" },
{ id: "2", name: "ron" },
]);
const deferred = (await (store as any).$$localPersistence?.getDeferred());
expect(store.deferredPresent).toBe(true);
const backup = await store.backup();
store.isOnline = true;
await store.sync();
await new Promise((r) => setTimeout(r, 100));
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual([]);
store.isOnline = false;
await store.restoreBackup(backup);
await new Promise((r) => setTimeout(r, 100));
expect(await (store as any).$$localPersistence?.getDeferred()).toEqual(deferred);
});
it("should restore and process deferred", async ()=>{
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await store.loaded;
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
await new Promise((r) => setTimeout(r, 100));
store.isOnline = false;
await new Promise((r) => setTimeout(r, 100));
store.updateByID("1", { name: "mathew", id: "1" });
store.updateByID("2", { name: "ron", id: "2" });
await new Promise((r) => setTimeout(r, 100));
expect(store.copy).toEqual([
{ id: "1", name: "mathew" },
{ id: "2", name: "ron" },
]);
const deferred = (await (store as any).$$localPersistence?.getDeferred());
expect(store.deferredPresent).toBe(true);
const backup = await store.backup();
store.isOnline = true;
await store.sync();
await new Promise((r) => setTimeout(r, 100));
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([]);
});
it("should get a new version after restore is completed", async ()=>{
{
// clearing local database before starting
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await store.loaded;
store.add({ id: "1", name: "alex" });
store.add({ id: "2", name: "mark" });
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());
expect(version1).toBe(version2);
const backup = await store.backup();
await store.restoreBackup(backup);
const version3 = (await (store as any).$$localPersistence?.getVersion());
const version4 = (await (store as any).$$remotePersistence?.getVersion());
expect(version3).toBe(version4);
expect(version3).greaterThan(version1);
});
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc