diff --git a/__tests__/debug/console-test.ts b/__tests__/debug/console.test.ts similarity index 100% rename from __tests__/debug/console-test.ts rename to __tests__/debug/console.test.ts diff --git a/__tests__/run/001-single-node-orchestrated.ts b/__tests__/e2e/001-single-node-orchestrated.test.ts similarity index 87% rename from __tests__/run/001-single-node-orchestrated.ts rename to __tests__/e2e/001-single-node-orchestrated.test.ts index a33d6d6..1b74f97 100644 --- a/__tests__/run/001-single-node-orchestrated.ts +++ b/__tests__/e2e/001-single-node-orchestrated.test.ts @@ -1,11 +1,11 @@ import { createOrchestrator, type NodeConfig } from '../../src/orchestration'; - +import type { NodeHandle } from '../../src/orchestration/types'; // Increase test timeout to 30 seconds jest.setTimeout(30000); describe('Run (Orchestrated)', () => { const orchestrator = createOrchestrator('in-memory'); - let nodeHandle: any; + let nodeHandle: NodeHandle; let apiUrl: string; beforeAll(async () => { @@ -14,7 +14,7 @@ describe('Run (Orchestrated)', () => { id: 'app-001', }; nodeHandle = await orchestrator.startNode(config); - apiUrl = nodeHandle.getApiUrl(); + apiUrl = nodeHandle.getApiUrl?.() || 'http://localhost:3000'; // Default URL if getApiUrl is not available }, 60000); // Increase timeout to 60s for this hook afterAll(async () => { diff --git a/__tests__/run/001-single-node.ts b/__tests__/e2e/001-single-node.test.ts similarity index 100% rename from __tests__/run/001-single-node.ts rename to __tests__/e2e/001-single-node.test.ts diff --git a/__tests__/run/002-two-nodes-orchestrated.ts b/__tests__/e2e/002-two-nodes-orchestrated.test.ts similarity index 100% rename from __tests__/run/002-two-nodes-orchestrated.ts rename to __tests__/e2e/002-two-nodes-orchestrated.test.ts diff --git a/__tests__/run/002-two-nodes.ts b/__tests__/e2e/002-two-nodes.test.ts similarity index 100% rename from __tests__/run/002-two-nodes.ts rename to __tests__/e2e/002-two-nodes.test.ts diff --git a/__tests__/run/005-docker-orchestrator.ts b/__tests__/e2e/005-docker-orchestrator.test.ts similarity index 94% rename from __tests__/run/005-docker-orchestrator.ts rename to __tests__/e2e/005-docker-orchestrator.test.ts index c2efab4..c485e96 100644 --- a/__tests__/run/005-docker-orchestrator.ts +++ b/__tests__/e2e/005-docker-orchestrator.test.ts @@ -1,16 +1,11 @@ import Docker from 'dockerode'; import { describe, it, beforeAll, afterAll, expect, jest } from '@jest/globals'; -import Debug from 'debug'; - -const debug = Debug('rz:test:docker-orchestrator-v2'); -import { createOrchestrator } from '../../src/orchestration'; -import type { NodeOrchestrator, NodeConfig, NodeHandle, NodeStatus } from '../../src/orchestration'; +import type { NodeConfig, NodeHandle, NodeStatus } from '../../src/orchestration'; +import { DockerOrchestrator, createOrchestrator } from '../../src/orchestration'; import { ImageManager } from '../../src/orchestration/docker-orchestrator/managers/image-manager'; +import Debug from 'debug'; +const debug = Debug('rz:test:docker-orchestrator-v2'); -// Extend the NodeOrchestrator type to include the docker client for DockerOrchestrator -interface DockerOrchestrator extends NodeOrchestrator { - docker: Docker; -} // Extended interface to include additional properties that might be present in the implementation interface ExtendedNodeStatus extends Omit { @@ -33,12 +28,8 @@ jest.setTimeout(300000); describe('Docker Orchestrator', () => { let docker: Docker; let orchestrator: DockerOrchestrator; - let node: NodeHandle | null = null; - let node2: NodeHandle | null = null; let nodeConfig: NodeConfig; - let node2Config: NodeConfig; let nodePort: number; - let node2Port: number; beforeAll(async () => { debug('Setting up Docker client and orchestrator...'); @@ -59,7 +50,10 @@ describe('Docker Orchestrator', () => { } // Initialize the orchestrator with the Docker client and test image - orchestrator = createOrchestrator('docker') as DockerOrchestrator; + orchestrator = createOrchestrator('docker', { + docker, + image: 'rhizome-node-test', + }) as DockerOrchestrator; debug('Docker orchestrator initialized'); // Create a basic node config for testing with unique network ID @@ -89,6 +83,9 @@ describe('Docker Orchestrator', () => { debug('All test cleanups completed'); }, 120000); // 2 minute timeout for afterAll + /** + * ! Note that this test fails if the build fails + */ it('should start and stop a node', async () => { debug('Starting test: should start and stop a node'); @@ -183,7 +180,7 @@ describe('Docker Orchestrator', () => { } // Get container info using ContainerManager - const container = await (orchestrator as any).containerManager.getContainer(status.containerId); + const container = await orchestrator.containerManager.getContainer(status.containerId); if (!container) { throw new Error('Container not found'); } @@ -358,11 +355,10 @@ describe('Docker Orchestrator', () => { // Start first node debug('Starting node 1...'); node1 = await orchestrator.startNode(node1Config); - debug(`Node 1 started with ID: ${node1.id}`); + debug(`Node 1 started with ID: ${node1.id} apiUrl: ${node1.getApiUrl?.()}`); // Get node 1's status and API URL const status1 = await node1.status() as ExtendedNodeStatus; - const node1ApiUrl = node1.getApiUrl?.(); // Update node 2's config with node 1's actual address if available if (status1.network?.address && node2Config.network) { @@ -373,11 +369,10 @@ describe('Docker Orchestrator', () => { // Start second node debug('Starting node 2...'); node2 = await orchestrator.startNode(node2Config); - debug(`Node 2 started with ID: ${node2.id}`); + debug(`Node 2 started with ID: ${node2.id} apiUrl: ${node2.getApiUrl?.()}`); // Get node 2's status const status2 = await node2.status() as ExtendedNodeStatus; - const node2ApiUrl = node2.getApiUrl?.(); // Verify both nodes are running expect(status1).toBeDefined(); diff --git a/__tests__/schema.ts b/__tests__/integration/schema.test.ts similarity index 97% rename from __tests__/schema.ts rename to __tests__/integration/schema.test.ts index 06a926b..6105443 100644 --- a/__tests__/schema.ts +++ b/__tests__/integration/schema.test.ts @@ -1,16 +1,16 @@ -import { LosslessViewOne } from '../src/views/lossless'; +import { LosslessViewOne } from '../../src/views/lossless'; import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas, ObjectSchema -} from '../src/schema'; -import { DefaultSchemaRegistry } from '../src/schema'; -import { CommonSchemas } from '../util/schemas'; -import { TypedCollectionImpl, SchemaValidationError } from '../src/collections'; -import { RhizomeNode } from '../src/node'; -import { createDelta } from '../src/core/delta-builder'; +} from '../../src/schema'; +import { DefaultSchemaRegistry } from '../../src/schema'; +import { CommonSchemas } from '../../util/schemas'; +import { TypedCollectionImpl, SchemaValidationError } from '../../src/collections'; +import { RhizomeNode } from '../../src/node'; +import { createDelta } from '../../src/core/delta-builder'; describe('Schema System', () => { let schemaRegistry: DefaultSchemaRegistry; diff --git a/__tests__/storage.ts b/__tests__/integration/storage.test.ts similarity index 97% rename from __tests__/storage.ts rename to __tests__/integration/storage.test.ts index bf58f85..9b2a3e5 100644 --- a/__tests__/storage.ts +++ b/__tests__/integration/storage.test.ts @@ -1,7 +1,6 @@ -import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../src/storage'; -import { createDelta } from '../src/core/delta-builder'; -import { Delta } from '../src/core'; -import { DeltaQueryStorage } from '../src/storage/interface'; +import { MemoryDeltaStorage, LevelDBDeltaStorage, StorageFactory } from '../../src/storage'; +import { createDelta } from '../../src/core/delta-builder'; +import { DeltaQueryStorage } from '../../src/storage/interface'; describe('Delta Storage', () => { const testDeltas = [ diff --git a/__tests__/jest-setup.ts b/__tests__/jest-setup.ts deleted file mode 100644 index 26539cb..0000000 --- a/__tests__/jest-setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Set up environment variables for tests -// DEBUG handling examples: -// npm test // will set DEBUG=rz:* by default -// NO_DEBUG=true npm test // will not set DEBUG -// DEBUG=other npm test // will set DEBUG=other -if (!process.env.DEBUG && !process.env.NO_DEBUG) { - // process.env.DEBUG = 'rz:*'; -} - -// This is a placeholder test to satisfy Jest's requirement for at least one test -describe('Test Setup', () => { - it('should pass', () => { - expect(true).toBe(true); - }); -}); \ No newline at end of file diff --git a/__tests__/concurrent-writes.ts b/__tests__/performance/concurrent-writes.test.ts similarity index 99% rename from __tests__/concurrent-writes.ts rename to __tests__/performance/concurrent-writes.test.ts index 19f34f0..3de569a 100644 --- a/__tests__/concurrent-writes.ts +++ b/__tests__/performance/concurrent-writes.test.ts @@ -1,4 +1,4 @@ -import { createDelta } from '../src/core/delta-builder'; +import { createDelta } from '../../src/core/delta-builder'; import { RhizomeNode, Lossless, @@ -7,7 +7,7 @@ import { LastWriteWinsPlugin, MajorityVotePlugin, TimestampResolver -} from "../src"; +} from "../../src"; describe('Concurrent Write Scenarios', () => { let node: RhizomeNode; diff --git a/__tests__/nested-resolution-performance.ts b/__tests__/performance/nested-resolution-performance.test.ts similarity index 98% rename from __tests__/nested-resolution-performance.ts rename to __tests__/performance/nested-resolution-performance.test.ts index 382ef61..09e0452 100644 --- a/__tests__/nested-resolution-performance.ts +++ b/__tests__/performance/nested-resolution-performance.test.ts @@ -9,13 +9,13 @@ */ import Debug from 'debug'; -import { RhizomeNode } from '../src/node'; +import { RhizomeNode } from '../../src/node'; +import { createDelta } from '../../src/core'; +import { DefaultSchemaRegistry } from '../../src/schema'; +import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../../src/schema'; +import { TypedCollectionImpl } from '../../src/collections'; const debug = Debug('rz:test:nested-resolution-performance'); -import { Delta, createDelta } from '../src/core'; -import { DefaultSchemaRegistry } from '../src/schema'; -import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas } from '../src/schema'; -import { TypedCollectionImpl } from '../src/collections'; describe('Nested Object Resolution Performance', () => { let node: RhizomeNode; diff --git a/__tests__/compose-decompose.ts b/__tests__/unit/core/compose-decompose.test.ts similarity index 98% rename from __tests__/compose-decompose.ts rename to __tests__/unit/core/compose-decompose.test.ts index 160b630..7f734bd 100644 --- a/__tests__/compose-decompose.ts +++ b/__tests__/unit/core/compose-decompose.test.ts @@ -4,8 +4,8 @@ * to the original deltas with all pointer relationships preserved. */ -import { RhizomeNode } from '../src/node'; -import { createDelta } from '../src/core/delta-builder'; +import { RhizomeNode } from '../../../src/node'; +import { createDelta } from '../../../src/core/delta-builder'; describe('Lossless View Compose/Decompose', () => { let node: RhizomeNode; diff --git a/__tests__/delta-builder.test.ts b/__tests__/unit/core/delta/delta-builder.test.ts similarity index 96% rename from __tests__/delta-builder.test.ts rename to __tests__/unit/core/delta/delta-builder.test.ts index 3b18fec..8ecb07c 100644 --- a/__tests__/delta-builder.test.ts +++ b/__tests__/unit/core/delta/delta-builder.test.ts @@ -1,8 +1,8 @@ -import { createDelta } from '../src/core/delta-builder'; -import { DeltaV1, DeltaV2 } from '../src/core/delta'; -import { Lossless } from '../src/views/lossless'; -import { RhizomeNode } from '../src/node'; -import { TimestampResolver } from '../src/views/resolvers/timestamp-resolvers'; +import { createDelta } from '../../../../src/core/delta-builder'; +import { DeltaV1, DeltaV2 } from '../../../../src/core/delta'; +import { Lossless } from '../../../../src/views/lossless'; +import { RhizomeNode } from '../../../../src/node'; +import { TimestampResolver } from '../../../../src/views/resolvers/timestamp-resolvers'; describe('DeltaBuilder', () => { const creator = 'creator-123'; diff --git a/__tests__/delta-validation.ts b/__tests__/unit/core/delta/delta-validation.test.ts similarity index 99% rename from __tests__/delta-validation.ts rename to __tests__/unit/core/delta/delta-validation.test.ts index 2ebbd74..bf6cd0f 100644 --- a/__tests__/delta-validation.ts +++ b/__tests__/unit/core/delta/delta-validation.test.ts @@ -6,7 +6,7 @@ import { InvalidPointerError, validateDeltaNetworkImageV1, validateDeltaNetworkImageV2 -} from "../src"; +} from "../../../../src"; describe("Delta Validation", () => { describe("Invalid Delta Formats", () => { diff --git a/__tests__/delta.ts b/__tests__/unit/core/delta/delta.test.ts similarity index 88% rename from __tests__/delta.ts rename to __tests__/unit/core/delta/delta.test.ts index 95da3cf..5177e99 100644 --- a/__tests__/delta.ts +++ b/__tests__/unit/core/delta/delta.test.ts @@ -1,5 +1,5 @@ -import { createDelta } from '../src/core/delta-builder'; -import {DeltaV1, DeltaV2} from "../src"; +import { createDelta } from '../../../../src/core/delta-builder'; +import {DeltaV1, DeltaV2} from "../../../../src"; describe("Delta", () => { it("can convert DeltaV1 to DeltaV2", () => { diff --git a/__tests__/json-ast.test.ts b/__tests__/unit/core/json-ast.test.ts similarity index 97% rename from __tests__/json-ast.test.ts rename to __tests__/unit/core/json-ast.test.ts index bf80941..2a4fc43 100644 --- a/__tests__/json-ast.test.ts +++ b/__tests__/unit/core/json-ast.test.ts @@ -1,5 +1,5 @@ -import { jsonToAst } from '../src/util/json-ast'; -import { JsonNode } from '../src/util/json-ast/types'; +import { jsonToAst } from '../../../src/util/json-ast'; +import { JsonNode } from '../../../src/util/json-ast/types'; describe('jsonToAst', () => { it('should handle primitive values', () => { diff --git a/__tests__/negation.ts b/__tests__/unit/core/negation.test.ts similarity index 98% rename from __tests__/negation.ts rename to __tests__/unit/core/negation.test.ts index 6038e1c..653014c 100644 --- a/__tests__/negation.ts +++ b/__tests__/unit/core/negation.test.ts @@ -1,8 +1,8 @@ import Debug from 'debug'; -import { createDelta } from '../src/core/delta-builder'; -import { NegationHelper } from '../src/features'; -import { RhizomeNode } from '../src/node'; -import { Lossless } from '../src/views'; +import { createDelta } from '../../../src/core/delta-builder'; +import { NegationHelper } from '../../../src/features'; +import { RhizomeNode } from '../../../src/node'; +import { Lossless } from '../../../src/views'; const debug = Debug('rz:negation:test'); diff --git a/__tests__/peer-address.ts b/__tests__/unit/core/peer-address.test.ts similarity index 90% rename from __tests__/peer-address.ts rename to __tests__/unit/core/peer-address.test.ts index 3234e4d..ce6c981 100644 --- a/__tests__/peer-address.ts +++ b/__tests__/unit/core/peer-address.test.ts @@ -1,4 +1,4 @@ -import {parseAddressList, PeerAddress} from '../src/network/peers'; +import {parseAddressList, PeerAddress} from '../../../src/network/peers'; describe('PeerAddress', () => { it('toString()', () => { diff --git a/__tests__/transactions.ts b/__tests__/unit/core/transactions.test.ts similarity index 97% rename from __tests__/transactions.ts rename to __tests__/unit/core/transactions.test.ts index 12855b5..ee381d9 100644 --- a/__tests__/transactions.ts +++ b/__tests__/unit/core/transactions.test.ts @@ -1,7 +1,7 @@ -import { createDelta } from '../src/core/delta-builder'; -import { Lossless } from '../src/views'; -import { RhizomeNode } from '../src/node'; -import { DeltaFilter } from '../src/core'; +import { createDelta } from '../../../src/core/delta-builder'; +import { Lossless } from '../../../src/views'; +import { RhizomeNode } from '../../../src/node'; +import { DeltaFilter } from '../../../src/core'; describe('Transactions', () => { let node: RhizomeNode; @@ -211,7 +211,7 @@ describe('Transactions', () => { const updateEvents: Array<{ entityId: string, deltaIds: string[] }> = []; // Listen for update events - lossless.eventStream.on('updated', (entityId, deltaIds) => { + lossless.eventStream.on('updated', (entityId: string, deltaIds: string[]) => { updateEvents.push({ entityId, deltaIds }); }); @@ -302,7 +302,7 @@ describe('Transactions', () => { .buildV1(); const updateEvents: string[] = []; - lossless.eventStream.on('updated', (entityId) => { + lossless.eventStream.on('updated', (entityId: string) => { updateEvents.push(entityId); }); diff --git a/__tests__/query.ts b/__tests__/unit/query/query.test.ts similarity index 96% rename from __tests__/query.ts rename to __tests__/unit/query/query.test.ts index 940ed96..f093601 100644 --- a/__tests__/query.ts +++ b/__tests__/unit/query/query.test.ts @@ -1,10 +1,10 @@ -import { QueryEngine } from '../src/query'; -import { Lossless } from '../src/views'; -import { DefaultSchemaRegistry } from '../src/schema'; -import { SchemaBuilder, PrimitiveSchemas } from '../src/schema'; -import { CommonSchemas } from '../util/schemas'; -import { createDelta } from '../src/core/delta-builder'; -import { RhizomeNode } from '../src/node'; +import { QueryEngine } from '../../../src/query'; +import { Lossless } from '../../../src/views'; +import { DefaultSchemaRegistry } from '../../../src/schema'; +import { SchemaBuilder, PrimitiveSchemas } from '../../../src/schema'; +import { CommonSchemas } from '../../../util/schemas'; +import { createDelta } from '../../../src/core/delta-builder'; +import { RhizomeNode } from '../../../src/node'; describe('Query Engine', () => { let queryEngine: QueryEngine; diff --git a/__tests__/lossless.ts b/__tests__/unit/views/lossless.test.ts similarity index 97% rename from __tests__/lossless.ts rename to __tests__/unit/views/lossless.test.ts index ac39add..e9a73cd 100644 --- a/__tests__/lossless.ts +++ b/__tests__/unit/views/lossless.test.ts @@ -1,7 +1,7 @@ -import {Delta, DeltaFilter, DeltaV2} from '../src/core'; -import {Lossless} from '../src/views'; -import {RhizomeNode} from '../src/node'; -import {createDelta} from '../src/core/delta-builder'; +import {DeltaFilter} from '../../../src/core'; +import {Lossless} from '../../../src/views'; +import {RhizomeNode} from '../../../src/node'; +import {createDelta} from '../../../src/core/delta-builder'; describe('Lossless', () => { const node = new RhizomeNode(); diff --git a/__tests__/lossy.ts b/__tests__/unit/views/lossy.test.ts similarity index 80% rename from __tests__/lossy.ts rename to __tests__/unit/views/lossy.test.ts index 5c2ad8c..21a420e 100644 --- a/__tests__/lossy.ts +++ b/__tests__/unit/views/lossy.test.ts @@ -1,14 +1,11 @@ import Debug from 'debug'; -import { - PointerTarget, - lastValueFromDeltas, - valueFromCollapsedDelta, - Lossless, - LosslessViewOne, - Lossy, - RhizomeNode -} from "../src"; -import { createDelta } from "../src/core/delta-builder"; +import { PointerTarget } from "../../../src/core/delta"; +import { Lossless, LosslessViewOne } from "../../../src/views/lossless"; +import { Lossy } from "../../../src/views/lossy"; +import { RhizomeNode } from "../../../src/node"; +import { valueFromCollapsedDelta } from "../../../src/views/resolvers/aggregation-resolvers"; +import { lastValueFromDeltas } from "../../../src/views/resolvers/timestamp-resolvers"; +import { createDelta } from "../../../src/core/delta-builder"; const debug = Debug('test:lossy'); type Role = { diff --git a/__tests__/multi-pointer-resolution.ts b/__tests__/unit/views/multi-pointer-resolution.test.ts similarity index 97% rename from __tests__/multi-pointer-resolution.ts rename to __tests__/unit/views/multi-pointer-resolution.test.ts index 7934c47..3b203de 100644 --- a/__tests__/multi-pointer-resolution.ts +++ b/__tests__/unit/views/multi-pointer-resolution.test.ts @@ -4,11 +4,11 @@ * between multiple entities and includes scalar values. */ -import { RhizomeNode } from '../src/node'; -import { createDelta } from '../src/core/delta-builder'; -import { DefaultSchemaRegistry } from '../src/schema'; -import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema'; -import { TypedCollectionImpl } from '../src/collections'; +import { RhizomeNode } from '../../../src/node'; +import { createDelta } from '../../../src/core/delta-builder'; +import { DefaultSchemaRegistry } from '../../../src/schema'; +import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../../../src/schema'; +import { TypedCollectionImpl } from '../../../src/collections'; describe('Multi-Pointer Delta Resolution', () => { let node: RhizomeNode; diff --git a/__tests__/nested-resolution.ts b/__tests__/unit/views/nested-resolution.test.ts similarity index 97% rename from __tests__/nested-resolution.ts rename to __tests__/unit/views/nested-resolution.test.ts index d6ea7be..e219a23 100644 --- a/__tests__/nested-resolution.ts +++ b/__tests__/unit/views/nested-resolution.test.ts @@ -9,12 +9,12 @@ * - Deep nesting scenarios */ -import { RhizomeNode } from '../src/node'; -import { DefaultSchemaRegistry } from '../src/schema'; -import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../src/schema'; -import { CommonSchemas } from '../util/schemas'; -import { TypedCollectionImpl } from '../src/collections'; -import { createDelta } from '../src/core/delta-builder'; +import { RhizomeNode } from '../../../src/node'; +import { DefaultSchemaRegistry } from '../../../src/schema'; +import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas } from '../../../src/schema'; +import { CommonSchemas } from '../../../util/schemas'; +import { TypedCollectionImpl } from '../../../src/collections'; +import { createDelta } from '../../../src/core/delta-builder'; describe('Nested Object Resolution', () => { let node: RhizomeNode; diff --git a/__tests__/aggregation-resolvers.ts b/__tests__/unit/views/resolvers/aggregation-resolvers.test.ts similarity index 98% rename from __tests__/aggregation-resolvers.ts rename to __tests__/unit/views/resolvers/aggregation-resolvers.test.ts index 46bff08..234346d 100644 --- a/__tests__/aggregation-resolvers.ts +++ b/__tests__/unit/views/resolvers/aggregation-resolvers.test.ts @@ -8,9 +8,8 @@ import { AverageResolver, CountResolver, AggregationType -} from "../src"; -import { createDelta } from "../src/core/delta-builder"; -import { Delta } from "../src/core/delta"; +} from "../../../../src"; +import { createDelta } from "../../../../src/core/delta-builder"; describe('Aggregation Resolvers', () => { let node: RhizomeNode; diff --git a/__tests__/custom-resolvers.ts b/__tests__/unit/views/resolvers/custom-resolvers.test.ts similarity index 99% rename from __tests__/custom-resolvers.ts rename to __tests__/unit/views/resolvers/custom-resolvers.test.ts index 6c3b7c1..339c3c0 100644 --- a/__tests__/custom-resolvers.ts +++ b/__tests__/unit/views/resolvers/custom-resolvers.test.ts @@ -1,7 +1,6 @@ import { RhizomeNode, Lossless, - Delta, CustomResolver, ResolverPlugin, LastWriteWinsPlugin, @@ -13,7 +12,7 @@ import { PropertyTypes, CollapsedDelta, createDelta -} from "../src"; +} from "../../../../src"; describe('Custom Resolvers', () => { let node: RhizomeNode; diff --git a/__tests__/last-write-wins.ts b/__tests__/unit/views/resolvers/last-write-wins.test.ts similarity index 83% rename from __tests__/last-write-wins.ts rename to __tests__/unit/views/resolvers/last-write-wins.test.ts index c34205d..ce31687 100644 --- a/__tests__/last-write-wins.ts +++ b/__tests__/unit/views/resolvers/last-write-wins.test.ts @@ -1,7 +1,7 @@ import Debug from "debug"; -import { createDelta } from '../src/core/delta-builder'; -import {Lossless, RhizomeNode} from "../src"; -import {TimestampResolver} from "../src/views/resolvers/timestamp-resolvers"; +import { createDelta } from '../../../../src/core/delta-builder'; +import { Lossless, RhizomeNode } from '../../../../src'; +import { TimestampResolver } from '../../../../src/views/resolvers/timestamp-resolvers'; const debug = Debug('test:last-write-wins'); // This was initially written to test a LastWriteWins resolver, but that has been diff --git a/__tests__/timestamp-resolvers.ts b/__tests__/unit/views/resolvers/timestamp-resolvers.test.ts similarity index 99% rename from __tests__/timestamp-resolvers.ts rename to __tests__/unit/views/resolvers/timestamp-resolvers.test.ts index 33c6160..52d464d 100644 --- a/__tests__/timestamp-resolvers.ts +++ b/__tests__/unit/views/resolvers/timestamp-resolvers.test.ts @@ -6,8 +6,8 @@ import { DeltaIdTimestampResolver, HostIdTimestampResolver, LexicographicTimestampResolver -} from "../src"; -import { createDelta } from "../src/core/delta-builder"; +} from "../../../../src"; +import { createDelta } from "../../../../src/core/delta-builder"; describe('Timestamp Resolvers', () => { let node: RhizomeNode; diff --git a/docs/classes.md b/docs/classes.md new file mode 100644 index 0000000..c3f951b --- /dev/null +++ b/docs/classes.md @@ -0,0 +1,132 @@ +# Rhizome Node Class Diagram + +This document provides an overview of the main classes in the Rhizome Node system and their relationships. + +```mermaid +classDiagram + %% Core Classes + class RhizomeNode { + -config: RhizomeNodeConfig + -pubSub: PubSub + -requestReply: RequestReply + -httpServer: HttpServer + -deltaStream: DeltaStream + -lossless: Lossless + -peers: Peers + -queryEngine: QueryEngine + -storageQueryEngine: StorageQueryEngine + -schemaRegistry: SchemaRegistry + -deltaStorage: DeltaStorage + } + + class Delta { + +id: DeltaID + +timeCreated: Timestamp + +host: HostID + +creator: CreatorID + +pointers: PointerV1[] + } + + class Lossless { + -domainEntities: Map + -transactions: Transactions + +view(ids: DomainEntityID[]): LosslessViewMany + +compose(ids: DomainEntityID[]): LosslessViewMany + } + + class QueryEngine { + -lossless: Lossless + -schemaRegistry: SchemaRegistry + +query(schemaId: SchemaID, filter?: JsonLogic): Promise + } + + class StorageQueryEngine { + -storage: DeltaQueryStorage + -schemaRegistry: SchemaRegistry + +query(schemaId: SchemaID, filter?: JsonLogic): Promise + } + + class SchemaRegistry { + +register(schema: ObjectSchema): void + +get(id: SchemaID): ObjectSchema | undefined + +list(): ObjectSchema[] + } + + class DeltaStream { + -deltas: Delta[] + +receiveDelta(delta: Delta): void + +ingestAll(): void + } + + class DockerOrchestrator { + -containerManager: ContainerManager + -networkManager: NetworkManager + -resourceManager: ResourceManager + -statusManager: StatusManager + +startNode(config: NodeConfig): Promise + +stopNode(nodeId: string): Promise + } + + %% Relationships + RhizomeNode --> DeltaStream + RhizomeNode --> Lossless + RhizomeNode --> QueryEngine + RhizomeNode --> StorageQueryEngine + RhizomeNode --> SchemaRegistry + RhizomeNode --> DeltaStorage + + Lossless --> Transactions + Lossless --> LosslessEntity + + QueryEngine --> SchemaRegistry + QueryEngine --> Lossless + + StorageQueryEngine --> DeltaStorage + StorageQueryEngine --> SchemaRegistry + + DeltaStream --> Delta + Lossless --> Delta + + DockerOrchestrator --> ContainerManager + DockerOrchestrator --> NetworkManager + DockerOrchestrator --> ResourceManager + DockerOrchestrator --> StatusManager +``` + +## Key Components + +1. **RhizomeNode**: The main entry point that coordinates all other components + - Manages the node's lifecycle and configuration + - Coordinates between different subsystems + +2. **Delta**: The fundamental data unit + - Represents atomic changes in the system + - Contains pointers to entities and their properties + +3. **Lossless**: Manages the lossless view of data + - Maintains the complete history of deltas + - Provides methods to view and compose entity states + +4. **QueryEngine**: Handles data queries + - Supports filtering with JSON Logic + - Works with the schema system for validation + +5. **StorageQueryEngine**: Handles storage-level queries + - Interfaces with the underlying storage backend + - Optimized for querying persisted data + +6. **SchemaRegistry**: Manages data schemas + - Validates data against schemas + - Supports schema versioning and evolution + +7. **DockerOrchestrator**: Manages containerized nodes + - Handles node lifecycle (start/stop) + - Manages networking between nodes + +## Data Flow + +1. Deltas are received through the DeltaStream +2. Lossless processes and stores these deltas +3. Queries can be made through either QueryEngine (in-memory) or StorageQueryEngine (persisted) +4. The system maintains consistency through the schema system +5. In distributed mode, DockerOrchestrator manages multiple node instances diff --git a/docs/resolvers.md b/docs/resolvers.md new file mode 100644 index 0000000..a825640 --- /dev/null +++ b/docs/resolvers.md @@ -0,0 +1,15 @@ +# Resolvers (Views) + +The workhorse of this system is likely going to be our lossy views. +This is where the computation likely generally occurs. + +So, let's talk about how to create a view. + +A lossy view initializes from a given lossless view. +The lossless view dispatches events when entity properties are updated. + +View semantics are similar to map-reduce, resolvers in Redux, etc. + +The key is to identify your accumulator object. +Your algorithm SHOULD be implemented so that the reducer is a pure function. +All state must therefore be stored in the accumulator. diff --git a/package.json b/package.json index 0ecb841..49fd73b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build:watch": "tsc --watch", "lint": "eslint", "test": "jest", - "coverage": "npm run test -- --coverage --coverageDirectory=coverage", + "coverage": "npm run test -- --coverage", "example-app": "node dist/examples/app.js", "stop-all": "docker ps -a --filter \"name=^/rhizome-node-\" --format {{.Names}} | xargs -r docker stop", "build-test-image": "docker build -t rhizome-node-test -f Dockerfile.test ." @@ -16,14 +16,19 @@ "testEnvironment": "node", "preset": "ts-jest/presets/default", "roots": [ - "./__tests__/" + "/__tests__" ], "testMatch": [ - "**/__tests__/**/*" + "**/*.test.ts", + "**/*.test.tsx" ], - "setupFilesAfterEnv": [ - "/__tests__/jest-setup.ts" + "testPathIgnorePatterns": [ + "/node_modules/", + "/dist/" ], + "moduleNameMapper": { + "^@/(.*)$": "/src/$1" + }, "transform": { "^\\.tsx?$": [ "ts-jest", @@ -37,7 +42,16 @@ "verbose": true, "testEnvironmentOptions": { "NODE_ENV": "test" - } + }, + "collectCoverageFrom": [ + "src/**/*.{ts,tsx}", + "util/**/*.{ts,tsx}", + "!**/node_modules/**", + "!**/dist/**", + "!**/__tests__/**", + "!**/*.test.{ts,tsx}" + ], + "coverageDirectory": "coverage" }, "author": "Lentil ", "license": "Unlicense", diff --git a/plans/dimensions.md b/plans/dimensions.md new file mode 100644 index 0000000..fea7ba5 --- /dev/null +++ b/plans/dimensions.md @@ -0,0 +1,9 @@ +# Dimensions + +Deltas are a hyperstructure capable of expressing many structures. + +We can consider models which we compose by expressing their dimensions, and +relationships between those dimensions. This can enable a robust form of multi-dimensional +composition. I'm thinking in terms of vectors and matrices, maybe quaternions, tensors. + +So basically we can use deltas to encode math. diff --git a/plans/housekeeping.md b/plans/housekeeping.md index c71f8af..99daecf 100644 --- a/plans/housekeeping.md +++ b/plans/housekeeping.md @@ -1,5 +1,7 @@ # Housekeeping tasks -- [] Organize tests? -- [] More documentation in docs/ -- [] Rename/consolidate, lossless view() and compose() --> composeView() \ No newline at end of file +- [x] Organize tests? +- [ ] More documentation in docs/ +- [ ] Rename/consolidate, lossless view() and compose() --> composeView() +- [ ] Rename Lossless to HyperView +- [ ] Rename Lossy to View diff --git a/src/collections/collection-abstract.ts b/src/collections/collection-abstract.ts index 85feb36..6a6ae39 100644 --- a/src/collections/collection-abstract.ts +++ b/src/collections/collection-abstract.ts @@ -4,7 +4,7 @@ import EventEmitter from "node:events"; import {Delta} from "../core/delta"; import {createDelta} from "../core/delta-builder"; import {Entity, EntityProperties} from "../core/entity"; -import {ResolvedViewOne} from '../views/resolvers/last-write-wins'; +import { ResolvedTimestampedViewOne as ResolvedViewOne } from '../views/resolvers/timestamp-resolvers'; import {RhizomeNode} from "../node"; import {DomainEntityID} from "../core/types"; const debug = Debug('rz:abstract-collection'); diff --git a/src/collections/collection-basic.ts b/src/collections/collection-basic.ts index 7827df7..8cd4437 100644 --- a/src/collections/collection-basic.ts +++ b/src/collections/collection-basic.ts @@ -4,7 +4,7 @@ // It could then be further extended with e.g. table semantics like filter, sort, join import {Collection} from '../collections/collection-abstract'; -import {ResolvedViewOne} from '../views/resolvers/last-write-wins'; +import { ResolvedTimestampedViewOne as ResolvedViewOne } from '../views/resolvers/timestamp-resolvers'; import {TimestampResolver} from '../views/resolvers/timestamp-resolvers'; export class BasicCollection extends Collection { diff --git a/src/collections/collection-relational.ts b/src/collections/collection-relational.ts index 939a173..4413d71 100644 --- a/src/collections/collection-relational.ts +++ b/src/collections/collection-relational.ts @@ -1,5 +1,5 @@ import {Collection} from "./collection-abstract"; -import {ResolvedViewOne} from "../views/resolvers/last-write-wins"; +import { ResolvedTimestampedViewOne as ResolvedViewOne } from "../views/resolvers/timestamp-resolvers"; import {TimestampResolver} from "../views/resolvers/timestamp-resolvers"; class RelationalView extends TimestampResolver { diff --git a/src/collections/collection-typed.ts b/src/collections/collection-typed.ts index 6f2313d..4c2b9dd 100644 --- a/src/collections/collection-typed.ts +++ b/src/collections/collection-typed.ts @@ -1,6 +1,6 @@ import Debug from 'debug'; import { Collection } from '../collections/collection-abstract'; -import { ResolvedViewOne } from '../views/resolvers/last-write-wins'; +import { ResolvedTimestampedViewOne as ResolvedViewOne } from '../views/resolvers/timestamp-resolvers'; import { TimestampResolver } from '../views/resolvers/timestamp-resolvers' import { ObjectSchema, diff --git a/src/orchestration/docker-orchestrator/index.ts b/src/orchestration/docker-orchestrator/index.ts index d6f5f25..31fede0 100644 --- a/src/orchestration/docker-orchestrator/index.ts +++ b/src/orchestration/docker-orchestrator/index.ts @@ -26,7 +26,8 @@ export class DockerOrchestrator extends BaseOrchestrator { private nodeHandles: Map = new Map(); // Managers - private readonly containerManager: ContainerManager; + // Visible for testing + readonly containerManager: ContainerManager; private readonly networkManager: NetworkManager; private readonly resourceManager: ResourceManager; private readonly statusManager: StatusManager; @@ -225,7 +226,7 @@ export class DockerOrchestrator extends BaseOrchestrator { /** * Create network partitions */ - async partitionNetwork(partitions: NetworkPartition): Promise { + async partitionNetwork(_partitions: NetworkPartition): Promise { // Implementation for network partitioning // This is a simplified version - in a real implementation, you would: // 1. Create separate networks for each partition diff --git a/src/orchestration/types.ts b/src/orchestration/types.ts index 51b2c7b..d9976c2 100644 --- a/src/orchestration/types.ts +++ b/src/orchestration/types.ts @@ -37,7 +37,7 @@ export interface NodeConfig { }; /** Additional configuration options */ - [key: string]: any; + [key: string]: unknown; } export interface NodeStatus { diff --git a/src/views/lossy.ts b/src/views/lossy.ts index 2e766e9..81294b6 100644 --- a/src/views/lossy.ts +++ b/src/views/lossy.ts @@ -5,9 +5,18 @@ import Debug from 'debug'; import {DeltaFilter, DeltaID} from "../core/delta"; import {Lossless, LosslessViewOne} from "./lossless"; -import {DomainEntityID} from "../core/types"; +import {DomainEntityID, PropertyID, PropertyTypes, ViewMany} from "../core/types"; const debug = Debug('rz:lossy'); +type PropertyMap = Record; + +export type LossyViewOne = { + id: DomainEntityID; + properties: T; +}; + +export type LossyViewMany = ViewMany>; + // We support incremental updates of lossy models. export abstract class Lossy { deltaFilter?: DeltaFilter; diff --git a/src/views/resolvers/aggregation-resolvers.ts b/src/views/resolvers/aggregation-resolvers.ts index cb185f3..f80e550 100644 --- a/src/views/resolvers/aggregation-resolvers.ts +++ b/src/views/resolvers/aggregation-resolvers.ts @@ -2,7 +2,7 @@ import { EntityProperties } from "../../core/entity"; import { Lossless, LosslessViewOne } from "../lossless"; import { Lossy } from '../lossy'; import { DomainEntityID, PropertyID, ViewMany } from "../../core/types"; -import { valueFromCollapsedDelta } from "./last-write-wins"; +import { CollapsedDelta } from "../lossless"; export type AggregationType = 'min' | 'max' | 'sum' | 'average' | 'count'; @@ -37,6 +37,20 @@ type ResolvedAggregatedViewMany = ViewMany; type Accumulator = AggregatedViewMany; type Result = ResolvedAggregatedViewMany; +// Extract a particular value from a delta's pointers +export function valueFromCollapsedDelta( + key: string, + delta: CollapsedDelta +): string | number | undefined { + for (const pointer of delta.pointers) { + for (const [k, value] of Object.entries(pointer)) { + if (k === key && (typeof value === "string" || typeof value === "number")) { + return value; + } + } + } +} + function aggregateValues(values: number[], type: AggregationType): number { if (values.length === 0) return 0; diff --git a/src/views/resolvers/index.ts b/src/views/resolvers/index.ts index c6d01ea..54bcb76 100644 --- a/src/views/resolvers/index.ts +++ b/src/views/resolvers/index.ts @@ -1,4 +1,4 @@ export * from './aggregation-resolvers'; export * from './custom-resolvers'; -export * from './last-write-wins'; -export * from './timestamp-resolvers'; \ No newline at end of file +export * from './timestamp-resolvers'; +export * from './relationship-graph'; \ No newline at end of file diff --git a/src/views/resolvers/last-write-wins.ts b/src/views/resolvers/last-write-wins.ts deleted file mode 100644 index de8d529..0000000 --- a/src/views/resolvers/last-write-wins.ts +++ /dev/null @@ -1,69 +0,0 @@ -// import Debug from 'debug'; -import {EntityProperties} from "../../core/entity"; -import {CollapsedDelta} from "../lossless"; -import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "../../core/types"; -// const debug = Debug('rz:lossy:last-write-wins'); - -type TimestampedProperty = { - value: PropertyTypes, - timeUpdated: Timestamp -}; - -type TimestampedProperties = { - [key: PropertyID]: TimestampedProperty -}; - -export type LossyViewOne = { - id: DomainEntityID; - properties: T; -}; - -export type LossyViewMany = ViewMany>; - -export type ResolvedViewOne = LossyViewOne; -export type ResolvedViewMany = ViewMany; - -type Accumulator = LossyViewMany; -type Result = LossyViewMany; - -// Extract a particular value from a delta's pointers -export function valueFromCollapsedDelta( - key: string, - delta: CollapsedDelta -): string | number | undefined { - for (const pointer of delta.pointers) { - for (const [k, value] of Object.entries(pointer)) { - if (k === key && (typeof value === "string" || typeof value === "number")) { - return value; - } - } - } -} - -// Resolve a value for an entity by last write wins -export function lastValueFromDeltas( - key: string, - deltas?: CollapsedDelta[] -): { - delta?: CollapsedDelta, - value?: string | number, - timeUpdated?: number -} | undefined { - const res: { - delta?: CollapsedDelta, - value?: string | number, - timeUpdated?: number - } = {}; - res.timeUpdated = 0; - - for (const delta of deltas || []) { - const value = valueFromCollapsedDelta(key, delta); - if (value === undefined) continue; - if (res.timeUpdated && delta.timeCreated < res.timeUpdated) continue; - res.delta = delta; - res.value = value; - res.timeUpdated = delta.timeCreated; - } - - return res; -} diff --git a/src/views/resolvers/relationship-graph.ts b/src/views/resolvers/relationship-graph.ts new file mode 100644 index 0000000..d5b5232 --- /dev/null +++ b/src/views/resolvers/relationship-graph.ts @@ -0,0 +1,219 @@ +import { CollapsedDelta, LosslessViewOne } from "../lossless"; +import { PropertyTypes } from "../../core/types"; +import { Lossy } from "../lossy"; + +/** + * Represents a single relationship between entities + */ +export interface Relationship { + id: string; + type: string; + source: string; + target: string; + properties: Record; + deltas: string[]; +} + +/** + * Tracks relationships from a single entity's perspective + */ +interface EntityRelationships { + outbound: Map; // relationshipId -> Relationship + inbound: Map; // relationshipId -> Relationship +} + +/** + * The accumulator that maintains the complete relationship graph state + */ +interface RelationshipGraphAccumulator { + // Maps entity ID to its relationships + entities: Map; + + // Maps relationship ID to the relationship data + relationships: Map; + + // For quick lookups by relationship type + relationshipsByType: Map>; // type -> Set + + // Timestamp of the most recent update + lastUpdated?: number; +} + +/** + * The public view of relationships for a single entity + */ +export type RelationshipView = { + outbound: Relationship[]; + inbound: Relationship[]; +}; + +/** + * A resolver that builds a relationship graph from a LosslessViewMany + */ +export class RelationshipGraphResolver extends Lossy { + /** + * Initialize a new accumulator + */ + protected createRelationshipGraphAccumulator(): RelationshipGraphAccumulator { + return { + entities: new Map(), + relationships: new Map(), + relationshipsByType: new Map(), + lastUpdated: Date.now() + }; + } + + /** + * Initialize the accumulator with a view + */ + initializer(view: LosslessViewOne): RelationshipGraphAccumulator { + const graph = this.createRelationshipGraphAccumulator(); + + // Initialize entity relationships if they don't exist + if (!graph.entities.has(view.id)) { + graph.entities.set(view.id, { + outbound: new Map(), + inbound: new Map() + }); + } + + return graph; + } + + /** + * Process a view and update the accumulator + */ + reducer(graph: RelationshipGraphAccumulator, view: LosslessViewOne): RelationshipGraphAccumulator { + // Ensure entity exists in the graph + if (!graph.entities.has(view.id)) { + graph.entities.set(view.id, { + outbound: new Map(), + inbound: new Map() + }); + } + + // Process relationship properties + for (const [property, deltas] of Object.entries(view.propertyDeltas)) { + // Skip non-relationship properties + if (!property.startsWith('_rel_')) continue; + + for (const delta of deltas) { + this.processRelationshipDelta(graph, delta); + } + } + + graph.lastUpdated = Date.now(); + return graph; + } + + /** + * Resolve the accumulator into a final result. + * For now, we just return the accumulator as is. + */ + resolver(graph: RelationshipGraphAccumulator): RelationshipGraphAccumulator { + return graph; + } + + /** + * Process a single relationship delta + */ + private processRelationshipDelta(graph: RelationshipGraphAccumulator, delta: CollapsedDelta): void { + // Extract relationship metadata from the delta + const relProps = this.extractRelationshipProperties(delta); + if (!relProps) return; + + const { type, sourceId, targetId, relId, properties } = relProps; + + // Ensure source and target entities exist in the graph + if (!graph.entities.has(sourceId)) { + graph.entities.set(sourceId, { outbound: new Map(), inbound: new Map() }); + } + if (!graph.entities.has(targetId)) { + graph.entities.set(targetId, { outbound: new Map(), inbound: new Map() }); + } + + // Get or create the relationship + let relationship = graph.relationships.get(relId); + + if (!relationship) { + // Create new relationship + relationship = { + id: relId, + type, + source: sourceId, + target: targetId, + properties: { ...properties }, + deltas: [delta.id] + }; + + // Add to relationships map + graph.relationships.set(relId, relationship); + + // Add to relationships by type index + if (!graph.relationshipsByType.has(type)) { + graph.relationshipsByType.set(type, new Set()); + } + graph.relationshipsByType.get(type)?.add(relId); + + // Update entity relationships + const sourceEntity = graph.entities.get(sourceId)!; + const targetEntity = graph.entities.get(targetId)!; + + sourceEntity.outbound.set(relId, relationship); + targetEntity.inbound.set(relId, relationship); + } else { + // Update existing relationship + // TODO: Conflict resolution e.g. using TimestampResolver + relationship.properties = { ...relationship.properties, ...properties }; + + // Track this delta if not already present + if (!relationship.deltas.includes(delta.id)) { + relationship.deltas.push(delta.id); + } + } + } + + /** + * Extract relationship properties from a delta + */ + private extractRelationshipProperties(delta: CollapsedDelta): { + relId: string; + sourceId: string; + targetId: string; + type: string; + properties: Record; + } | null { + // Get all pointers that are part of this relationship + const relPointers = delta.pointers.filter(p => + p._rel_source || p._rel_target || p._rel_type + ); + + if (relPointers.length === 0) return null; + + // Find the relationship metadata + const sourcePtr = relPointers.find(p => p._rel_source); + const targetPtr = relPointers.find(p => p._rel_target); + const typePtr = relPointers.find(p => p._rel_type); + + if (!sourcePtr || !targetPtr || !typePtr) return null; + + const relId = delta.id; // Use delta ID as relationship ID + const sourceId = sourcePtr._rel_source as string; + const targetId = targetPtr._rel_target as string; + const type = typePtr._rel_type as string; + + // Extract other properties (non-special _rel_ pointers) + const properties: Record = {}; + + for (const ptr of delta.pointers) { + for (const [key, value] of Object.entries(ptr)) { + if (key.startsWith('_rel_') && !['_rel_source', '_rel_target', '_rel_type'].includes(key)) { + const propName = key.substring(5); // Remove '_rel_' prefix + properties[propName] = value as PropertyTypes; + } + } + } + + return { relId, sourceId, targetId, type, properties }; + } +} diff --git a/src/views/resolvers/timestamp-resolvers.ts b/src/views/resolvers/timestamp-resolvers.ts index d0c3f07..686e4a9 100644 --- a/src/views/resolvers/timestamp-resolvers.ts +++ b/src/views/resolvers/timestamp-resolvers.ts @@ -1,8 +1,19 @@ import { EntityProperties } from "../../core/entity"; -import { Lossless, LosslessViewOne } from "../lossless"; +import { Lossless, LosslessViewOne, CollapsedDelta } from "../lossless"; import { Lossy } from '../lossy'; import { DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany } from "../../core/types"; -import { valueFromCollapsedDelta } from "./last-write-wins"; +import { valueFromCollapsedDelta } from "./aggregation-resolvers"; + +// Moved here from last-write-wins.ts before removing that file +export type TimestampedProperty = { + value: PropertyTypes, + timeUpdated: Timestamp +}; + +// Moved here from last-write-wins.ts before removing that file +export type TimestampedProperties = { + [key: PropertyID]: TimestampedProperty +}; export type TieBreakingStrategy = 'creator-id' | 'delta-id' | 'host-id' | 'lexicographic'; @@ -152,4 +163,32 @@ export class LexicographicTimestampResolver extends TimestampResolver { constructor(lossless: Lossless) { super(lossless, 'lexicographic'); } +} + +// Resolve a value for an entity by last write wins +export function lastValueFromDeltas( + key: string, + deltas?: CollapsedDelta[] +): { + delta?: CollapsedDelta, + value?: string | number, + timeUpdated?: number +} | undefined { + const res: { + delta?: CollapsedDelta, + value?: string | number, + timeUpdated?: number + } = {}; + res.timeUpdated = 0; + + for (const delta of deltas || []) { + const value = valueFromCollapsedDelta(key, delta); + if (value === undefined) continue; + if (res.timeUpdated && delta.timeCreated < res.timeUpdated) continue; + res.delta = delta; + res.value = value; + res.timeUpdated = delta.timeCreated; + } + + return res; } \ No newline at end of file