This commit is contained in:
Martti Malmi 2023-08-29 15:16:26 +03:00
parent ad9a3158c4
commit 74a4e1261b
5 changed files with 69 additions and 21 deletions

View File

@ -21,11 +21,11 @@ export default class LocalForageAdapter extends Adapter {
return unsub;
}
set(path: string, data: NodeValue) {
async set(path: string, data: NodeValue) {
if (data === undefined) {
localForage.removeItem(path);
await localForage.removeItem(path);
} else {
localForage.setItem(path, data);
await localForage.setItem(path, data);
}
}
}

View File

@ -11,8 +11,8 @@ export default class MemoryAdapter extends Adapter {
return () => {};
}
set(path: string, value: NodeValue): void {
if (!value.updatedAt || !value.value) {
async set(path: string, value: NodeValue) {
if (value.updatedAt === undefined) {
throw new Error(`Invalid value: ${JSON.stringify(value)}`);
}
if (value === undefined) {

View File

@ -67,7 +67,7 @@ describe('Node', () => {
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('should trigger list callbacks when children are present', async () => {
it('should trigger map callbacks when children are present', async () => {
const node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
@ -99,4 +99,36 @@ describe('Node', () => {
// Should still have been called only twice
expect(mockCallback).toHaveBeenCalledTimes(2);
});
it('should return children when on() is called on a branch node', async () => {
const node = new Node({ id: 'settings', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
// save settings object
await node.put({
theme: 'dark',
fontSize: 14,
});
node.on(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
{
theme: 'dark',
fontSize: 14,
},
'settings',
expect.any(Number),
expect.any(Function),
);
node.get('theme').on(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
'dark',
'settings/theme',
expect.any(Number),
expect.any(Function),
);
});
});

View File

@ -43,26 +43,38 @@ export default class Node {
return new_node;
}
private async putLeaf(value: any, updatedAt: number) {
this.children = new Map();
const nodeValue: NodeValue = {
updatedAt,
value,
};
const promises = this.adapters.map((adapter) => adapter.set(this.id, nodeValue));
this.on_subscriptions.forEach((callback) => {
callback(value, this.id, updatedAt, () => {});
});
await Promise.all(promises);
}
private async putBranch(value: Record<string, any>, updatedAt: number) {
const promises = this.adapters.map((adapter) =>
adapter.set(this.id, { value: '__DIR__', updatedAt }),
);
const children = Object.keys(value);
const childPromises = children.map((key) => this.get(key).put(value[key], updatedAt));
await Promise.all([...promises, ...childPromises]);
}
/**
* Set a value to the node. If the value is an object, it will be converted to child nodes.
* @param value
* @example node.get('users').get('alice').put({name: 'Alice'})
*/
async put(value, updatedAt = Date.now()) {
async put(value: any, updatedAt = Date.now()) {
if (typeof value === 'object' && value !== null) {
this.adapters.forEach((adapter) => adapter.set(this.id, { value: '__DIR__', updatedAt }));
const children = Object.keys(value);
children.map((key) => this.get(key).put(value[key], updatedAt));
await this.putBranch(value, updatedAt);
} else {
this.children = new Map();
const nodeValue: NodeValue = {
updatedAt,
value,
};
this.adapters.forEach((adapter) => adapter.set(this.id, nodeValue));
this.on_subscriptions.forEach((callback) => {
callback(value, this.id, updatedAt, () => {});
});
await this.putLeaf(value, updatedAt);
}
}
@ -72,7 +84,11 @@ export default class Node {
*/
on(callback: Callback): Unsubscribe {
let latest: NodeValue | null = null;
// TODO handle case where it's a branch node
if (this.children.size > 0) {
// TODO handle branch node
} else {
// TODO handle leaf node
}
const cb = (value, path, updatedAt, unsubscribe) => {
if (latest === null || latest.updatedAt < value.updatedAt) {
latest = { value, updatedAt };

View File

@ -11,5 +11,5 @@ export type Callback = (
) => void;
export abstract class Adapter {
abstract get(path: string, callback: Callback): Unsubscribe;
abstract set(path: string, data: NodeValue): void;
abstract set(path: string, data: NodeValue): Promise<void>;
}