New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.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.6 to 0.0.7

src/observable/arr.ts

2

package.json
{
"name": "apical-store",
"version": "0.0.6",
"version": "0.0.7",
"description": "Mobx-Syncable-IndexedDB",

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

# Apical-Store
// TODO: check version in conflicts for each row
// TODO: check next page when fetching (are we?)
Example workflow
```typescript
import { Document, Store, SubDocument, mapSubModel, observe } from "apical-store";
import { Document, Store, SubDocument, mapSubModel, observe, CloudFlareApexoDB, IDB } from "apical-store";

@@ -24,7 +21,11 @@ class Department extends SubDocument {

const myStore = new Store<Employee>({
name: "my-store", // remote table name and local indexedDB
localPersistance: new IDB({
name: "my-database"
}),
remotePersistence: new CloudFlareApexoDB({
endpoint: "http://someurl",
token: "token",
name: "my-database",
}),
model: Employee,
persist: true,
endpoint: "http://api.myendpoint.com",
token: "my-token",
debounceRate: 1000,

@@ -35,3 +36,3 @@ encode: (data: any) => JSON.stringify(data),

@observe(myStore)
@observe([myStore])
class MyComponent extends React.Component {

@@ -38,0 +39,0 @@ render() {

export { Store } from "./store";
export { Document, SubDocument, mapSubModel } from "./model";
export { observe } from "./react";
export { observe } from "./react";
export { LocalPersistence, IDB, deferredArray } from "./persistence/local";
export { CloudFlareApexoDB } from "./persistence/remote";
import { Document } from "./model";
import { Store } from "./store";
/**
* Enhances a React component to automatically re-render when the observed store changes.
* @param store - An instance of Store that extends Document.
* @returns A higher-order function that takes a React component as an argument.
*/
export function observe<D extends Document, G extends Store<D>>(
store: G
store: G | G[]
): (component: any) => any {
return function (component: any) {
let oCDM = component.prototype.componentDidMount || (() => {});
const originalComponentDidMount =
component.prototype.componentDidMount || (() => {});
component.prototype.componentDidMount = function () {
let unObservers: (() => void)[] = [];
this.setState({});
const observer = () => this.setState({});
(store as any).$$observableObject.observe(observer);
unObservers.push(() => (store as any).$$observableObject.unobserve(observer));
const oCWU = this.componentWillUnmount || (() => {});
this.componentWillUnmount = () => {
unObservers.forEach((u) => u());
oCWU.call(this);
}
oCDM.call(this);
const unObservers: (() => void)[] = [];
this.setState({});
const observer = () => this.setState({});
if (Array.isArray(store)) {
store.forEach((singleStore) => {
// @ts-ignore
singleStore.$$observableObject.observe(observer);
unObservers.push(() =>
// @ts-ignore
singleStore.$$observableObject.unobserve(observer)
);
});
} else {
// @ts-ignore
store.$$observableObject.observe(observer);
// @ts-ignore
store.$$observableObject.unobserve(observer);
}
const originalComponentWillUnmount =
this.componentWillUnmount || (() => {});
this.componentWillUnmount = () => {
unObservers.forEach((unObserver) => unObserver());
originalComponentWillUnmount.call(this);
};
originalComponentDidMount.call(this);
};
return component;
return component;
};
}

@@ -1,10 +0,10 @@

import { Change, observable, ObservableArray } from "./observable";
import { IDB } from "./idb";
import { SyncService } from "./sync-service";
import { Change, Observable } from "./observable";
import { deferredArray, LocalPersistence } from "./persistence/local";
import { debounce } from "./debounce";
import { Document } from "./model";
import { RemotePersistence } from "./persistence/remote";
export type deferredArray = { ts: number; data: string }[];
export class Store<T extends Document> {
export class Store<
T extends Document,
> {
public isOnline = true;

@@ -14,7 +14,7 @@ public deferredPresent: boolean = false;

public onSyncEnd: () => void = () => {};
private $$idb: IDB | undefined;
private $$observableObject: ObservableArray<T[]> = observable([] as T[]);
private $$observableObject: Observable<T> = new Observable([] as T[]);
private $$changes: Change<T[]>[] = [];
private $$token: string | undefined;
private $$syncService: SyncService | null = null;
private $$localPersistence: LocalPersistence | undefined;
private $$remotePersistence: RemotePersistence | undefined;
private $$debounceRate: number = 100;

@@ -27,6 +27,2 @@ private $$lastProcessChanges: number = 0;

constructor({
name,
token,
persist = true,
endpoint,
debounceRate,

@@ -38,7 +34,5 @@ model,

onSyncEnd,
localPersistence,
remotePersistence,
}: {
name?: string;
token?: string;
persist?: boolean;
endpoint?: string;
debounceRate?: number;

@@ -50,3 +44,5 @@ model?: typeof Document;

onSyncEnd?: () => void;
}) {
localPersistence?: LocalPersistence;
remotePersistence?: RemotePersistence;
} = {}) {
this.$$model = model || Document;

@@ -68,14 +64,19 @@ if (onSyncStart) {

}
if (name && persist) {
this.$$idb = new IDB(name);
if (localPersistence) {
this.$$localPersistence = localPersistence;
this.$$loadFromLocal();
this.$$setupObservers();
}
if (token && endpoint && name && persist) {
this.$$token = token;
this.$$syncService = new SyncService(endpoint, this.$$token, name);
if (remotePersistence) {
this.$$remotePersistence = remotePersistence;
}
}
private $$serialize(item: T) {
/**
* Serializes an item of type T into an encoded JSON string.
* Date objects are converted to a custom format before encoding.
* @param item An instance of type T which extends Document.
* @returns An encoded JSON string representing the item.
*/
private $$serialize(item: T): string {
const stripped = item._stripDefaults ? item._stripDefaults() : item;

@@ -92,7 +93,12 @@ const str = JSON.stringify(stripped, function (key, value) {

private $$deserialize(line: string) {
/**
* Decodes a serialized string, parses it into a JavaScript object, and converts custom date formats back into Date objects.
* @param line A string representing the serialized data.
* @returns A new instance of the model with the deserialized data.
*/
private $$deserialize(line: string): any {
line = this.$$decode(line);
const item = JSON.parse(line, function (key, val) {
const item = JSON.parse(line, (key, val) => {
if (key === "$$date") return new Date(val);
let t = typeof val;
const t = typeof val;
if (t === "string" || t === "number" || t === "boolean" || val === null)

@@ -106,7 +112,15 @@ return val;

private async $$loadFromLocal() {
if (!this.$$idb) return;
const deserialized = (await this.$$idb.values()).map((x) =>
this.$$deserialize(x)
) as T[];
/**
* Loads data from an IndexedDB instance, deserializes it, and updates the observable array silently without triggering observers.
*/
private async $$loadFromLocal(): Promise<void> {
// Check if IndexedDB instance is available
if (!this.$$localPersistence) return;
// Retrieve values from IndexedDB and deserialize them
const deserialized: T[] = await Promise.all(
(await this.$$localPersistence.getAll()).map((x) => this.$$deserialize(x))
);
// Update the observable array silently with deserialized data
this.$$observableObject.silently((o) => {

@@ -118,3 +132,3 @@ o.splice(0, o.length, ...deserialized);

private async $$processChanges() {
if (!this.$$idb) return;
if (!this.$$localPersistence) return;
if (this.$$changes.length === 0) return;

@@ -124,4 +138,3 @@ this.onSyncStart();

const toWriteLocally: [string, string][] = [];
const toSendRemotely: { [key: string]: string } = {};
const toWrite: [string, string][] = [];
const toDeffer: deferredArray = [];

@@ -136,4 +149,3 @@ const changesToProcess = [...this.$$changes]; // Create a copy of changes to process

const serializedLine = this.$$serialize(item);
toWriteLocally.push([item.id, serializedLine]);
toSendRemotely[item.id] = serializedLine;
toWrite.push([item.id, serializedLine]);
toDeffer.push({

@@ -144,9 +156,12 @@ ts: Date.now(),

}
await this.$$idb.setBulk(toWriteLocally);
const deferred = (await this.$$idb.getMetadata("deferred")) || "[]";
let deferredArray = JSON.parse(deferred) as deferredArray;
if (this.isOnline && this.$$syncService && deferredArray.length === 0) {
await this.$$localPersistence.put(toWrite);
let deferredArray = await this.$$localPersistence.getDeferred();
if (
this.isOnline &&
this.$$remotePersistence &&
deferredArray.length === 0
) {
try {
await this.$$syncService.sendUpdates(toSendRemotely);
await this.$$remotePersistence.put(toWrite);
this.onSyncEnd();

@@ -166,4 +181,5 @@ return;

*/
deferredArray = deferredArray.concat(...toDeffer);
await this.$$idb.setMetadata("deferred", JSON.stringify(deferredArray));
await this.$$localPersistence.putDeferred(
deferredArray.concat(...toDeffer)
);
this.deferredPresent = true;

@@ -197,7 +213,2 @@ this.onSyncEnd();

private async $$localVersion() {
if (!this.$$idb) return 0;
return Number((await this.$$idb.getMetadata("version")) || 0);
}
/**

@@ -240,10 +251,10 @@ *

}> {
if (!this.$$idb) {
if (!this.$$localPersistence) {
return {
exception: "IDB not available",
exception: "Local persistence not available",
};
}
if (!this.$$syncService) {
if (!this.$$remotePersistence) {
return {
exception: "Sync service not available",
exception: "Remote persistence not available",
};

@@ -257,6 +268,5 @@ }

try {
const localVersion = await this.$$localVersion();
const remoteVersion = await this.$$syncService.latestVersion();
const deferred = (await this.$$idb.getMetadata("deferred")) || "[]";
let deferredArray = JSON.parse(deferred) as deferredArray;
const localVersion = await this.$$localPersistence.getVersion();
const remoteVersion = await this.$$remotePersistence.getVersion();
let deferredArray = await this.$$localPersistence.getDeferred();

@@ -270,3 +280,5 @@ if (localVersion === remoteVersion && deferredArray.length === 0) {

// fetch updates since our local version
const remoteUpdates = await this.$$syncService.fetchData(localVersion);
const remoteUpdates = await this.$$remotePersistence.getSince(
localVersion
);

@@ -277,2 +289,3 @@ // check for conflicts

const conflict = remoteUpdates.rows.findIndex((y) => y.id === item.id);
// take row-specific version if available, otherwise rely on latest version
const comparison = Number(

@@ -299,21 +312,25 @@ (

// we should start with remote
for (const remote of remoteUpdates.rows) {
await this.$$idb.set(remote.id, remote.data);
}
await this.$$localPersistence.put(
remoteUpdates.rows.map((row) => [row.id, row.data])
);
// then local
const updatedRows: { [key: string]: string } = {};
const updatedRows = new Map();
for (const local of deferredArray) {
let item = this.$$deserialize(local.data);
updatedRows[item.id] = local.data;
updatedRows.set(item.id, local.data);
// latest deferred write wins since it would overwrite the previous one
}
await this.$$syncService.sendUpdates(updatedRows);
await this.$$remotePersistence.put(
[...updatedRows.keys()].map((x) => [x, updatedRows.get(x)])
);
// reset deferred
await this.$$idb.setMetadata("deferred", "[]");
await this.$$localPersistence.putDeferred([]);
this.deferredPresent = false;
// set local version
await this.$$idb.setMetadata("version", remoteUpdates.version.toString());
// set local version to the version given by the current request
// this might be outdated as soon as this functions ends
// that's why this function will run on a while loop (below)
await this.$$localPersistence.putVersion(remoteUpdates.version);

@@ -366,18 +383,20 @@ // but if we had deferred updates then the remoteUpdates.version is outdated

get list() {
return this.$$observableObject.observable.filter((x) => !x.$$deleted);
return this.$$observableObject.target.filter((x) => !x.$$deleted);
}
copy = this.$$observableObject.copy;
getByID(id: string) {
return this.$$observableObject.observable.find((x) => x.id === id);
return this.$$observableObject.target.find((x) => x.id === id);
}
add(item: T) {
if (this.$$observableObject.observable.find((x) => x.id === item.id)) {
if (this.$$observableObject.target.find((x) => x.id === item.id)) {
throw new Error("Duplicate ID detected: " + JSON.stringify(item.id));
}
this.$$observableObject.observable.push(item);
this.$$observableObject.target.push(item);
}
delete(item: T) {
const index = this.$$observableObject.observable.findIndex(
const index = this.$$observableObject.target.findIndex(
(x) => x.id === item.id

@@ -392,12 +411,10 @@ );

deleteByIndex(index: number) {
if (!this.$$observableObject.observable[index]) {
if (!this.$$observableObject.target[index]) {
throw new Error("Item not found.");
}
this.$$observableObject.observable[index].$$deleted = true;
this.$$observableObject.target[index].$$deleted = true;
}
deleteByID(id: string) {
const index = this.$$observableObject.observable.findIndex(
(x) => x.id === id
);
const index = this.$$observableObject.target.findIndex((x) => x.id === id);
if (index === -1) {

@@ -410,9 +427,9 @@ throw new Error("Item not found.");

updateByIndex(index: number, item: T) {
if (!this.$$observableObject.observable[index]) {
if (!this.$$observableObject.target[index]) {
throw new Error("Item not found.");
}
if (this.$$observableObject.observable[index].id !== item.id) {
if (this.$$observableObject.target[index].id !== item.id) {
throw new Error("ID mismatch.");
}
this.$$observableObject.observable[index] = item;
this.$$observableObject.target[index] = item;
}

@@ -423,7 +440,9 @@

async isUpdated() {
return this.$$syncService
? (await this.$$syncService.latestVersion()) ===
(await this.$$localVersion())
: true;
if (this.$$localPersistence && this.$$remotePersistence) {
return (
(await this.$$localPersistence.getVersion()) ===
(await this.$$remotePersistence.getVersion())
);
} else return false;
}
}
}

@@ -1,89 +0,91 @@

import { IDB } from '../src/idb';
import { describe, test, expect } from 'vitest';
import { deferredArray, IDB } from '../src/persistence/local';
import "fake-indexeddb/auto";
describe('IDB', () => {
test('get', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
const result = await idb.get('key1');
expect(result).toBe('value1');
});
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
test('getBulk', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
await idb.set('key2', 'value2');
const result = await idb.getBulk(['key1', 'key2']);
expect(result).toEqual(['value1', 'value2']);
});
describe('IDB Class', () => {
const dbName = 'testDB';
let idb: IDB;
test('set', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
const result = await idb.get('key1');
expect(result).toBe('value1');
});
beforeEach(async () => {
idb = new IDB({ name: dbName });
});
test('setBulk', async () => {
const idb = new IDB('testDB');
await idb.setBulk([['key1', 'value1'], ['key2', 'value2']]);
const result1 = await idb.get('key1');
const result2 = await idb.get('key2');
expect(result1).toBe('value1');
expect(result2).toBe('value2');
});
afterEach(async () => {
await idb.clear();
await idb.clearMetadata()
});
test('delBulk', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
await idb.set('key2', 'value2');
await idb.delBulk(['key1', 'key2']);
const result1 = await idb.get('key1');
const result2 = await idb.get('key2');
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
it('should initialize the database and object stores', async () => {
// Simulate the request to ensure the database and object stores are created
const request = indexedDB.open(dbName);
const result = await new Promise<IDBDatabase>((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
test('clear', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
await idb.set('key2', 'value2');
await idb.clear();
const result1 = await idb.get('key1');
const result2 = await idb.get('key2');
expect(result1).toBeUndefined();
expect(result2).toBeUndefined();
});
expect(result.objectStoreNames.contains(dbName)).toBe(true);
expect(result.objectStoreNames.contains('metadata')).toBe(true);
});
test('keys', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
await idb.set('key2', 'value2');
const result = await idb.keys();
expect(result).toEqual(['key1', 'key2']);
});
it('should store and retrieve multiple entries', async () => {
const entries = [['key1', 'value1'], ['key2', 'value2']] as [string, string][];
await idb.put(entries);
test('values', async () => {
const idb = new IDB('testDB');
await idb.set('key1', 'value1');
await idb.set('key2', 'value2');
const result = await idb.values();
expect(result).toEqual(['value1', 'value2']);
});
const allEntries = await idb.getAll();
expect(allEntries).toContain('value1');
expect(allEntries).toContain('value2');
});
test('setMetadata', async () => {
const idb = new IDB('testDB');
await idb.setMetadata('metadata1', 'value1');
const result = await idb.getMetadata('metadata1');
expect(result).toBe('value1');
});
it('should store and retrieve metadata', async () => {
await idb.setMetadata('testKey', 'testValue');
const value = await idb.getMetadata('testKey');
test('getMetadata', async () => {
const idb = new IDB('testDB');
await idb.setMetadata('metadata1', 'value1');
const result = await idb.getMetadata('metadata1');
expect(result).toBe('value1');
});
});
expect(value).toBe('testValue');
});
it('should store and retrieve version', async () => {
await idb.putVersion(1);
const version = await idb.getVersion();
expect(version).toBe(1);
});
it('should store and retrieve deferred array', async () => {
const deferredArray: deferredArray = [{ data: "data", ts: 12 }, {data: "data2", ts: 24}];
await idb.putDeferred(deferredArray);
const retrievedArray = await idb.getDeferred();
expect(retrievedArray).toEqual(deferredArray);
});
it('should clear all entries', async () => {
const entries = [['key1', 'value1'], ['key2', 'value2']] as [string, string][];
await idb.put(entries);
await idb.clear();
const allEntries = await idb.getAll();
expect(allEntries.length).toBe(0);
});
it('should clear metadata', async () => {
await idb.setMetadata('testKey', 'testValue');
await idb.clearMetadata();
const value = await idb.getMetadata('testKey');
expect(value).toBeUndefined();
});
it('should handle concurrent transactions', async () => {
const entries1 = [['key1', 'value1']] as [string, string][];
const entries2 = [['key2', 'value2']] as [string, string][];
await Promise.all([idb.put(entries1), idb.put(entries2)]);
const allEntries = await idb.getAll();
expect(allEntries).toContain('value1');
expect(allEntries).toContain('value2');
});
});

@@ -1,118 +0,330 @@

import { describe, test, it, expect } from "vitest";
import {
observable,
isObservable,
Change,
ObservableArray,
} from "../src/observable";
import { describe, test, it, expect, vi } from "vitest";
import { Change, Observable } from "../src/observable";
describe("observable", () => {
test("should create an observable array", () => {
const arr = [1, 2, 3];
const { observable: obsArr } = observable(arr);
expect(isObservable(obsArr)).toBe(true);
describe("initialization", () => {
it("should initialize correctly with a regular array", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
expect(Observable.isObservable(observableArray.target)).toBe(
true
);
expect(JSON.stringify(observableArray.target)).toEqual(
JSON.stringify(arr)
);
});
it("should initialize correctly with an observable array", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
expect(Observable.isObservable(observableArray.target)).toBe(
true
);
expect(JSON.stringify(observableArray.target)).toEqual(
JSON.stringify(arr)
);
});
it("should maintain array methods and properties when adding elements", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
observableArray.target.push(4);
expect(observableArray.target.length).toBe(4);
expect(observableArray.target.includes(2)).toBe(true);
});
});
test("should observe changes in the array", async () => {
const arr = [1, 2, 3];
const { observable: obsArr, observe } = observable(arr);
let changes: Change<number[]>[] = [];
describe("isObservable", () => {
it("should identify non-observable array", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
expect(Observable.isObservable(arr)).toBe(false);
});
});
observe((c) => {
changes = c;
describe("observe", () => {
it("should add an observer successfully when calling observe method", () => {
const observer = (changes) => console.info(changes);
const observableArray = new Observable([]);
observableArray.observe(observer);
expect(observableArray.observers).toContain(observer);
});
test("should observe changes in the array", async () => {
const arr = [1, 2, 3];
const o = new Observable(arr);
let changes: Change<number[]>[] = [];
obsArr.push(4);
await new Promise((r) => setTimeout(r, 0));
o.observe((c) => {
changes = c;
});
expect(changes.length).toBe(1);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([3]);
expect(changes[0].value).toBe(4);
});
o.target.push(4);
await new Promise((r) => setTimeout(r, 100));
test("should unobserve changes in the array", async () => {
const arr = [1, 2, 3];
const { observable: obsArr, observe, unobserve } = observable(arr);
let changes: Change<number[]>[] = [];
expect(changes.length).toBe(1);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([3]);
expect(changes[0].value).toBe(4);
});
const observer = (c: Change<number[]>[]) => {
changes = c;
};
test("should observe multiple changes in the array", async () => {
const arr = [1, 2, 3];
const o = new Observable(arr);
let changes: Change<number[]>[] = [];
observe(observer);
o.observe((c) => {
changes = c;
});
obsArr.push(4);
o.target.push(4);
o.target.pop();
o.target.unshift(0);
await new Promise((r) => setTimeout(r, 0));
await new Promise((r) => setTimeout(r, 0));
expect(changes.length).toBe(3);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([3]);
expect(changes[0].value).toBe(4);
expect(changes[1].type).toBe("delete");
expect(changes[1].path).toEqual([3]);
expect(changes[1].oldValue).toBe(4);
expect(changes[2].type).toBe("insert");
expect(changes[2].path).toEqual([0]);
expect(changes[2].value).toBe(0);
});
expect(changes.length).toBe(1);
test("should handle array modifications inside a nested array", async () => {
const arr = [
[1, 2],
[3, 4],
];
const o = new Observable(arr);
let changes: Change<number[][]>[] = [];
await unobserve(observer);
o.observe((c) => {
changes = c;
});
obsArr.push(5);
o.target[0].push(5);
await new Promise((r) => setTimeout(r, 0));
expect(changes.length).toBe(1);
expect(changes.length).toBe(1);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([0, 2]);
expect(changes[0].value).toBe(5);
});
});
test("should silently modify the array without notifying observers", () => {
const arr = [1, 2, 3];
const { observable: obsArr, observe, silently } = observable(arr);
let changes: Change<number[]>[] = [];
describe("unobserve", () => {
test("should unobserve changes in the array", async () => {
const arr = [1, 2, 3];
const o = new Observable(arr);
let changes: Change<number[]>[] = [];
observe((c) => {
changes = c;
const observer = (c: Change<number[]>[]) => {
changes = c;
};
o.observe(observer);
o.target.push(4);
await new Promise((r) => setTimeout(r, 0));
expect(changes.length).toBe(1);
o.unobserve(observer);
o.target.push(5);
expect(changes.length).toBe(1);
});
silently((o) => {
o.push(4);
o.pop();
it("should handle removing non-existent observers gracefully", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
const observer = (changes) => {};
observableArray.observe(() => {});
observableArray.unobserve(observer); // Trying to unobserve a non-existent observer
expect(observableArray.observers.length).toBe(1);
});
expect(changes.length).toBe(0);
});
it("should remove a specific observer when unobserve is called with that observer", () => {
const observer1 = (changes) => console.info("Observer 1:", changes);
const observer2 = (changes) => console.info("Observer 2:", changes);
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
test("should observe multiple changes in the array", async () => {
const arr = [1, 2, 3];
const { observable: obsArr, observe } = observable(arr);
let changes: Change<number[]>[] = [];
observableArray.observe(observer1);
observableArray.observe(observer2);
expect(observableArray.observers.length).toBe(2);
observe((c) => {
changes = c;
observableArray.unobserve(observer1);
expect(observableArray.observers.length).toBe(1);
expect(observableArray.observers[0]).toBe(observer2);
});
obsArr.push(4);
obsArr.pop();
obsArr.unshift(0);
await new Promise((r) => setTimeout(r, 0));
it("should remove all observers when no argument is provided", () => {
const observer1 = (changes) => console.info("Observer 1 called");
const observer2 = (changes) => console.info("Observer 2 called");
const observableArray = new Observable([1, 2, 3]);
observableArray.observe(observer1);
observableArray.observe(observer2);
expect(changes.length).toBe(3);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([3]);
expect(changes[0].value).toBe(4);
expect(changes[1].type).toBe("delete");
expect(changes[1].path).toEqual([3]);
expect(changes[1].oldValue).toBe(4);
expect(changes[2].type).toBe("insert");
expect(changes[2].path).toEqual([0]);
expect(changes[2].value).toBe(0);
observableArray.unobserve();
expect(observableArray.observers).toEqual([]);
});
it("should not alter observers list if observer is not found", () => {
const observer = (changes: Change<number[]>[]) => {};
const observableArray = new Observable<number>([]);
observableArray.observe(observer);
const result = observableArray.unobserve(
(changes: Change<number[]>[]) => {}
);
expect(result).toEqual([]);
});
it("should return removed observers", () => {
const observer = (changes: Change<number[]>[]) => {};
const observableArray = new Observable([1, 2, 3]);
observableArray.observe(observer);
const result = observableArray.unobserve();
expect(result).toEqual([observer]);
});
it("should return an empty array when no observers are removed", () => {
const observer = (changes: Change<number[]>[]) => {};
const observableArray = new Observable([1, 2, 3]);
observableArray.observe(observer);
const result = observableArray.unobserve([]);
expect(result).toEqual([]);
});
});
test("should handle array modifications inside a nested array", async () => {
const arr = [[1, 2], [3, 4]];
const { observable: obsArr, observe } = observable(arr);
let changes: Change<number[][]>[] = [];
describe("silently", () => {
test("should silently modify the array without notifying observers", () => {
const arr = [1, 2, 3];
const o = new Observable(arr);
let changes: Change<number[]>[] = [];
observe((c) => {
changes = c;
o.observe((c) => {
changes = c;
});
o.silently((o) => {
o.push(4);
o.pop();
});
expect(changes.length).toBe(0);
});
obsArr[0].push(5);
await new Promise((r) => setTimeout(r, 0));
it("should temporarily disable observers and re-enable them after execution", async () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
let observerCalled = false;
const observer = (changes: Change<number[]>[]) => {
observerCalled = true;
};
observableArray.observe(observer);
observableArray.silently((o) => {
o[0] = 10;
});
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(false);
expect(changes.length).toBe(1);
expect(changes[0].type).toBe("insert");
expect(changes[0].path).toEqual([0, 2]);
expect(changes[0].value).toBe(5);
observableArray.target.push(100);
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(true);
});
it("should temporarily disable observers and re-enable them after execution (deep)", async () => {
const arr = [{ numbers: [1] }, { numbers: [2] }, { numbers: [3] }];
const observableArray = new Observable(arr);
let observerCalled = false;
observableArray.observe((changes) => {
observerCalled = true;
});
observableArray.silently((o) => {
o[0].numbers[0] = 10;
});
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(false);
observableArray.target[2].numbers[0] = 30;
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(true);
});
it("should persist changes made during the work function", async () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
const observer = vi.fn();
observableArray.observe(observer);
observableArray.silently((o) => {
o[0] = 10;
o.push(4);
});
await new Promise((r) => setTimeout(r, 10));
expect(observer).toHaveBeenCalledTimes(0);
expect(observableArray.copy).toEqual([10, 2, 3, 4]);
});
it("should re-enable observers even if work function throws an exception", async () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
let observerCalled = false;
const observer = (changes: Change<number[]>[]) => {
observerCalled = true;
};
observableArray.observe(observer);
expect(Observable.isObservable(observableArray.target)).toBe(
true
);
expect(observableArray.copy).toEqual(arr);
try {
observableArray.silently((o) => {
throw new Error("Exception in work function");
});
} catch (e) {}
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(false);
observableArray.target[0] = 12;
await new Promise((r) => setTimeout(r, 10));
expect(observerCalled).toBe(true);
});
// Changes made before the exception should persist
it("should persist changes made before an exception is thrown during the work function execution", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
try {
observableArray.silently((o) => {
o[0] = 10;
throw new Error("Exception during work function");
});
} catch (e) {
// Exception thrown intentionally
}
expect(observableArray.target[0]).toBe(10);
});
it("should propagate exception when work function throws an error", () => {
const arr = [1, 2, 3];
const observableArray = new Observable(arr);
const error = new Error("Test Error");
expect(() => {
observableArray.silently(() => {
throw error;
});
}).toThrow(error);
});
});
});
import { Store } from "../src/store";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Miniflare } from "miniflare";
import "fake-indexeddb/auto";
import { D1Database, KVNamespace } from "@cloudflare/workers-types";
import { readFileSync, writeFileSync } from "fs";
import { IDB } from "../src/persistence/local";
import { CloudFlareApexoDB } from "../src/persistence/remote";
import "fake-indexeddb/auto";

@@ -19,6 +21,2 @@ describe("Store", () => {

store = new Store({
name: Math.random().toString(36).substring(7),
token: token,
persist: true,
endpoint: "http://example.com",
});

@@ -30,31 +28,5 @@

).replace(
`var Auth = class {
static async authenticate(token) {
try {
const response = await fetch("https://auth1.apexo.app", {
method: "PUT",
body: JSON.stringify({ operation: "jwt", token })
});
const result = await response.json();
if (!result.success) {
return { success: false };
}
const account = JSON.parse(atob(token)).payload.prefix;
return { success: true, account };
} catch (e) {
return { success: false };
}
}
};`,
`var Auth = class {
static async authenticate(token) {
try {
return { success: true, account: "ali" };
} catch (e) {
return { success: false };
}
}
}`
/const response(.|\n)*return \{ success: true, account \};/,
`return {success: true, account: "ali"}`
);
writeFileSync("./worker.js", workerFile);

@@ -70,2 +42,3 @@

env.DB = await mf.getD1Database("DB");
global.fetch = mf.dispatchFetch as any;

@@ -78,3 +51,2 @@ await env.DB.exec(

);
global.fetch = mf.dispatchFetch as any;
});

@@ -148,6 +120,10 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -168,3 +144,3 @@

expect(await (store as any).$$localVersion()).toBe(99);
expect(await (store as any).$$localPersistence.getVersion()).toBe(99);
});

@@ -184,6 +160,10 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -204,3 +184,3 @@

expect(await (store as any).$$localVersion()).toBe(123);
expect(await (store as any).$$localPersistence.getVersion()).toBe(123);

@@ -235,3 +215,3 @@ await env.DB.prepare(

expect(await (store as any).$$localVersion()).toBe(124);
expect(await (store as any).$$localPersistence.getVersion()).toBe(124);
});

@@ -243,15 +223,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -288,15 +276,23 @@ await new Promise((r) => setTimeout(r, 300));

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -330,15 +326,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -372,15 +376,23 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -418,3 +430,3 @@

expect(store.list[0].id).toBe("12");
expect(await (store as any).$$localVersion()).toBe(version);
expect(await (store as any).$$localPersistence.getVersion()).toBe(version);
});

@@ -426,15 +438,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -477,3 +497,3 @@ {

expect(store.list[0].name).toBe("alex2");
expect(await (store as any).$$localVersion()).toBe(version);
expect(await (store as any).$$localPersistence.getVersion()).toBe(version);
});

@@ -485,15 +505,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -534,5 +562,5 @@

expect((store as any).$$observableObject.observable.length).toBe(1);
expect((store as any).$$observableObject.target.length).toBe(1);
expect(store.list.length).toBe(0);
expect(await (store as any).$$localVersion()).toBe(version);
expect(await (store as any).$$localPersistence.getVersion()).toBe(version);
});

@@ -544,15 +572,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -610,15 +646,23 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -688,15 +732,23 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -720,2 +772,12 @@ {

it("should not sync if not online", async () => {
store = new Store({
remotePersistence: new CloudFlareApexoDB({
endpoint: "https://apexo-database.vercel.app",
token: "any",
name: "staff"
}),
localPersistence: new IDB({
name: "staff"
})
});
store.isOnline = false;

@@ -728,10 +790,28 @@ {

it("should not sync if sync service is not available", async () => {
(store as any).$$syncService = null;
it("should not sync if local persistence is not available", async () => {
store = new Store({
remotePersistence: new CloudFlareApexoDB({
endpoint: "https://apexo-database.vercel.app",
token: "any",
name: "staff"
})
});
{
const tries = await store.sync();
expect(tries[0].exception).toBe("Sync service not available");
expect(tries[0].exception).toBe("Local persistence not available");
}
});
it("should not sync if remote persistence is not available", async () => {
store = new Store({
localPersistence: new IDB({
name: "staff",
}),
});
{
const tries = await store.sync();
expect(tries[0].exception).toBe("Remote persistence not available");
}
});
it("should sync push (deferred) and pull at the same time", async () => {

@@ -741,15 +821,23 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
debounceRate: 1,

@@ -805,15 +893,23 @@ });

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -868,15 +964,23 @@ store.add({ id: "0", name: "ali" });

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -940,19 +1044,27 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
expect(await (store as any).$$localVersion()).toBe(0);
expect(await (store as any).$$syncService.latestVersion()).toBe(0);
expect(await (store as any).$$localPersistence.getVersion()).toBe(0);
expect(await (store as any).$$remotePersistence.getVersion()).toBe(0);
{

@@ -962,4 +1074,4 @@ const tries = await store.sync();

}
expect(await (store as any).$$localVersion()).toBe(0);
expect(await (store as any).$$syncService.latestVersion()).toBe(0);
expect(await (store as any).$$localPersistence.getVersion()).toBe(0);
expect(await (store as any).$$remotePersistence.getVersion()).toBe(0);
});

@@ -971,16 +1083,24 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
debounceRate: 1000,

@@ -992,21 +1112,21 @@ });

store.add({ id: "1", name: "alex" });
expect((await (store as any).$$idb.values()).length).toBe(1); // 1
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 1
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 2
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 2
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 3
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 3
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 4
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 4
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 5
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 5
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 6
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 6
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 7
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 7
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 8
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 8
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 9
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 9
await new Promise((r) => setTimeout(r, 150));
expect((await (store as any).$$idb.values()).length).toBe(2); // 10.5
expect((await (store as any).$$localPersistence.getAll()).length).toBe(2); // 10.5
});

@@ -1018,16 +1138,24 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
debounceRate: 500,

@@ -1039,11 +1167,11 @@ });

store.add({ id: "1", name: "alex" });
expect((await (store as any).$$idb.values()).length).toBe(1); // 1
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 1
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 2
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 2
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 3
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 3
await new Promise((r) => setTimeout(r, 100));
expect((await (store as any).$$idb.values()).length).toBe(1); // 4
expect((await (store as any).$$localPersistence.getAll()).length).toBe(1); // 4
await new Promise((r) => setTimeout(r, 150));
expect((await (store as any).$$idb.values()).length).toBe(2); // 5.5
expect((await (store as any).$$localPersistence.getAll()).length).toBe(2); // 5.5
});

@@ -1055,15 +1183,23 @@

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -1136,15 +1272,23 @@ {

store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
await (store as any).$$idb.clear();
await (store as any).$$idb.clearMetadata();
await (store as any).$$localPersistence.clear();
await (store as any).$$localPersistence.clearMetadata();
}
store = new Store({
name: "staff",
token: token,
persist: true,
endpoint: "http://example.com",
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});

@@ -1216,2 +1360,93 @@ {

});
it("Rely on the specific version of the row when it is 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({
remotePersistence: new CloudFlareApexoDB({
token,
endpoint: "https://apexo-database.vercel.app",
name: "staff",
}),
localPersistence: new IDB({
name: "staff",
}),
});
{
const tries = await store.sync();
expect(tries[0].exception).toBe("Nothing to sync");
}
store.add({ id: "1", name: "alex" });
await new Promise((r) => setTimeout(r, 300));
{
const tries = await store.sync();
expect(tries[0].pulled).toBe(1);
expect(tries[0].pushed).toBe(0);
expect(tries[1].exception).toBe("Nothing to sync");
}
store.isOnline = false;
store.updateByIndex(0, { id: "1", name: "mathew" });
await new Promise((r) => setTimeout(r, 300));
expect(store.deferredPresent).toBe(true);
await env.DB.exec(
`UPDATE staff SET data = '{"id":"1","name":"john"}' WHERE id = 1`
);
await env.DB.exec(
'INSERT INTO staff (id, account, data) VALUES (\'2\', \'ali\', \'{"id":"2","name":"ron"}\');'
);
const deferredVersion = Number(
JSON.parse(
await (store as any).$$localPersistence.getMetadata("deferred")
)[0].ts
);
const localVersion = Number(await (store as any).$$localPersistence.getVersion());
expect(deferredVersion).toBeGreaterThan(localVersion);
const remoteConflictVersion = (deferredVersion + localVersion) / 2;
await env.DB.exec(
`INSERT INTO staff_changes (version, account, ids) VALUES (${remoteConflictVersion}, 'ali', '1');`
);
await env.DB.exec(
`INSERT INTO staff_changes (version, account, ids) VALUES (${
deferredVersion + 1000
}, 'ali', '2');`
);
store.isOnline = true;
const keys = (await env.CACHE.list()).keys.map((x) => x.name);
for (let index = 0; index < keys.length; index++) {
const element = keys[index];
await env.CACHE.delete(element);
}
{
const tries = await store.sync();
expect(tries[0].pulled).toBe(1);
expect(tries[0].pushed).toBe(1); // deferred won
expect(tries[1].exception).toBe("Nothing to sync");
}
expect(JSON.stringify(store.list)).toBe(
`[{"id":"1","name":"mathew"},{"id":"2","name":"ron"}]`
);
});
});
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