Skip to content

Commit 0232d30

Browse files
committed
fix tests and remove preferences keys usage
1 parent 71d53d6 commit 0232d30

8 files changed

Lines changed: 234 additions & 73 deletions

File tree

__test__/unit/core/operationCache.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
import { LegacyOperation } from 'src/core/operationRepo/LegacyOperation';
12
import ModelCache from '../../../src/core/caching/ModelCache';
23
import OperationCache from '../../../src/core/caching/OperationCache';
34
import { CoreChangeType } from '../../../src/core/models/CoreChangeType';
45
import { ModelName } from '../../../src/core/models/SupportedModels';
5-
import { Operation } from '../../../src/core/operationRepo/Operation';
6+
import { TestEnvironment } from '../../support/environment/TestEnvironment';
67
import {
78
getDummyIdentityOSModel,
89
getMockDeltas,
910
} from '../../support/helpers/core';
10-
import { TestEnvironment } from '../../support/environment/TestEnvironment';
1111

1212
describe('OperationCache: operation results in correct operation queue result', () => {
1313
beforeEach(async () => {
@@ -23,7 +23,7 @@ describe('OperationCache: operation results in correct operation queue result',
2323
});
2424

2525
test('Add operation to cache -> operation queue +1', async () => {
26-
const operation = new Operation(
26+
const operation = new LegacyOperation(
2727
CoreChangeType.Add,
2828
ModelName.Identity,
2929
getMockDeltas(),
@@ -36,7 +36,7 @@ describe('OperationCache: operation results in correct operation queue result',
3636
});
3737

3838
test('Remove operation from cache -> operation queue -1', async () => {
39-
const operation = new Operation(
39+
const operation = new LegacyOperation(
4040
CoreChangeType.Add,
4141
ModelName.Identity,
4242
getMockDeltas(),
@@ -54,12 +54,12 @@ describe('OperationCache: operation results in correct operation queue result',
5454
});
5555

5656
test('Add multiple operations to cache -> operation queue +2', async () => {
57-
const operation = new Operation(
57+
const operation = new LegacyOperation(
5858
CoreChangeType.Add,
5959
ModelName.Identity,
6060
getMockDeltas(),
6161
);
62-
const operation2 = new Operation(
62+
const operation2 = new LegacyOperation(
6363
CoreChangeType.Add,
6464
ModelName.Identity,
6565
getMockDeltas(),
@@ -75,12 +75,12 @@ describe('OperationCache: operation results in correct operation queue result',
7575
});
7676

7777
test('Flush operation cache -> operation queue 0', async () => {
78-
const operation = new Operation(
78+
const operation = new LegacyOperation(
7979
CoreChangeType.Add,
8080
ModelName.Identity,
8181
getMockDeltas(),
8282
);
83-
const operation2 = new Operation(
83+
const operation2 = new LegacyOperation(
8484
CoreChangeType.Add,
8585
ModelName.Identity,
8686
getMockDeltas(),

__test__/unit/core/requestService/userPropertyRequests.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Operation } from '../../../../src/core/operationRepo/Operation';
1+
import { LegacyOperation } from 'src/core/operationRepo/LegacyOperation';
2+
import { ExecutorResultFailNotRetriable } from '../../../../src/core/executors/ExecutorResult';
3+
import { OSModel } from '../../../../src/core/modelRepo/OSModel';
24
import { CoreChangeType } from '../../../../src/core/models/CoreChangeType';
3-
import UserPropertyRequests from '../../../../src/core/requestService/UserPropertyRequests';
45
import { ModelName } from '../../../../src/core/models/SupportedModels';
56
import { UserPropertiesModel } from '../../../../src/core/models/UserPropertiesModel';
6-
import { OSModel } from '../../../../src/core/modelRepo/OSModel';
7-
import { ExecutorResultFailNotRetriable } from '../../../../src/core/executors/ExecutorResult';
7+
import UserPropertyRequests from '../../../../src/core/requestService/UserPropertyRequests';
88
import Database from '../../../../src/shared/services/Database';
99

1010
const getJWTTokenSpy = vi.spyOn(Database.prototype, 'getJWTToken');
@@ -22,7 +22,7 @@ describe('User Property Request tests', () => {
2222
property: 'tags',
2323
newValue: { key: 'value' },
2424
};
25-
const operation = new Operation<UserPropertiesModel>(
25+
const operation = new LegacyOperation<UserPropertiesModel>(
2626
CoreChangeType.Update,
2727
ModelName.Properties,
2828
[delta],

src/core/caching/OperationCache.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Log from '../../shared/libraries/Log';
22
import { logMethodCall } from '../../shared/utils/utils';
33
import { ModelName, SupportedModel } from '../models/SupportedModels';
4-
import { Operation } from '../operationRepo/Operation';
4+
import { LegacyOperation } from '../operationRepo/LegacyOperation';
55

66
export default class OperationCache {
7-
static enqueue<Model>(operation: Operation<Model>): void {
7+
static enqueue<Model>(operation: LegacyOperation<Model>): void {
88
logMethodCall('OperationCache.enqueue', { operation });
99
const fromCache = localStorage.getItem('operationCache');
1010
const operations: { [key: string]: unknown } = fromCache
@@ -16,22 +16,23 @@ export default class OperationCache {
1616

1717
static getOperationsWithModelName(
1818
modelName: ModelName,
19-
): Operation<SupportedModel>[] {
19+
): LegacyOperation<SupportedModel>[] {
2020
const fromCache = localStorage.getItem('operationCache');
21-
const rawOperations: Operation<SupportedModel>[] = fromCache
21+
const rawOperations: LegacyOperation<SupportedModel>[] = fromCache
2222
? Object.values(JSON.parse(fromCache))
2323
: [];
24-
const operations: Operation<SupportedModel>[] = [];
24+
const operations: LegacyOperation<SupportedModel>[] = [];
2525

2626
for (let i = 0; i < rawOperations.length; i++) {
2727
const rawOperation = rawOperations[i];
2828

2929
try {
3030
// return an operation object with correct references (in particular reference to the model)
31-
const operation = Operation.getInstanceWithModelReference(rawOperation);
31+
const operation =
32+
LegacyOperation.getInstanceWithModelReference(rawOperation);
3233

3334
if (operation) {
34-
operations.push(operation as Operation<SupportedModel>);
35+
operations.push(operation as LegacyOperation<SupportedModel>);
3536
}
3637
} catch (e) {
3738
Log.warn(

src/core/modelRepo/ModelStore.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
// Implements logic similar to Android SDK's ModelStore
2+
// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt
13
import { EventProducer } from 'src/shared/helpers/EventProducer';
24
import Log from 'src/shared/libraries/Log';
35
import type { IEventNotifier } from 'src/types/events';
46
import type { IModelStore, IModelStoreChangeHandler } from 'src/types/models';
5-
import {
6-
PreferenceOneSignalKeys,
7-
PreferenceStores,
8-
type IPreferencesService,
9-
} from 'src/types/preferences';
10-
import type { IModelChangedHandler, Model, ModelChangedArgs } from './Model';
7+
import type { IPreferencesService } from 'src/types/preferences';
8+
import type {
9+
IModelChangedHandler,
10+
Model,
11+
ModelChangedArgs,
12+
} from '../models/Model';
13+
const STORE = 'OneSignal_ModelStore';
1114

1215
/**
1316
* The abstract implementation of a model store. Implements all but the `create` method,
@@ -141,11 +144,7 @@ export abstract class ModelStore<TModel extends Model>
141144
protected load(): void {
142145
if (!this.name || !this._prefs) return;
143146

144-
const str = this._prefs.getValue<string>(
145-
PreferenceStores.ONESIGNAL,
146-
PreferenceOneSignalKeys.MODEL_STORE_PREFIX + this.name,
147-
'[]',
148-
);
147+
const str = this._prefs.getValue<string>(STORE, this.name, '[]');
149148

150149
const jsonArray = JSON.parse(str);
151150
const shouldRePersist = this.models.length > 0;
@@ -188,11 +187,7 @@ export abstract class ModelStore<TModel extends Model>
188187
jsonArray.push(model.toJSON());
189188
}
190189

191-
this._prefs.setValue<string>(
192-
PreferenceStores.ONESIGNAL,
193-
PreferenceOneSignalKeys.MODEL_STORE_PREFIX + this.name,
194-
JSON.stringify(jsonArray),
195-
);
190+
this._prefs.setValue<string>(STORE, this.name, JSON.stringify(jsonArray));
196191
}
197192

198193
subscribe(handler: IModelStoreChangeHandler<TModel>): void {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import OneSignalError from '../../shared/errors/OneSignalError';
2+
import Database from '../../shared/services/Database';
3+
import { OSModel } from '../modelRepo/OSModel';
4+
import { CoreChangeType } from '../models/CoreChangeType';
5+
import { CoreDelta } from '../models/CoreDeltas';
6+
import { ModelName, SupportedModel } from '../models/SupportedModels';
7+
import { isPropertyDelta, isPureObject } from '../utils/typePredicates';
8+
9+
export class LegacyOperation<Model> {
10+
operationId: string;
11+
timestamp: number;
12+
payload?: Partial<SupportedModel>;
13+
model?: OSModel<Model>;
14+
applyToRecordId?: string;
15+
jwtTokenAvailable: Promise<void>;
16+
jwtToken?: string | null;
17+
18+
constructor(
19+
readonly changeType: CoreChangeType,
20+
readonly modelName: ModelName,
21+
deltas?: CoreDelta<Model>[],
22+
) {
23+
this.operationId = Math.random().toString(36).substring(2);
24+
this.payload = deltas ? this.getPayload(deltas) : undefined;
25+
this.model = deltas ? deltas[deltas.length - 1].model : undefined;
26+
this.applyToRecordId = deltas?.[deltas.length - 1]?.applyToRecordId;
27+
this.timestamp = Date.now();
28+
// eslint-disable-next-line no-async-promise-executor
29+
this.jwtTokenAvailable = new Promise<void>(async (resolve) => {
30+
this.jwtToken = await Database.getJWTToken();
31+
resolve();
32+
});
33+
}
34+
35+
private getPayload(deltas: CoreDelta<Model>[]): any {
36+
// for update operations, we send the aggregated property-level changes
37+
if (isPropertyDelta(deltas[0])) {
38+
return this.aggregateDeltas(deltas);
39+
}
40+
41+
// for add and remove operations, we send the model data itself
42+
return deltas[0].model.data;
43+
}
44+
45+
private aggregateDeltas(deltas: CoreDelta<Model>[]): {
46+
[key: string]: unknown;
47+
} {
48+
const result: { [key: string]: Record<string, unknown> } = {};
49+
50+
deltas.forEach((delta) => {
51+
if (isPropertyDelta(delta)) {
52+
/**
53+
* If the delta is a property delta, we need to check if the property is an object.
54+
* If the object has been previously set, we need to merge the new value with the old value.
55+
* Example:
56+
* 1. Initial value: { a: { b: 1 } }
57+
* 2. Delta 1: { a: { b: 2 } }
58+
* 3. Delta 2: { a: { c: 3 } }
59+
* 4. Result: { a: { b: 2, c: 3 } }
60+
*/
61+
// eslint-disable-next-line no-prototype-builtins
62+
const hasExistingProperty = result.hasOwnProperty(delta.property);
63+
const newValueIsPureObject = isPureObject(delta.newValue);
64+
const oldValueIsPureObject = isPureObject(delta.oldValue);
65+
const isNewAndOldCompatible =
66+
newValueIsPureObject === oldValueIsPureObject ||
67+
delta.oldValue === undefined;
68+
69+
if (!isNewAndOldCompatible) {
70+
throw new Error('Cannot merge incompatible values');
71+
}
72+
73+
const shouldMergeExistingAndNew =
74+
hasExistingProperty && newValueIsPureObject;
75+
76+
const mergedObject = { ...result[delta.property], ...delta.newValue };
77+
78+
result[delta.property] = shouldMergeExistingAndNew
79+
? mergedObject
80+
: delta.newValue;
81+
}
82+
});
83+
return result;
84+
}
85+
86+
static getInstanceWithModelReference(
87+
rawOperation: LegacyOperation<SupportedModel>,
88+
): LegacyOperation<SupportedModel> | undefined {
89+
const { operationId, payload, modelName, changeType, timestamp, model } =
90+
rawOperation;
91+
if (!model) {
92+
throw new OneSignalError('Operation.fromJSON: model is undefined');
93+
}
94+
const osModel = OneSignal.coreDirector?.getModelByTypeAndId(
95+
modelName,
96+
model.modelId,
97+
);
98+
99+
if (!!osModel) {
100+
const operation = new LegacyOperation<SupportedModel>(
101+
changeType,
102+
modelName,
103+
);
104+
operation.model = osModel;
105+
operation.operationId = operationId;
106+
operation.timestamp = timestamp;
107+
operation.payload = payload;
108+
operation.jwtToken = rawOperation.jwtToken;
109+
operation.jwtTokenAvailable = Promise.resolve();
110+
return operation;
111+
} else {
112+
throw new Error(
113+
`Could not find model with id ${model.modelId} of type ${modelName}. Maybe user logged out?`,
114+
);
115+
}
116+
}
117+
}

src/core/operationRepo/Operation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export const GroupComparisonType = {
66
NONE: 2,
77
} as const;
88

9+
export type GroupComparisonValue =
10+
(typeof GroupComparisonType)[keyof typeof GroupComparisonType];
11+
912
export abstract class Operation extends Model {
1013
constructor(name: string) {
1114
super();
@@ -42,7 +45,7 @@ export abstract class Operation extends Model {
4245
* The comparison type to use when this operation is the starting operation, in terms of
4346
* which operations can be grouped with it.
4447
*/
45-
abstract get groupComparisonType(): typeof GroupComparisonType;
48+
abstract get groupComparisonType(): GroupComparisonValue;
4649

4750
/**
4851
* Whether the operation can currently execute given its current state.

0 commit comments

Comments
 (0)