diff --git a/src/js/components/events/note/Content.tsx b/src/js/components/events/note/Content.tsx index 5cb73c09..3c052679 100644 --- a/src/js/components/events/note/Content.tsx +++ b/src/js/components/events/note/Content.tsx @@ -102,7 +102,7 @@ const Content = ({ standalone, isQuote, fullWidth, asInlineQuote, event, isPrevi 0}>
diff --git a/src/js/state/Node.test.ts b/src/js/state/Node.test.ts index ca4c295e..b402bfaf 100644 --- a/src/js/state/Node.test.ts +++ b/src/js/state/Node.test.ts @@ -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), + ); + }); }); }); diff --git a/src/js/state/Node.ts b/src/js/state/Node.ts index 568112a3..584ec8c6 100644 --- a/src/js/state/Node.ts +++ b/src/js/state/Node.ts @@ -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, 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 = {}; + 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());