node & node test

This commit is contained in:
Martti Malmi 2023-08-29 15:27:51 +03:00
parent 74a4e1261b
commit 1e3480899f
3 changed files with 149 additions and 119 deletions

View File

@ -102,7 +102,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, isPrevi
</Show>
<Show when={text?.length > 0}>
<div
className={`preformatted-wrap pb-1 ${emojiOnly && 'text-2xl'} ${
className={`preformatted-wrap pb-1 ${emojiOnly && 'text-3xl'} ${
fullWidth ? 'full-width-note' : ''
} ${asInlineQuote ? 'inline-quote' : ''}`}
>

View File

@ -6,129 +6,136 @@ import { Callback, Unsubscribe } from '@/state/types.ts';
import Node from './Node';
describe('Node', () => {
let node;
beforeEach(() => {
vi.resetAllMocks();
node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
});
it('should initialize with defaults', () => {
const node = new Node({ adapters: [] });
expect(node.id).toEqual('');
describe('new Node()', () => {
it('should initialize with defaults', () => {
const newNode = new Node({ adapters: [] });
expect(newNode.id).toEqual('');
});
});
it('should subscribe and unsubscribe with on()', () => {
const node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
describe('node.on()', () => {
it('should subscribe and unsubscribe with on()', () => {
const mockCallback: Callback = vi.fn();
const unsubscribe: Unsubscribe = node.on(mockCallback);
const unsubscribe: Unsubscribe = node.on(mockCallback);
expect(typeof unsubscribe).toBe('function');
node.put('someValue');
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
node.put('someValue');
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
unsubscribe();
node.put('someValue2');
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('should callback when subscribed after put()', () => {
const node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
node.put('someValue');
node.on(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
});
it('should trigger callback once when calling once()', async () => {
const node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
node.put('someValue');
const result = await node.once(mockCallback);
expect(result).toBe('someValue');
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
node.put('someValue2');
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('should trigger map callbacks when children are present', async () => {
const node = new Node({ id: 'test', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
// Adding children to the node
await node.get('child1').put('value1');
await node.get('child2').put('value2');
const unsubscribe: Unsubscribe = node.map(mockCallback);
// Should trigger for both child nodes
expect(mockCallback).toHaveBeenCalledWith(
'value1',
'test/child1',
expect.any(Number),
expect.any(Function),
);
expect(mockCallback).toHaveBeenCalledWith(
'value2',
'test/child2',
expect.any(Number),
expect.any(Function),
);
unsubscribe();
// Add another child to ensure the callback is not called after unsubscribe
await node.get('child3').put('value3');
// 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,
unsubscribe();
node.put('someValue2');
expect(mockCallback).toHaveBeenCalledTimes(1);
});
node.on(mockCallback);
it('should callback when subscribed after put()', () => {
const mockCallback: Callback = vi.fn();
node.put('someValue');
node.on(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
{
theme: 'dark',
fontSize: 14,
},
'settings',
expect.any(Number),
expect.any(Function),
);
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
});
});
node.get('theme').on(mockCallback);
describe('node.once()', () => {
it('should trigger callback once when calling once()', async () => {
const mockCallback: Callback = vi.fn();
node.put('someValue');
expect(mockCallback).toHaveBeenCalledWith(
'dark',
'settings/theme',
expect.any(Number),
expect.any(Function),
);
const result = await node.once(mockCallback);
expect(result).toBe('someValue');
expect(mockCallback).toHaveBeenCalledWith(
'someValue',
'test',
expect.any(Number),
expect.any(Function),
);
node.put('someValue2');
expect(mockCallback).toHaveBeenCalledTimes(1);
});
});
describe('node.map()', () => {
it('should trigger map callbacks when children are present', async () => {
const mockCallback: Callback = vi.fn();
await node.get('child1').put('value1');
await node.get('child2').put('value2');
const unsubscribe: Unsubscribe = node.map(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(
'value1',
'test/child1',
expect.any(Number),
expect.any(Function),
);
expect(mockCallback).toHaveBeenCalledWith(
'value2',
'test/child2',
expect.any(Number),
expect.any(Function),
);
unsubscribe();
await node.get('child3').put('value3');
expect(mockCallback).toHaveBeenCalledTimes(2);
});
it('should trigger map callbacks when a new child is added', async () => {
const node = new Node({ id: 'root', adapters: [new MemoryAdapter()] });
const mockCallback: Callback = vi.fn();
const unsubscribe = node.get('chats').map(mockCallback);
await node.get('chats').get('someChatId').put({ id: 'someChatId' });
expect(mockCallback).toHaveBeenCalledWith(
{ id: 'someChatId' },
'root/chats/someChatId',
expect.any(Number),
expect.any(Function),
);
unsubscribe();
});
});
describe('Branch node behavior', () => {
it('should return children when on() is called on a branch node', async () => {
const settingsNode = new Node({ id: 'settings', adapters: [new MemoryAdapter()] });
const mockCallback1: Callback = vi.fn();
const mockCallback2: Callback = vi.fn();
await settingsNode.put({ theme: 'dark', fontSize: 14 });
settingsNode.on(mockCallback1);
expect(mockCallback1).toHaveBeenCalledWith(
{ theme: 'dark', fontSize: 14 },
'settings',
expect.any(Number),
expect.any(Function),
);
settingsNode.get('theme').on(mockCallback2);
expect(mockCallback2).toHaveBeenCalledWith(
'dark',
'settings/theme',
expect.any(Number),
expect.any(Function),
);
});
});
});

View File

@ -12,6 +12,8 @@ type NodeProps = {
parent?: Node | null;
};
const DIR_VALUE = '__DIR__';
export default class Node {
id: string;
parent: Node | null;
@ -58,7 +60,7 @@ export default class Node {
private async putBranch(value: Record<string, any>, updatedAt: number) {
const promises = this.adapters.map((adapter) =>
adapter.set(this.id, { value: '__DIR__', updatedAt }),
adapter.set(this.id, { value: DIR_VALUE, updatedAt }),
);
const children = Object.keys(value);
const childPromises = children.map((key) => this.get(key).put(value[key], updatedAt));
@ -76,6 +78,12 @@ export default class Node {
} else {
await this.putLeaf(value, updatedAt);
}
if (this.parent) {
this.parent.map_subscriptions.forEach((callback) => {
callback(value, this.id, updatedAt, () => {});
});
}
}
/**
@ -84,20 +92,35 @@ export default class Node {
*/
on(callback: Callback): Unsubscribe {
let latest: NodeValue | null = null;
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) {
if (value !== DIR_VALUE && (latest === null || latest.updatedAt < value.updatedAt)) {
latest = { value, updatedAt };
callback(value, path, updatedAt, unsubscribe);
// TODO send to other adapters? or PubSub which decides where to send?
}
};
const subId = this.counter++;
this.on_subscriptions.set(subId, cb);
const adapterSubs = this.adapters.map((adapter) => adapter.get(this.id, cb));
if (this.children.size > 0) {
const aggregated: Record<string, any> = {};
const keys = Array.from(this.children.keys());
const total = keys.length;
let count = 0;
keys.forEach((key) => {
this.children.get(key)?.once((childValue) => {
aggregated[key] = childValue;
count++;
if (count === total) {
callback(aggregated, this.id, Date.now(), () => {});
}
});
});
}
const unsubscribe = () => {
this.on_subscriptions.delete(subId);
adapterSubs.forEach((unsub) => unsub());