Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 101 additions & 271 deletions ably.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/plugins/objects/batchcontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class DefaultBatchContext implements AnyBatchContext {
throw new this._client.ErrorInfo('Cannot set a key on a non-LiveMap instance', 92007, 400);
}
this._rootContext.queueMessages(async () =>
LiveMap.createMapSetMessage(this._realtimeObject, this._instance.id()!, key, value as Primitive),
LiveMap.createMapSetMessage(this._realtimeObject, this._instance.id()!, key, value),
);
}

Expand Down
6 changes: 3 additions & 3 deletions src/plugins/objects/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
InstanceSubscriptionEvent,
LiveObject as LiveObjectType,
Primitive,
SubscribeResponse,
Subscription,
Value,
} from '../../../ably';
import { LiveCounter } from './livecounter';
Expand Down Expand Up @@ -169,11 +169,11 @@ export class DefaultInstance<T extends Value> implements AnyInstance<T> {
return this._value.decrement(amount ?? 1);
}

subscribe(listener: EventCallback<InstanceSubscriptionEvent<T>>): SubscribeResponse {
subscribe(listener: EventCallback<InstanceSubscriptionEvent<T>>): Subscription {
if (!(this._value instanceof LiveObject)) {
throw new this._client.ErrorInfo('Cannot subscribe to a non-LiveObject instance', 92007, 400);
}
return this._value.instanceSubscribe((event: InstanceEvent) => {
return this._value.subscribe((event: InstanceEvent) => {
listener({
object: this as unknown as Instance<T>,
message: event.message?.toUserFacingMessage(this._realtimeObject.getChannel()),
Expand Down
59 changes: 24 additions & 35 deletions src/plugins/objects/livemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
ObjectsMapEntry,
ObjectsMapOp,
ObjectsMapSemantics,
PrimitiveObjectValue,
} from './objectmessage';
import { RealtimeObject } from './realtimeobject';

Expand All @@ -24,7 +23,7 @@ export interface ObjectIdObjectData {

export interface ValueObjectData {
/** A decoded leaf value from {@link WireObjectData}. */
value: string | number | boolean | Buffer | ArrayBuffer | API.JsonArray | API.JsonObject;
value: API.Primitive;
}

export type LiveMapObjectData = ObjectIdObjectData | ValueObjectData;
Expand All @@ -40,13 +39,18 @@ export interface LiveMapData extends LiveObjectData {
data: Map<string, LiveMapEntry>; // RTLM3
}

export interface LiveMapUpdate<T extends API.LiveMapType> extends LiveObjectUpdate {
export interface LiveMapUpdate<T extends Record<string, API.Value>> extends LiveObjectUpdate {
update: { [keyName in keyof T & string]?: 'updated' | 'removed' };
_type: 'LiveMapUpdate';
}

/** @spec RTLM1, RTLM2 */
export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, LiveMapUpdate<T>> {
export class LiveMap<T extends Record<string, API.Value> = Record<string, API.Value>>
extends LiveObject<LiveMapData, LiveMapUpdate<T>>
implements API.LiveMap<T>
{
declare readonly [API.__livetype]: 'LiveMap'; // type-only, unique symbol to satisfy branded interfaces, no JS emitted

constructor(
realtimeObject: RealtimeObject,
private _semantics: ObjectsMapSemantics,
Expand All @@ -61,8 +65,8 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
* @internal
* @spec RTLM4
*/
static zeroValue<T extends API.LiveMapType>(realtimeObject: RealtimeObject, objectId: string): LiveMap<T> {
return new LiveMap<T>(realtimeObject, ObjectsMapSemantics.LWW, objectId);
static zeroValue(realtimeObject: RealtimeObject, objectId: string): LiveMap {
return new LiveMap(realtimeObject, ObjectsMapSemantics.LWW, objectId);
}

/**
Expand All @@ -71,23 +75,20 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
*
* @internal
*/
static fromObjectState<T extends API.LiveMapType>(
realtimeObject: RealtimeObject,
objectMessage: ObjectMessage,
): LiveMap<T> {
const obj = new LiveMap<T>(realtimeObject, objectMessage.object!.map!.semantics!, objectMessage.object!.objectId);
static fromObjectState(realtimeObject: RealtimeObject, objectMessage: ObjectMessage): LiveMap {
const obj = new LiveMap(realtimeObject, objectMessage.object!.map!.semantics!, objectMessage.object!.objectId);
obj.overrideWithObjectState(objectMessage);
return obj;
}

/**
* @internal
*/
static async createMapSetMessage<TKey extends keyof API.LiveMapType & string>(
static async createMapSetMessage(
realtimeObject: RealtimeObject,
objectId: string,
key: TKey,
value: API.LiveMapType[TKey] | LiveCounterValueType | LiveMapValueType,
key: string,
value: API.Value,
): Promise<ObjectMessage[]> {
const client = realtimeObject.getClient();

Expand All @@ -111,12 +112,8 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,

const typedObjectData: ObjectIdObjectData = { objectId: mapCreateMsg.operation?.objectId! };
objectData = typedObjectData;
} else if (value instanceof LiveObject) {
// TODO: remove this branch when LiveObject is no longer directly supported as a map value
const typedObjectData: ObjectIdObjectData = { objectId: value.getObjectId() };
objectData = typedObjectData;
} else {
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
const typedObjectData: ValueObjectData = { value: value as API.Primitive };
objectData = typedObjectData;
}

Expand All @@ -141,11 +138,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
/**
* @internal
*/
static createMapRemoveMessage<TKey extends keyof API.LiveMapType & string>(
realtimeObject: RealtimeObject,
objectId: string,
key: TKey,
): ObjectMessage {
static createMapRemoveMessage(realtimeObject: RealtimeObject, objectId: string, key: string): ObjectMessage {
const client = realtimeObject.getClient();

if (typeof key !== 'string') {
Expand All @@ -170,11 +163,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
/**
* @internal
*/
static validateKeyValue<TKey extends keyof API.LiveMapType & string>(
realtimeObject: RealtimeObject,
key: TKey,
value: API.LiveMapType[TKey] | LiveCounterValueType | LiveMapValueType,
): void {
static validateKeyValue(realtimeObject: RealtimeObject, key: string, value: API.Value): void {
const client = realtimeObject.getClient();

if (typeof key !== 'string') {
Expand Down Expand Up @@ -209,19 +198,19 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
this._realtimeObject.throwIfInvalidAccessApiConfiguration(); // RTLM5b, RTLM5c

if (this.isTombstoned()) {
return undefined as T[TKey];
return undefined;
}

const element = this._dataRef.data.get(key);

// RTLM5d1
if (element === undefined) {
return undefined as T[TKey];
return undefined;
}

// RTLM5d2a
if (element.tombstone === true) {
return undefined as T[TKey];
return undefined;
}

// data always exists for non-tombstoned elements
Expand Down Expand Up @@ -507,7 +496,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
*
* @internal
*/
compact(): { [K in keyof T]: any } {
compact(): API.CompactedValue<API.LiveMap<T>> {
const result: Record<keyof T, any> = {} as Record<keyof T, any>;

// Use public entries() method to ensure we only include publicly exposed properties
Expand Down Expand Up @@ -911,7 +900,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
/**
* Returns value as is if object data stores a primitive type, or a reference to another LiveObject from the pool if it stores an objectId.
*/
private _getResolvedValueFromObjectData(data: LiveMapObjectData): PrimitiveObjectValue | LiveObject | undefined {
private _getResolvedValueFromObjectData(data: LiveMapObjectData): API.Value | undefined {
// if object data stores primitive value, just return it as is.
const primitiveValue = (data as ValueObjectData).value;
if (primitiveValue != null) {
Expand All @@ -930,7 +919,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
return undefined;
}

return refObject; // RTLM5d2f2
return refObject as unknown as API.LiveObject; // RTLM5d2f2
}

private _isMapEntryTombstoned(entry: LiveMapEntry): boolean {
Expand Down
8 changes: 2 additions & 6 deletions src/plugins/objects/livemapvaluetype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ObjectOperationAction,
ObjectsMapEntry,
ObjectsMapSemantics,
PrimitiveObjectValue,
} from './objectmessage';
import { RealtimeObject } from './realtimeobject';

Expand Down Expand Up @@ -75,10 +74,7 @@ export class LiveMapValueType<T extends Record<string, API.Value> = Record<strin
throw new client.ErrorInfo('Map entries should be a key-value object', 40003, 400);
}

// TODO: fix as any type assertion when LiveMap type is updated to support new path based types
Object.entries(entries ?? {}).forEach(([key, value]) =>
LiveMap.validateKeyValue(realtimeObject, key, value as any),
);
Object.entries(entries ?? {}).forEach(([key, value]) => LiveMap.validateKeyValue(realtimeObject, key, value));

const { initialValueOperation, nestedObjectsCreateMsgs } = await LiveMapValueType._createInitialValueOperation(
realtimeObject,
Expand Down Expand Up @@ -142,7 +138,7 @@ export class LiveMapValueType<T extends Record<string, API.Value> = Record<strin
objectData = typedObjectData;
} else {
// Handle primitive values
const typedObjectData: ValueObjectData = { value: value as PrimitiveObjectValue };
const typedObjectData: ValueObjectData = { value: value as API.Primitive };
objectData = typedObjectData;
}

Expand Down
48 changes: 3 additions & 45 deletions src/plugins/objects/liveobject.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type BaseClient from 'common/lib/client/baseclient';
import type EventEmitter from 'common/lib/util/eventemitter';
import type { EventCallback, LiveMapType, SubscribeResponse } from '../../../ably';
import type { EventCallback, Subscription } from '../../../ably';
import { ROOT_OBJECT_ID } from './constants';
import { InstanceEvent } from './instance';
import { LiveMapUpdate } from './livemap';
import { ObjectData, ObjectMessage, ObjectOperation } from './objectmessage';
import { PathEvent } from './pathobjectsubscriptionregister';
import { RealtimeObject } from './realtimeobject';
Expand Down Expand Up @@ -32,23 +31,12 @@ export interface LiveObjectUpdateNoop {
noop: true;
}

export enum LiveObjectLifecycleEvent {
deleted = 'deleted',
}

export type LiveObjectLifecycleEventCallback = () => void;

export interface OnLiveObjectLifecycleEventResponse {
off(): void;
}

export abstract class LiveObject<
TData extends LiveObjectData = LiveObjectData,
TUpdate extends LiveObjectUpdate = LiveObjectUpdate,
> {
protected _client: BaseClient;
protected _subscriptions: EventEmitter;
protected _lifecycleEvents: EventEmitter;
protected _objectId: string;
/**
* Represents an aggregated value for an object, which combines the initial value for an object from the create operation,
Expand All @@ -71,7 +59,6 @@ export abstract class LiveObject<
) {
this._client = this._realtimeObject.getClient();
this._subscriptions = new this._client.EventEmitter(this._client.logger);
this._lifecycleEvents = new this._client.EventEmitter(this._client.logger);
this._objectId = objectId;
this._dataRef = this._getZeroValueData();
// use empty map of serials by default, so any future operation can be applied to this object
Expand All @@ -81,7 +68,7 @@ export abstract class LiveObject<
this._parentReferences = new Map<LiveObject, Set<string>>();
}

instanceSubscribe(listener: EventCallback<InstanceEvent>): SubscribeResponse {
subscribe(listener: EventCallback<InstanceEvent>): Subscription {
this._realtimeObject.throwIfInvalidAccessApiConfiguration();

this._subscriptions.on(LiveObjectSubscriptionEvent.updated, listener);
Expand All @@ -93,33 +80,6 @@ export abstract class LiveObject<
return { unsubscribe };
}

on(event: LiveObjectLifecycleEvent, callback: LiveObjectLifecycleEventCallback): OnLiveObjectLifecycleEventResponse {
// this public API method can be called without specific configuration, so checking for invalid settings is unnecessary.
this._lifecycleEvents.on(event, callback);

const off = () => {
this._lifecycleEvents.off(event, callback);
};

return { off };
}

off(event: LiveObjectLifecycleEvent, callback: LiveObjectLifecycleEventCallback): void {
// this public API method can be called without specific configuration, so checking for invalid settings is unnecessary.

// prevent accidentally calling .off without any arguments on an EventEmitter and removing all callbacks
if (this._client.Utils.isNil(event) && this._client.Utils.isNil(callback)) {
return;
}

this._lifecycleEvents.off(event, callback);
}

offAll(): void {
// this public API method can be called without specific configuration, so checking for invalid settings is unnecessary.
this._lifecycleEvents.off();
}

/**
* @internal
*/
Expand Down Expand Up @@ -170,8 +130,6 @@ export abstract class LiveObject<
update.objectMessage = objectMessage;
update.tombstone = true;

this._lifecycleEvents.emit(LiveObjectLifecycleEvent.deleted);

return update;
}

Expand Down Expand Up @@ -347,7 +305,7 @@ export abstract class LiveObject<

// For LiveMapUpdate, also create non-bubbling events for each updated key
if (update._type === 'LiveMapUpdate') {
const updatedKeys = Object.keys((update as LiveMapUpdate<LiveMapType>).update);
const updatedKeys = Object.keys(update.update);

for (const key of updatedKeys) {
for (const basePath of paths) {
Expand Down
4 changes: 1 addition & 3 deletions src/plugins/objects/objectmessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ export enum ObjectsMapSemantics {
LWW = 0,
}

export type PrimitiveObjectValue = string | number | boolean | Buffer | ArrayBuffer | JsonArray | JsonObject;

/**
* An ObjectData represents a value in an object on a channel decoded from {@link WireObjectData}.
* @spec OD1
Expand All @@ -43,7 +41,7 @@ export interface ObjectData {
/** A reference to another object, used to support composable object structures. */
objectId?: string; // OD2a
/** A decoded leaf value from {@link WireObjectData}. */
value?: PrimitiveObjectValue;
value?: API.Primitive;
}

/**
Expand Down
5 changes: 2 additions & 3 deletions src/plugins/objects/objectspool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type BaseClient from 'common/lib/client/baseclient';
import type * as API from '../../../ably';
import { ROOT_OBJECT_ID } from './constants';
import { DEFAULTS } from './defaults';
import { LiveCounter } from './livecounter';
Expand Down Expand Up @@ -31,8 +30,8 @@ export class ObjectsPool {
return this._pool.get(objectId);
}

getRoot<T extends API.LiveMapType = API.AblyDefaultObject>(): LiveMap<T> {
return this._pool.get(ROOT_OBJECT_ID) as LiveMap<T>;
getRoot(): LiveMap {
return this._pool.get(ROOT_OBJECT_ID) as LiveMap;
}

/**
Expand Down
Loading
Loading