diff --git a/__tests__/query.ts b/__tests__/query.ts new file mode 100644 index 0000000..cbb40c7 --- /dev/null +++ b/__tests__/query.ts @@ -0,0 +1,4 @@ +describe.skip('Query', () => { + it('can use a json logic expression to filter the queries', () => {}); + it('can use a json logic expression to implement a lossy resolver', () => {}); +}); diff --git a/examples/app.ts b/examples/app.ts index 397d519..dcef86c 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -1,7 +1,7 @@ import Debug from 'debug'; import {RhizomeNode} from "../src/node"; import {Entity} from "../src/entity"; -import {TypedCollection} from "../src/typed-collection"; +import {Collection} from "../src/collection"; const debug = Debug('example-app'); // As an app we want to be able to write and read data. @@ -19,7 +19,7 @@ type User = { (async () => { const rhizomeNode = new RhizomeNode(); - const users = new TypedCollection("user"); + const users = new Collection("user"); users.rhizomeConnect(rhizomeNode); users.onUpdate((u: Entity) => { @@ -37,27 +37,63 @@ type User = { // - Logging // - Chat // - - const taliesin = await users.put(undefined, { + const taliesinData: User = { id: 'taliesin-1', name: 'Taliesin', nameLong: 'Taliesin (Ladd)', age: Math.floor(Math.random() * 1000) - }); + }; + + const taliesinPutResult = await users.put(undefined, taliesinData); + + { + const result = JSON.stringify(taliesinPutResult); + const expected = JSON.stringify(taliesinData); + + if (result === expected) { + debug('Put result matches expected: ' + expected); + } else { + debug(`Put result does not match expected.` + + `\n\nExpected \n${expected}` + + `\nReceived\n${result}`); + } + } // TODO: Allow configuration regarding read/write concern i.e. // if we perform a read immediately do we see the value we wrote? // Intuition says yes, we want that-- but how do we expose the propagation status? - const result = users.get(taliesin.id); - const matches: boolean = JSON.stringify(result) === JSON.stringify(taliesin); - if (matches) { - debug('Result matches expected: ' + JSON.stringify(taliesin)); - } else { - debug(`Result does not match expected.` + - `\n\nExpected \n${JSON.stringify(taliesin)}` + - `\nReceived\n${JSON.stringify(result)}`); + const resolved = users.resolve('taliesin-1'); + if (!resolved) throw new Error('unable to resolve entity we just created'); + + const resolvedUser = { + id: resolved.id, + ...resolved.properties + } as User; + + /* + function sortKeys (o: {[key: string]: unknown}): {[key: string]: unknown} { + const r: {[key: string]: unknown} = {}; + r.id = o.id; + Object.keys(o).sort().forEach((key) => { + if (key === "id") return; + r[key] = o[key]; + }) + return r; } + */ + + const result = JSON.stringify(resolvedUser); + const expected = JSON.stringify(taliesinData); + + if (result === expected) { + debug('Get result matches expected: ' + expected); + } else { + debug(`Get result does not match expected.` + + `\n\nExpected \n${expected}` + + `\nReceived\n${result}`); + } + })(); diff --git a/package-lock.json b/package-lock.json index 3fb72e2..19fe238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,31 +1,34 @@ { "name": "rhizome-node", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rhizome-node", - "version": "1.0.0", + "version": "0.1.0", "license": "Unlicense", "dependencies": { - "@types/bluebird": "^3.5.42", - "@types/debug": "^4.1.12", - "@types/json-logic-js": "^2.0.8", - "@types/object-hash": "^3.0.6", "debug": "^4.4.0", "express": "^4.21.2", "json-logic-js": "^2.0.5", "level": "^9.0.0", + "microtime": "^3.1.1", "object-hash": "^3.0.0", "showdown": "^2.1.0", + "util": "./util/", "zeromq": "^6.1.2" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/bluebird": "^3.5.42", + "@types/debug": "^4.1.12", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/json-logic-js": "^2.0.8", + "@types/microtime": "^2.1.2", "@types/node": "^22.10.2", + "@types/object-hash": "^3.0.6", "@types/showdown": "^2.0.6", "eslint": "^9.17.0", "eslint-config-airbnb-base-typescript": "^1.1.0", @@ -1438,6 +1441,7 @@ "version": "3.5.42", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "dev": true, "license": "MIT" }, "node_modules/@types/body-parser": { @@ -1465,6 +1469,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -1562,6 +1567,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.8.tgz", "integrity": "sha512-WgNsDPuTPKYXl0Jh0IfoCoJoAGGYZt5qzpmjuLSEg7r0cKp/kWtWp0HAsVepyPSPyXiHo6uXp/B/kW/2J1fa2Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1579,6 +1585,13 @@ "license": "MIT", "peer": true }, + "node_modules/@types/microtime": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/microtime/-/microtime-2.1.2.tgz", + "integrity": "sha512-d5odaV/0jPwfehN1t+y7+TcbGxECQLtl7mVETpMaYA0SnlhyKQKgWPCRetbSJVP7i2Kzx8CuTDgDs2kjS1MCOw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1590,6 +1603,7 @@ "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1605,6 +1619,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-3.0.6.tgz", "integrity": "sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==", + "dev": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -6907,6 +6922,26 @@ "node": ">=8.6" } }, + "node_modules/microtime": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/microtime/-/microtime-3.1.1.tgz", + "integrity": "sha512-to1r7o24cDsud9IhN6/8wGmMx5R2kT0w2Xwm5okbYI3d1dk6Xv0m+Z+jg2vS9pt+ocgQHTCtgs/YuyJhySzxNg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.4.0" + }, + "engines": { + "node": ">= 14.13.0" + } + }, + "node_modules/microtime/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -8974,6 +9009,10 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "license": "MIT" }, + "node_modules/util": { + "resolved": "util", + "link": true + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9280,6 +9319,7 @@ "node": ">= 10", "pnpm": ">= 9" } - } + }, + "util": {} } } diff --git a/package.json b/package.json index 9ab813e..14d6637 100644 --- a/package.json +++ b/package.json @@ -13,25 +13,32 @@ "jest": { "testEnvironment": "node", "preset": "ts-jest", - "roots": ["__tests__/"] + "roots": [ + "./__tests__/" + ], + "testMatch": [ + "**/__tests__/**/*" + ] }, "author": "Taliesin (Ladd) ", "license": "Unlicense", "dependencies": { - "@types/bluebird": "^3.5.42", - "@types/debug": "^4.1.12", - "@types/json-logic-js": "^2.0.8", - "@types/object-hash": "^3.0.6", "debug": "^4.4.0", "express": "^4.21.2", "json-logic-js": "^2.0.5", "level": "^9.0.0", + "microtime": "^3.1.1", "object-hash": "^3.0.0", "showdown": "^2.1.0", - "zeromq": "^6.1.2", - "util": "./util/" + "util": "./util/", + "zeromq": "^6.1.2" }, "devDependencies": { + "@types/bluebird": "^3.5.42", + "@types/debug": "^4.1.12", + "@types/json-logic-js": "^2.0.8", + "@types/microtime": "^2.1.2", + "@types/object-hash": "^3.0.6", "@eslint/js": "^9.17.0", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", diff --git a/scratch/jsonlogic.ts b/scratch/jsonlogic.ts new file mode 100644 index 0000000..703253d --- /dev/null +++ b/scratch/jsonlogic.ts @@ -0,0 +1,12 @@ +import { apply } from 'json-logic-js'; + +console.log(apply({"map":[ + {"var":"integers"}, + {"*":[{"var":""},2]} +]}, {"integers":[1,2,3,4,5]})); + +console.log(apply({"reduce":[ + {"var":"integers"}, + {"+":[{"var":"current"}, {"var":"accumulator"}]}, + 0 +]}, {"integers":[1,2,3,4,5]})); diff --git a/src/collection.ts b/src/collection.ts index 0c364be..10d4153 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -9,7 +9,7 @@ import EventEmitter from "node:events"; import {Delta, DeltaID} from "./delta"; import {Entity, EntityProperties} from "./entity"; import {LosslessViewMany} from "./lossless"; -import {firstValueFromLosslessViewOne, Lossy, LossyViewMany, LossyViewOne} from "./lossy"; +import {lastValueFromLosslessViewOne, Lossy, ResolvedViewMany, ResolvedViewOne, Resolver} from "./lossy"; import {RhizomeNode} from "./node"; import {DomainEntityID} from "./types"; const debug = Debug('collection'); @@ -17,22 +17,12 @@ const debug = Debug('collection'); export class Collection { rhizomeNode?: RhizomeNode; name: string; - entities = new Map(); eventStream = new EventEmitter(); constructor(name: string) { this.name = name; } - ingestDelta(delta: Delta) { - if (!this.rhizomeNode) return; - - const updated = this.rhizomeNode.lossless.ingestDelta(delta); - - this.eventStream.emit('ingested', delta); - this.eventStream.emit('updated', updated); - } - // Instead of trying to update our final view of the entity with every incoming delta, // let's try this: // - keep a lossless view (of everything) @@ -53,39 +43,24 @@ export class Collection { debug(`connected ${this.name} to rhizome`); } - onCreate(cb: (entity: Entity) => void) { - // TODO: Trigger for changes received from peers - this.eventStream.on('create', (entity: Entity) => { - cb(entity); - }); - } + ingestDelta(delta: Delta) { + if (!this.rhizomeNode) return; - onUpdate(cb: (entity: Entity) => void) { - // TODO: Trigger for changes received from peers - this.eventStream.on('update', (entity: Entity) => { - cb(entity); - }); - } + const updated = this.rhizomeNode.lossless.ingestDelta(delta); - defaultResolver(losslessView: LosslessViewMany): LossyViewMany { - const resolved: LossyViewMany = {}; - debug('default resolver, lossless view', JSON.stringify(losslessView)); - for (const [id, ent] of Object.entries(losslessView)) { - resolved[id] = {id, properties: {}}; - for (const key of Object.keys(ent.properties)) { - const {value} = firstValueFromLosslessViewOne(ent, key) || {}; - debug(`[ ${key} ] = ${value}`); - resolved[id].properties[key] = value; - } - } - return resolved; + this.eventStream.emit('ingested', delta); + this.eventStream.emit('updated', updated); } // Applies the javascript rules for updating object values, - // e.g. set to `undefined` to delete a property + // e.g. set to `undefined` to delete a property. + // This function is here instead of Entity so that it can: + // - read the current state in order to build its delta + // - include the collection name in the delta it produces generateDeltas( entityId: DomainEntityID, newProperties: EntityProperties, + resolver?: Resolver, creator?: string, host?: string ): Delta[] { @@ -93,7 +68,7 @@ export class Collection { let oldProperties: EntityProperties = {}; if (entityId) { - const entity = this.get(entityId); + const entity = this.resolve(entityId, resolver); if (entity) { oldProperties = entity.properties; } @@ -123,17 +98,39 @@ export class Collection { return deltas; } + onCreate(cb: (entity: Entity) => void) { + // TODO: Trigger for changes received from peers + this.eventStream.on('create', (entity: Entity) => { + cb(entity); + }); + } + + onUpdate(cb: (entity: Entity) => void) { + // TODO: Trigger for changes received from peers + this.eventStream.on('update', (entity: Entity) => { + cb(entity); + }); + } + + getIds(): string[] { + if (!this.rhizomeNode) return []; + return Array.from(this.rhizomeNode.lossless.domainEntities.keys()); + } + + // THIS PUT SHOULD CORRESOND TO A PARTICULAR MATERIALIZED VIEW... + // How can we encode that? + // Well, we have a way to do that, we just need the same particular inputs. + // We take a resolver as an optional argument. async put( entityId: DomainEntityID | undefined, - properties: EntityProperties - ): Promise { - // const deltas: Delta[] = []; - // const entity = this.updateEntity(entityId, properties, true, deltas); - - // THIS PUT SHOULD CORRESOND TO A PARTICULAR MATERIALIZED VIEW... - // How can we encode that? - // Well, we have a way to do that, we just need the same particular inputs - + properties: EntityProperties, + resolver?: Resolver + ): Promise { + // For convenience, we allow setting id via properties.id + if (!entityId && !!properties.id && typeof properties.id === 'string') { + entityId = properties.id; + } + // Generate an ID if none is provided if (!entityId) { entityId = randomUUID(); } @@ -141,12 +138,17 @@ export class Collection { const deltas = this.generateDeltas( entityId, properties, + resolver, this.rhizomeNode?.config.creator, this.rhizomeNode?.config.peerId, ); debug(`put ${entityId} generated deltas:`, JSON.stringify(deltas)); + // Here we set up a listener so we can wait for all our deltas to be + // ingested into our lossless view before proceeding. + // TODO: Hoist this into a more generic transaction mechanism. + const allIngested = new Promise((resolve) => { const ingestedIds = new Set(); this.eventStream.on('ingested', (delta: Delta) => { @@ -160,7 +162,6 @@ export class Collection { }) }); - // updateEntity may have generated some deltas for us to store and publish deltas.forEach(async (delta: Delta) => { // record this delta just as if we had received it from a peer @@ -181,7 +182,7 @@ export class Collection { await allIngested; - const res = this.get(entityId); + const res = this.resolve(entityId, resolver); if (!res) throw new Error("could not get what we just put!"); this.eventStream.emit("update", res); @@ -189,18 +190,41 @@ export class Collection { return res; } - get(id: string): LossyViewOne | undefined { - // Now with lossy view approach, instead of just returning what we already have, - // let's compute our view now. - // return this.entities.get(id); - if (!this.rhizomeNode) return undefined; - const lossy = new Lossy(this.rhizomeNode.lossless); - const res = lossy.resolve((view) => this.defaultResolver(view), [id]); - return res[id]; + // TODO: default should probably be last write wins + defaultResolver(losslessView: LosslessViewMany): ResolvedViewMany { + const resolved: ResolvedViewMany = {}; + + // debug('default resolver, lossless view', JSON.stringify(losslessView)); + for (const [id, ent] of Object.entries(losslessView)) { + resolved[id] = {id, properties: {}}; + + for (const key of Object.keys(ent.properties)) { + const {value} = lastValueFromLosslessViewOne(ent, key) || {}; + + // debug(`[ ${key} ] = ${value}`); + resolved[id].properties[key] = value; + } + } + return resolved; } - getIds(): string[] { - if (!this.rhizomeNode) return []; - return Array.from(this.rhizomeNode.lossless.domainEntities.keys()); + resolve(id: string, resolver?: Resolver): ResolvedViewOne | undefined { + // Now with lossy view approach, instead of just returning what we + // already have, let's compute our view now. + // return this.entities.resolve(id); + // TODO: Caching + + if (!this.rhizomeNode) return undefined; + + if (!resolver) { + debug('using default resolver'); + resolver = (view) => this.defaultResolver(view); + } + + const lossy = new Lossy(this.rhizomeNode.lossless); + const res = lossy.resolve(resolver, [id]); + debug('lossy view', res); + + return res[id]; } } diff --git a/src/delta.ts b/src/delta.ts index 4a34e4c..8f4a176 100644 --- a/src/delta.ts +++ b/src/delta.ts @@ -1,5 +1,6 @@ +import microtime from 'microtime'; import {randomUUID} from "crypto"; -import {PeerAddress} from "./types"; +import {PeerAddress, Timestamp} from "./types"; export type DeltaID = string; @@ -14,11 +15,15 @@ export type Pointer = { export class Delta { id: DeltaID; receivedFrom?: PeerAddress; + timeReceived: Timestamp; + timeCreated: Timestamp; creator: string; host: string; pointers: Pointer[] = []; - constructor(delta: Omit) { + constructor(delta: Omit) { this.id = randomUUID(); + this.timeCreated = microtime.now(); + this.timeReceived = this.timeCreated; this.creator = delta.creator; this.host = delta.host; this.receivedFrom = delta.receivedFrom; diff --git a/src/deltas.ts b/src/deltas.ts index 87ae7b2..eda77fe 100644 --- a/src/deltas.ts +++ b/src/deltas.ts @@ -42,7 +42,7 @@ export class DeltaStream { switch (decision) { case Decision.Accept: this.deltasAccepted.push(delta); - this.deltaStream.emit('delta', {delta}); + this.deltaStream.emit('delta', delta); break; case Decision.Reject: this.deltasRejected.push(delta); @@ -80,7 +80,7 @@ export class DeltaStream { } subscribeDeltas(fn: (delta: Delta) => void) { - this.deltaStream.on('delta', ({delta}) => { + this.deltaStream.on('delta', (delta) => { fn(delta); }); } @@ -90,11 +90,12 @@ export class DeltaStream { await this.rhizomeNode.pubSub.publish("deltas", this.serializeDelta(delta)); } - serializeDelta(delta: Delta) { + serializeDelta(delta: Delta): string { return JSON.stringify(delta); } - deserializeDelta(input: string) { + deserializeDelta(input: string): Delta { + // TODO: Input validation return JSON.parse(input); } } diff --git a/src/entity.ts b/src/entity.ts index fd85dbd..616da1b 100644 --- a/src/entity.ts +++ b/src/entity.ts @@ -7,6 +7,7 @@ // - As typescript interfaces? // - As typescript classes? +import {Collection} from "./collection"; import {PropertyTypes} from "./types"; export type EntityProperties = { @@ -19,6 +20,11 @@ export class Entity { constructor( readonly id: string, + readonly collection?: Collection ) {} -} + async save() { + if (!this.collection) throw new Error('to save this entity you must specify the collection'); + return this.collection.put(this.id, this.properties); + } +} diff --git a/src/http/api.ts b/src/http/api.ts index 156f298..4aef2cc 100644 --- a/src/http/api.ts +++ b/src/http/api.ts @@ -61,7 +61,7 @@ export class HttpApi { // Get a single domain entity by ID this.router.get(`/${name}/:id`, (req: express.Request, res: express.Response) => { const {params: {id}} = req; - const ent = collection.get(id); + const ent = collection.resolve(id); if (!ent) { res.status(404).send({error: "Not Found"}); return; diff --git a/src/lossless.ts b/src/lossless.ts index 767c516..ff3b56a 100644 --- a/src/lossless.ts +++ b/src/lossless.ts @@ -3,7 +3,7 @@ import Debug from 'debug'; import {Delta, DeltaFilter} from './delta'; -import {DomainEntityID, PropertyID, PropertyTypes} from "./types"; +import {DomainEntityID, PropertyID, PropertyTypes, ViewMany} from "./types"; const debug = Debug('lossless'); export type CollapsedPointer = {[key: string]: PropertyTypes}; @@ -19,9 +19,7 @@ export type LosslessViewOne = { } }; -export type LosslessViewMany = { - [key: DomainEntityID]: LosslessViewOne; -}; +export type LosslessViewMany = ViewMany; class DomainEntityMap extends Map {}; @@ -66,7 +64,7 @@ class DomainEntity { export class Lossless { domainEntities = new DomainEntityMap(); - ingestDelta(delta: Delta): LosslessViewMany { + ingestDelta(delta: Delta) { const targets = delta.pointers .filter(({targetContext}) => !!targetContext) .map(({target}) => target) @@ -86,8 +84,6 @@ export class Lossless { debug('after add, domain entity:', JSON.stringify(ent)); } - - return this.view(targets); } //TODO: json logic -- view(deltaFilter?: FilterExpr) { diff --git a/src/lossy.ts b/src/lossy.ts index dcdac74..3bf7d70 100644 --- a/src/lossy.ts +++ b/src/lossy.ts @@ -6,21 +6,30 @@ // Fields in the output can be described as transformations import Debug from 'debug'; -import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless"; -import {DomainEntityID, Properties} from "./types"; import {DeltaFilter} from "./delta"; +import {CollapsedDelta, Lossless, LosslessViewMany, LosslessViewOne} from "./lossless"; +import {DomainEntityID, PropertyID, PropertyTypes, Timestamp, ViewMany} from "./types"; const debug = Debug('lossy'); -export type LossyViewOne = { +type TimestampedProperty = { + value: PropertyTypes, + timeUpdated: Timestamp +}; + +export type LossyViewOne = { id: DomainEntityID; - properties: T; + properties: { + [key: PropertyID]: T + }; }; -export type LossyViewMany = { - [key: DomainEntityID]: LossyViewOne; -}; +export type LossyViewMany = ViewMany>; -type Resolver = (losslessView: LosslessViewMany) => T; +export type ResolvedViewOne = LossyViewOne; +export type ResolvedViewMany = ViewMany; + +export type Resolver = + (losslessView: LosslessViewMany) => T; // Extract a particular value from a delta's pointers export function valueFromCollapsedDelta( @@ -44,13 +53,42 @@ export function firstValueFromLosslessViewOne( delta: CollapsedDelta, value: string | number } | undefined { - debug(`trying to get value for ${key} from ${JSON.stringify(ent.properties[key])}`); + debug(`trying to get first value for ${key} from ${JSON.stringify(ent.properties[key])}`); for (const delta of ent.properties[key] || []) { const value = valueFromCollapsedDelta(delta, key); if (value) return {delta, value}; } } +// Function for resolving a value for an entity by last write wins +export function lastValueFromLosslessViewOne( + ent: LosslessViewOne, + key: string +): { + delta?: CollapsedDelta, + value?: string | number, + timeUpdated?: number +} | undefined { + const res: { + delta?: CollapsedDelta, + value?: string | number, + timeUpdated?: number + } = {}; + debug(`trying to get last value for ${key} from ${JSON.stringify(ent.properties[key])}`); + res.timeUpdated = 0; + + for (const delta of ent.properties[key] || []) { + const value = valueFromCollapsedDelta(delta, key); + if (value === undefined) continue; + if (delta.timeCreated < res.timeUpdated) continue; + res.delta = delta; + res.value = value; + res.timeUpdated = delta.timeCreated; + } + + return res; +} + export class Lossy { lossless: Lossless; diff --git a/src/typed-collection.ts b/src/typed-collection.ts deleted file mode 100644 index ff58ee2..0000000 --- a/src/typed-collection.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Collection} from './collection'; -import {EntityProperties} from './entity'; -import {LossyViewOne} from './lossy'; -import {DomainEntityID} from './types'; - -export class TypedCollection extends Collection { - async put(id: DomainEntityID | undefined, properties: T): Promise { - return super.put(id, properties); - } -} diff --git a/src/types.ts b/src/types.ts index 46e5b6a..bdee6a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,8 +9,13 @@ export type PropertyTypes = string | number | undefined; export type DomainEntityID = string; export type PropertyID = string; -export type Properties = {[key: PropertyID]: PropertyTypes}; +export type Timestamp = number; +export type ViewMany = { + [key: DomainEntityID]: T; +}; + +// TODO: Move to ./peers.ts export class PeerAddress { addr: string; port: number; diff --git a/tsconfig.json b/tsconfig.json index 1cad126..a148e62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,15 +4,22 @@ "module": "CommonJS", "esModuleInterop": true, "moduleResolution": "Node", - "sourceMap": true, + "sourceMap": false, "baseUrl": ".", "outDir": "dist", "importsNotUsedAsValues": "remove", "strict": true, - "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*", "examples/**/*", "__tests__/**/*"], - "exclude": ["node_modules"] + "include": [ + "src/**/*", + "util/**/*", + "examples/**/*", + "scratch/**/*", + "__tests__/**/*" + ], + "exclude": [ + "node_modules" + ] } diff --git a/util/app.ts b/util/app.ts index 67aba31..c1515bd 100644 --- a/util/app.ts +++ b/util/app.ts @@ -1,13 +1,5 @@ import {RhizomeNode, RhizomeNodeConfig} from "../src/node"; -import {TypedCollection} from "../src/typed-collection"; - -type User = { - id?: string; - name: string; - nameLong?: string; - email?: string; - age: number; -}; +import {Collection} from "../src/collection"; const start = 5000; const range = 5000; @@ -25,7 +17,7 @@ export class App extends RhizomeNode { ...config, }); - const users = new TypedCollection("user"); + const users = new Collection("user"); users.rhizomeConnect(this); const {httpAddr, httpPort} = this.config;