Skip to content

Commit 5f9da2c

Browse files
committed
Infer key name of the LiveMap in its direct subscription update object
Resolves PUB-1094
1 parent 84c1643 commit 5f9da2c

File tree

3 files changed

+42
-28
lines changed

3 files changed

+42
-28
lines changed

ably.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,7 +2299,7 @@ export declare interface BatchContextLiveCounter {
22992299
*
23002300
* Keys must be strings. Values can be another {@link LiveObject}, or a primitive type, such as a string, number, boolean, or binary data (see {@link ObjectValue}).
23012301
*/
2302-
export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveMapUpdate> {
2302+
export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveMapUpdate<T>> {
23032303
/**
23042304
* Returns the value associated with a given key. Returns `undefined` if the key doesn't exist in a map or if the associated {@link LiveObject} has been deleted.
23052305
*
@@ -2359,13 +2359,13 @@ export declare interface LiveMap<T extends LiveMapType> extends LiveObject<LiveM
23592359
/**
23602360
* Represents an update to a {@link LiveMap} object, describing the keys that were updated or removed.
23612361
*/
2362-
export declare interface LiveMapUpdate extends LiveObjectUpdate {
2362+
export declare interface LiveMapUpdate<T extends LiveMapType> extends LiveObjectUpdate {
23632363
/**
23642364
* An object containing keys from a `LiveMap` that have changed, along with their change status:
23652365
* - `updated` - the value of a key in the map was updated.
23662366
* - `removed` - the key was removed from the map.
23672367
*/
2368-
update: { [keyName: string]: 'updated' | 'removed' };
2368+
update: { [keyName in keyof T & string]?: 'updated' | 'removed' };
23692369
}
23702370

23712371
/**

src/plugins/objects/livemap.ts

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ export interface LiveMapData extends LiveObjectData {
4747
data: Map<string, LiveMapEntry>;
4848
}
4949

50-
export interface LiveMapUpdate extends LiveObjectUpdate {
51-
update: { [keyName: string]: 'updated' | 'removed' };
50+
export interface LiveMapUpdate<T extends API.LiveMapType> extends LiveObjectUpdate {
51+
update: { [keyName in keyof T & string]?: 'updated' | 'removed' };
5252
}
5353

54-
export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, LiveMapUpdate> {
54+
export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, LiveMapUpdate<T>> {
5555
constructor(
5656
objects: Objects,
5757
private _semantics: MapSemantics,
@@ -391,7 +391,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
391391
return;
392392
}
393393

394-
let update: LiveMapUpdate | LiveObjectUpdateNoop;
394+
let update: LiveMapUpdate<T> | LiveObjectUpdateNoop;
395395
switch (op.action) {
396396
case ObjectOperationAction.MAP_CREATE:
397397
update = this._applyMapCreate(op);
@@ -435,7 +435,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
435435
/**
436436
* @internal
437437
*/
438-
overrideWithObjectState(objectState: ObjectState): LiveMapUpdate | LiveObjectUpdateNoop {
438+
overrideWithObjectState(objectState: ObjectState): LiveMapUpdate<T> | LiveObjectUpdateNoop {
439439
if (objectState.objectId !== this.getObjectId()) {
440440
throw new this._client.ErrorInfo(
441441
`Invalid object state: object state objectId=${objectState.objectId}; LiveMap objectId=${this.getObjectId()}`,
@@ -526,21 +526,23 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
526526
return { data: new Map<string, LiveMapEntry>() };
527527
}
528528

529-
protected _updateFromDataDiff(prevDataRef: LiveMapData, newDataRef: LiveMapData): LiveMapUpdate {
530-
const update: LiveMapUpdate = { update: {} };
529+
protected _updateFromDataDiff(prevDataRef: LiveMapData, newDataRef: LiveMapData): LiveMapUpdate<T> {
530+
const update: LiveMapUpdate<T> = { update: {} };
531531

532532
for (const [key, currentEntry] of prevDataRef.data.entries()) {
533+
const typedKey: keyof T & string = key;
533534
// any non-tombstoned properties that exist on a current map, but not in the new data - got removed
534-
if (currentEntry.tombstone === false && !newDataRef.data.has(key)) {
535-
update.update[key] = 'removed';
535+
if (currentEntry.tombstone === false && !newDataRef.data.has(typedKey)) {
536+
update.update[typedKey] = 'removed';
536537
}
537538
}
538539

539540
for (const [key, newEntry] of newDataRef.data.entries()) {
540-
if (!prevDataRef.data.has(key)) {
541+
const typedKey: keyof T & string = key;
542+
if (!prevDataRef.data.has(typedKey)) {
541543
// if property does not exist in the current map, but new data has it as a non-tombstoned property - got updated
542544
if (newEntry.tombstone === false) {
543-
update.update[key] = 'updated';
545+
update.update[typedKey] = 'updated';
544546
continue;
545547
}
546548

@@ -551,17 +553,17 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
551553
}
552554

553555
// properties that exist both in current and new map data need to have their values compared to decide on the update type
554-
const currentEntry = prevDataRef.data.get(key)!;
556+
const currentEntry = prevDataRef.data.get(typedKey)!;
555557

556558
// compare tombstones first
557559
if (currentEntry.tombstone === true && newEntry.tombstone === false) {
558560
// current prop is tombstoned, but new is not. it means prop was updated to a meaningful value
559-
update.update[key] = 'updated';
561+
update.update[typedKey] = 'updated';
560562
continue;
561563
}
562564
if (currentEntry.tombstone === false && newEntry.tombstone === true) {
563565
// current prop is not tombstoned, but new is. it means prop was removed
564-
update.update[key] = 'removed';
566+
update.update[typedKey] = 'removed';
565567
continue;
566568
}
567569
if (currentEntry.tombstone === true && newEntry.tombstone === true) {
@@ -572,28 +574,28 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
572574
// both props exist and are not tombstoned, need to compare values with deep equals to see if it was changed
573575
const valueChanged = !deepEqual(currentEntry.data, newEntry.data, { strict: true });
574576
if (valueChanged) {
575-
update.update[key] = 'updated';
577+
update.update[typedKey] = 'updated';
576578
continue;
577579
}
578580
}
579581

580582
return update;
581583
}
582584

583-
protected _mergeInitialDataFromCreateOperation(objectOperation: ObjectOperation): LiveMapUpdate {
585+
protected _mergeInitialDataFromCreateOperation(objectOperation: ObjectOperation): LiveMapUpdate<T> {
584586
if (this._client.Utils.isNil(objectOperation.map)) {
585587
// if a map object is missing for the MAP_CREATE op, the initial value is implicitly an empty map.
586588
// in this case there is nothing to merge into the current map, so we can just end processing the op.
587589
return { update: {} };
588590
}
589591

590-
const aggregatedUpdate: LiveMapUpdate | LiveObjectUpdateNoop = { update: {} };
592+
const aggregatedUpdate: LiveMapUpdate<T> = { update: {} };
591593
// in order to apply MAP_CREATE op for an existing map, we should merge their underlying entries keys.
592594
// we can do this by iterating over entries from MAP_CREATE op and apply changes on per-key basis as if we had MAP_SET, MAP_REMOVE operations.
593595
Object.entries(objectOperation.map.entries ?? {}).forEach(([key, entry]) => {
594596
// for a MAP_CREATE operation we must use the serial value available on an entry, instead of a serial on a message
595597
const opSerial = entry.timeserial;
596-
let update: LiveMapUpdate | LiveObjectUpdateNoop;
598+
let update: LiveMapUpdate<T> | LiveObjectUpdateNoop;
597599
if (entry.tombstone === true) {
598600
// entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op
599601
update = this._applyMapRemove({ key }, opSerial);
@@ -624,7 +626,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
624626
);
625627
}
626628

627-
private _applyMapCreate(op: ObjectOperation): LiveMapUpdate | LiveObjectUpdateNoop {
629+
private _applyMapCreate(op: ObjectOperation): LiveMapUpdate<T> | LiveObjectUpdateNoop {
628630
if (this._createOperationIsMerged) {
629631
// There can't be two different create operation for the same object id, because the object id
630632
// fully encodes that operation. This means we can safely ignore any new incoming create operations
@@ -649,7 +651,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
649651
return this._mergeInitialDataFromCreateOperation(op);
650652
}
651653

652-
private _applyMapSet(op: MapOp, opSerial: string | undefined): LiveMapUpdate | LiveObjectUpdateNoop {
654+
private _applyMapSet(op: MapOp, opSerial: string | undefined): LiveMapUpdate<T> | LiveObjectUpdateNoop {
653655
const { ErrorInfo, Utils } = this._client;
654656

655657
const existingEntry = this._dataRef.data.get(op.key);
@@ -698,10 +700,15 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
698700
};
699701
this._dataRef.data.set(op.key, newEntry);
700702
}
701-
return { update: { [op.key]: 'updated' } };
703+
704+
const update: LiveMapUpdate<T> = { update: {} };
705+
const typedKey: keyof T & string = op.key;
706+
update.update[typedKey] = 'updated';
707+
708+
return update;
702709
}
703710

704-
private _applyMapRemove(op: MapOp, opSerial: string | undefined): LiveMapUpdate | LiveObjectUpdateNoop {
711+
private _applyMapRemove(op: MapOp, opSerial: string | undefined): LiveMapUpdate<T> | LiveObjectUpdateNoop {
705712
const existingEntry = this._dataRef.data.get(op.key);
706713
if (existingEntry && !this._canApplyMapOperation(existingEntry.timeserial, opSerial)) {
707714
// the operation's serial <= the entry's serial, ignore the operation.
@@ -729,7 +736,11 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
729736
this._dataRef.data.set(op.key, newEntry);
730737
}
731738

732-
return { update: { [op.key]: 'removed' } };
739+
const update: LiveMapUpdate<T> = { update: {} };
740+
const typedKey: keyof T & string = op.key;
741+
update.update[typedKey] = 'removed';
742+
743+
return update;
733744
}
734745

735746
/**

test/package/browser/template/src/index-objects.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ globalThis.testAblyPackage = async function () {
4242

4343
// check LiveMap subscription callback has correct TypeScript types
4444
const { unsubscribe } = root.subscribe(({ update }) => {
45-
switch (update.someKey) {
45+
// check update object infers keys from map type
46+
const typedKeyOnMap = update.stringKey;
47+
switch (typedKeyOnMap) {
4648
case 'removed':
4749
case 'updated':
50+
case undefined:
4851
break;
4952
default:
5053
// check all possible types are exhausted
51-
const shouldExhaustAllTypes: never = update.someKey;
54+
const shouldExhaustAllTypes: never = typedKeyOnMap;
5255
}
5356
});
5457
unsubscribe();

0 commit comments

Comments
 (0)