From e52665cb8a41bad0d5c1c0a97dffeaee302080bd Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 2 Oct 2024 04:01:12 +0100 Subject: [PATCH 1/4] Add abstract LiveObject class with base shared functionality for Live Objects Resolves DTP-952 --- scripts/moduleReport.ts | 2 +- src/plugins/liveobjects/liveobject.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/plugins/liveobjects/liveobject.ts diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index f5e0f93f3..3714bc93c 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -308,7 +308,7 @@ async function checkLiveObjectsPluginFiles() { const pluginBundleInfo = getBundleInfo('./build/liveobjects.js'); // These are the files that are allowed to contribute >= `threshold` bytes to the LiveObjects bundle. - const allowedFiles = new Set(['src/plugins/liveobjects/index.ts']); + const allowedFiles = new Set(['src/plugins/liveobjects/index.ts', 'src/plugins/liveobjects/liveobject.ts']); return checkBundleFiles(pluginBundleInfo, allowedFiles, 100); } diff --git a/src/plugins/liveobjects/liveobject.ts b/src/plugins/liveobjects/liveobject.ts new file mode 100644 index 000000000..945e09ced --- /dev/null +++ b/src/plugins/liveobjects/liveobject.ts @@ -0,0 +1,33 @@ +import { LiveObjects } from './liveobjects'; + +interface LiveObjectData { + data: any; +} + +export abstract class LiveObject { + protected _dataRef: T; + protected _objectId: string; + + constructor( + protected _liveObjects: LiveObjects, + initialData?: T | null, + objectId?: string, + ) { + this._dataRef = initialData ?? this._getZeroValueData(); + this._objectId = objectId ?? this._createObjectId(); + } + + /** + * @internal + */ + getObjectId(): string { + return this._objectId; + } + + private _createObjectId(): string { + // TODO: implement object id generation based on live object type and initial value + return Math.random().toString().substring(2); + } + + protected abstract _getZeroValueData(): T; +} From 96049bec2f66d90f0ed66ae8e58cc7f491ff00ef Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 2 Oct 2024 04:03:03 +0100 Subject: [PATCH 2/4] Add LiveMap and LiveCounter concrete classes Decoupling between underlying data and client-held reference will be achieved using `_dataRef` property on ancestor LiveObject class. Resolves DTP-953 --- src/plugins/liveobjects/livecounter.ts | 11 +++++++++ src/plugins/liveobjects/livemap.ts | 34 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/plugins/liveobjects/livecounter.ts create mode 100644 src/plugins/liveobjects/livemap.ts diff --git a/src/plugins/liveobjects/livecounter.ts b/src/plugins/liveobjects/livecounter.ts new file mode 100644 index 000000000..276fd6b99 --- /dev/null +++ b/src/plugins/liveobjects/livecounter.ts @@ -0,0 +1,11 @@ +import { LiveObject } from './liveobject'; + +export interface LiveCounterData { + data: number; +} + +export class LiveCounter extends LiveObject { + protected _getZeroValueData(): LiveCounterData { + return { data: 0 }; + } +} diff --git a/src/plugins/liveobjects/livemap.ts b/src/plugins/liveobjects/livemap.ts new file mode 100644 index 000000000..331d4403e --- /dev/null +++ b/src/plugins/liveobjects/livemap.ts @@ -0,0 +1,34 @@ +import { LiveObject } from './liveobject'; + +export type StateValue = string | number | boolean | Uint8Array; + +export interface ObjectIdStateData { + /** + * A reference to another state object, used to support composable state objects. + */ + objectId: string; +} + +export interface ValueStateData { + /** + * A concrete leaf value in the state object graph. + */ + value: StateValue; +} + +export type StateData = ObjectIdStateData | ValueStateData; + +export interface MapEntry { + // TODO: add tombstone, timeserial + data: StateData; +} + +export interface LiveMapData { + data: Map; +} + +export class LiveMap extends LiveObject { + protected _getZeroValueData(): LiveMapData { + return { data: new Map() }; + } +} From a63a41529b6ae6cce97387665fa84f2b1fd86ad0 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 2 Oct 2024 04:07:09 +0100 Subject: [PATCH 3/4] Add LiveObjectsPool class to store pool of live objects --- scripts/moduleReport.ts | 6 +++++- src/plugins/liveobjects/liveobjectspool.ts | 25 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/plugins/liveobjects/liveobjectspool.ts diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 3714bc93c..7183619c6 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -308,7 +308,11 @@ async function checkLiveObjectsPluginFiles() { const pluginBundleInfo = getBundleInfo('./build/liveobjects.js'); // These are the files that are allowed to contribute >= `threshold` bytes to the LiveObjects bundle. - const allowedFiles = new Set(['src/plugins/liveobjects/index.ts', 'src/plugins/liveobjects/liveobject.ts']); + const allowedFiles = new Set([ + 'src/plugins/liveobjects/index.ts', + 'src/plugins/liveobjects/liveobject.ts', + 'src/plugins/liveobjects/liveobjectspool.ts', + ]); return checkBundleFiles(pluginBundleInfo, allowedFiles, 100); } diff --git a/src/plugins/liveobjects/liveobjectspool.ts b/src/plugins/liveobjects/liveobjectspool.ts new file mode 100644 index 000000000..3431992c1 --- /dev/null +++ b/src/plugins/liveobjects/liveobjectspool.ts @@ -0,0 +1,25 @@ +import { LiveMap } from './livemap'; +import { LiveObject } from './liveobject'; +import { LiveObjects } from './liveobjects'; + +export type ObjectId = string; +export const ROOT_OBJECT_ID = 'root'; + +export class LiveObjectsPool { + private _pool: Map; + + constructor(private _liveObjects: LiveObjects) { + this._pool = this._getInitialPool(); + } + + get(objectId: ObjectId): LiveObject | undefined { + return this._pool.get(objectId); + } + + private _getInitialPool(): Map { + const pool = new Map(); + const root = new LiveMap(this._liveObjects, null, ROOT_OBJECT_ID); + pool.set(root.getObjectId(), root); + return pool; + } +} From d7211a48694a890f9448c7477ed6aec27d7afd75 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 2 Oct 2024 04:07:50 +0100 Subject: [PATCH 4/4] Add LiveObjectsPool to LiveObjects and naive implementation of `getRoot` method --- scripts/moduleReport.ts | 1 + src/plugins/liveobjects/liveobjects.ts | 9 ++++++ test/common/modules/private_api_recorder.js | 1 + test/realtime/live_objects.test.js | 31 +++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/scripts/moduleReport.ts b/scripts/moduleReport.ts index 7183619c6..dce162b9c 100644 --- a/scripts/moduleReport.ts +++ b/scripts/moduleReport.ts @@ -311,6 +311,7 @@ async function checkLiveObjectsPluginFiles() { const allowedFiles = new Set([ 'src/plugins/liveobjects/index.ts', 'src/plugins/liveobjects/liveobject.ts', + 'src/plugins/liveobjects/liveobjects.ts', 'src/plugins/liveobjects/liveobjectspool.ts', ]); diff --git a/src/plugins/liveobjects/liveobjects.ts b/src/plugins/liveobjects/liveobjects.ts index 6dfed511f..6ba94384f 100644 --- a/src/plugins/liveobjects/liveobjects.ts +++ b/src/plugins/liveobjects/liveobjects.ts @@ -1,12 +1,21 @@ import type BaseClient from 'common/lib/client/baseclient'; import type RealtimeChannel from 'common/lib/client/realtimechannel'; +import { LiveMap } from './livemap'; +import { LiveObjectsPool, ROOT_OBJECT_ID } from './liveobjectspool'; export class LiveObjects { private _client: BaseClient; private _channel: RealtimeChannel; + private _liveObjectsPool: LiveObjectsPool; constructor(channel: RealtimeChannel) { this._channel = channel; this._client = channel.client; + this._liveObjectsPool = new LiveObjectsPool(this); + } + + async getRoot(): Promise { + // TODO: wait for SYNC sequence to finish to return root + return this._liveObjectsPool.get(ROOT_OBJECT_ID) as LiveMap; } } diff --git a/test/common/modules/private_api_recorder.js b/test/common/modules/private_api_recorder.js index 57cc6c55d..848004242 100644 --- a/test/common/modules/private_api_recorder.js +++ b/test/common/modules/private_api_recorder.js @@ -44,6 +44,7 @@ define(['test/support/output_directory_paths'], function (outputDirectoryPaths) 'call.http._getHosts', 'call.http.checkConnectivity', 'call.http.doUri', + 'call.LiveObject.getObjectId', 'call.msgpack.decode', 'call.msgpack.encode', 'call.presence._myMembers.put', diff --git a/test/realtime/live_objects.test.js b/test/realtime/live_objects.test.js index e24d6a002..e66a882a8 100644 --- a/test/realtime/live_objects.test.js +++ b/test/realtime/live_objects.test.js @@ -47,6 +47,37 @@ define(['ably', 'shared_helper', 'async', 'chai', 'live_objects'], function ( const channel = client.channels.get('channel'); expect(channel.liveObjects.constructor.name).to.equal('LiveObjects'); }); + + describe('LiveObjects instance', () => { + /** @nospec */ + it('getRoot() returns LiveMap instance', async function () { + const helper = this.test.helper; + const client = LiveObjectsRealtime(helper); + + await helper.monitorConnectionThenCloseAndFinish(async () => { + const channel = client.channels.get('channel'); + const liveObjects = channel.liveObjects; + const root = await liveObjects.getRoot(); + + expect(root.constructor.name).to.equal('LiveMap'); + }, client); + }); + + /** @nospec */ + it('getRoot() returns live object with id "root"', async function () { + const helper = this.test.helper; + const client = LiveObjectsRealtime(helper); + + await helper.monitorConnectionThenCloseAndFinish(async () => { + const channel = client.channels.get('channel'); + const liveObjects = channel.liveObjects; + const root = await liveObjects.getRoot(); + + helper.recordPrivateApi('call.LiveObject.getObjectId'); + expect(root.getObjectId()).to.equal('root'); + }, client); + }); + }); }); }); });