Skip to content

Commit

Permalink
Handle createOp field on StateObject messages
Browse files Browse the repository at this point in the history
Refactors LiveMap/LiveCounter object creation and moves most of the
creation related busy work inside those classes.

Resolves DTP-1076
  • Loading branch information
VeskeR committed Nov 22, 2024
1 parent 86fc932 commit 567ad15
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 274 deletions.
128 changes: 77 additions & 51 deletions src/plugins/liveobjects/livecounter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject';
import { LiveObjects } from './liveobjects';
import { StateCounter, StateCounterOp, StateMessage, StateOperation, StateOperationAction } from './statemessage';
import { DefaultTimeserial, Timeserial } from './timeserial';
import { StateCounterOp, StateMessage, StateObject, StateOperation, StateOperationAction } from './statemessage';
import { DefaultTimeserial } from './timeserial';

export interface LiveCounterData extends LiveObjectData {
data: number;
Expand All @@ -12,46 +12,29 @@ export interface LiveCounterUpdate extends LiveObjectUpdate {
}

export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate> {
constructor(
liveObjects: LiveObjects,
private _created: boolean,
initialData?: LiveCounterData | null,
objectId?: string,
siteTimeserials?: Record<string, Timeserial>,
) {
super(liveObjects, initialData, objectId, siteTimeserials);
}

/**
* Returns a {@link LiveCounter} instance with a 0 value.
*
* @internal
*/
static zeroValue(
liveobjects: LiveObjects,
isCreated: boolean,
objectId?: string,
siteTimeserials?: Record<string, Timeserial>,
): LiveCounter {
return new LiveCounter(liveobjects, isCreated, null, objectId, siteTimeserials);
}

value(): number {
return this._dataRef.data;
static zeroValue(liveobjects: LiveObjects, objectId: string): LiveCounter {
return new LiveCounter(liveobjects, objectId);
}

/**
* Returns a {@link LiveCounter} instance based on the provided state object.
* The provided state object must hold a valid counter object data.
*
* @internal
*/
isCreated(): boolean {
return this._created;
static fromStateObject(liveobjects: LiveObjects, stateObject: StateObject): LiveCounter {
const obj = new LiveCounter(liveobjects, stateObject.objectId);
obj.overrideWithStateObject(stateObject);
return obj;
}

/**
* @internal
*/
setCreated(created: boolean): void {
this._created = created;
value(): number {
return this._dataRef.data;
}

/**
Expand Down Expand Up @@ -83,7 +66,7 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
let update: LiveCounterUpdate | LiveObjectUpdateNoop;
switch (op.action) {
case StateOperationAction.COUNTER_CREATE:
update = this._applyCounterCreate(op.counter);
update = this._applyCounterCreate(op);
break;

case StateOperationAction.COUNTER_INC:
Expand All @@ -107,15 +90,69 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
this.notifyUpdated(update);
}

/**
* @internal
*/
overrideWithStateObject(stateObject: StateObject): LiveCounterUpdate {
if (stateObject.objectId !== this.getObjectId()) {
throw new this._client.ErrorInfo(
`Invalid state object: state object objectId=${stateObject.objectId}; LiveCounter objectId=${this.getObjectId()}`,
50000,
500,
);
}

if (!this._client.Utils.isNil(stateObject.createOp)) {
// it is expected that create operation can be missing in the state object, so only validate it when it exists
if (stateObject.createOp.objectId !== this.getObjectId()) {
throw new this._client.ErrorInfo(
`Invalid state object: state object createOp objectId=${stateObject.createOp?.objectId}; LiveCounter objectId=${this.getObjectId()}`,
50000,
500,
);
}

if (stateObject.createOp.action !== StateOperationAction.COUNTER_CREATE) {
throw new this._client.ErrorInfo(
`Invalid state object: state object createOp action=${stateObject.createOp?.action}; LiveCounter objectId=${this.getObjectId()}`,
50000,
500,
);
}
}

const previousDataRef = this._dataRef;
// override all relevant data for this object with data from the state object
this._createOperationIsMerged = false;
this._dataRef = { data: stateObject.counter?.count ?? 0 };
this._siteTimeserials = this._timeserialMapFromStringMap(stateObject.siteTimeserials);
if (!this._client.Utils.isNil(stateObject.createOp)) {
this._mergeInitialDataFromCreateOperation(stateObject.createOp);
}

return this._updateFromDataDiff(previousDataRef, this._dataRef);
}

protected _getZeroValueData(): LiveCounterData {
return { data: 0 };
}

protected _updateFromDataDiff(currentDataRef: LiveCounterData, newDataRef: LiveCounterData): LiveCounterUpdate {
const counterDiff = newDataRef.data - currentDataRef.data;
protected _updateFromDataDiff(prevDataRef: LiveCounterData, newDataRef: LiveCounterData): LiveCounterUpdate {
const counterDiff = newDataRef.data - prevDataRef.data;
return { update: { inc: counterDiff } };
}

protected _mergeInitialDataFromCreateOperation(stateOperation: StateOperation): LiveCounterUpdate {
// if a counter object is missing for the COUNTER_CREATE op, the initial value is implicitly 0 in this case.
// note that it is intentional to SUM the incoming count from the create op.
// if we got here, it means that current counter instance is missing the initial value in its data reference,
// which we're going to add now.
this._dataRef.data += stateOperation.counter?.count ?? 0;
this._createOperationIsMerged = true;

return { update: { inc: stateOperation.counter?.count ?? 0 } };
}

private _throwNoPayloadError(op: StateOperation): void {
throw new this._client.ErrorInfo(
`No payload found for ${op.action} op for LiveCounter objectId=${this.getObjectId()}`,
Expand All @@ -124,32 +161,21 @@ export class LiveCounter extends LiveObject<LiveCounterData, LiveCounterUpdate>
);
}

private _applyCounterCreate(op: StateCounter | undefined): LiveCounterUpdate | LiveObjectUpdateNoop {
if (this.isCreated()) {
// skip COUNTER_CREATE op if this counter is already created
private _applyCounterCreate(op: StateOperation): LiveCounterUpdate | LiveObjectUpdateNoop {
if (this._createOperationIsMerged) {
// There can't be two different create operation for the same object id, because the object id
// fully encodes that operation. This means we can safely ignore any new incoming create operations
// if we already merged it once.
this._client.Logger.logAction(
this._client.logger,
this._client.Logger.LOG_MICRO,
'LiveCounter._applyCounterCreate()',
`skipping applying COUNTER_CREATE op on a counter instance as it is already created; objectId=${this._objectId}`,
`skipping applying COUNTER_CREATE op on a counter instance as it was already applied before; objectId=${this._objectId}`,
);
return { noop: true };
}

if (this._client.Utils.isNil(op)) {
// if a counter object is missing for the COUNTER_CREATE op, the initial value is implicitly 0 in this case.
// we need to SUM the initial value to the current value due to the reasons below, but since it's a 0, we can skip addition operation
this.setCreated(true);
return { update: { inc: 0 } };
}

// note that it is intentional to SUM the incoming count from the create op.
// if we get here, it means that current counter instance wasn't initialized from the COUNTER_CREATE op,
// so it is missing the initial value that we're going to add now.
this._dataRef.data += op.count ?? 0;
this.setCreated(true);

return { update: { inc: op.count ?? 0 } };
return this._mergeInitialDataFromCreateOperation(op);
}

private _applyCounterInc(op: StateCounterOp): LiveCounterUpdate {
Expand Down
Loading

0 comments on commit 567ad15

Please sign in to comment.