-
Notifications
You must be signed in to change notification settings - Fork 116
Web SDK Refactor: Add Model Class #1267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6d28936
add model class
fadi-george ae48a70
simplify model methods by having a simple get and set method
fadi-george ea343c0
remove unneeded PreferenceOneSignalKeys and PreferencePlayerPurchases…
fadi-george 1ad90f6
clean up
fadi-george ddba512
address pr feedback
fadi-george File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| import { EventProducer } from 'src/shared/helpers/EventProducer'; | ||
| import { IEventNotifier } from 'src/types/events'; | ||
| import { ModelChangeTags } from 'src/types/models'; | ||
|
|
||
| /** | ||
| * Implement `IModelChangedHandler` and subscribe implementation via `Model.subscribe` to | ||
| * be notified when the `Model` has changed. | ||
| */ | ||
| export interface IModelChangedHandler { | ||
| /** | ||
| * Called when the subscribed model has been changed. | ||
| * | ||
| * @param args Information related to what has changed. | ||
| * @param tag The tag which identifies how/why the model was changed. | ||
| */ | ||
| onChanged(args: ModelChangedArgs, tag: string): void; | ||
| } | ||
|
|
||
| /** | ||
| * The arguments passed to the IModelChangedHandler when subscribed via Model.subscribe | ||
| */ | ||
| export interface ModelChangedArgs { | ||
| /** | ||
| * The full model in its current state. | ||
| */ | ||
| model: Model; | ||
|
|
||
| /** | ||
| * The path to the property, from the root Model, that has changed. | ||
| * This can be a dot notation path like: | ||
| * - `mapProperty.new_key` | ||
| * - `complexProperty.simpleProperty` | ||
| * - `complexProperty.mapProperty.new_key` | ||
| */ | ||
| path: string; | ||
|
|
||
| /** | ||
| * The property that was changed. | ||
| */ | ||
| property: string; | ||
|
|
||
| /** | ||
| * The old value of the property, prior to it being changed. | ||
| */ | ||
| oldValue: unknown; | ||
|
|
||
| /** | ||
| * The new value of the property, after it has been changed. | ||
| */ | ||
| newValue: unknown; | ||
| } | ||
|
|
||
| // Implements logic similar to Android SDK's Model | ||
| // Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/Model.kt | ||
| /** | ||
| * The base class for a Model. A model is effectively a map of data, each key in the map being | ||
| * a property of the model, each value in the map being the property value. A property can be | ||
| * one of the following values: | ||
| * | ||
| * 1. A simple type. | ||
| * 2. An instance of Model type. | ||
| * 2. An Array of simple types. | ||
| * 3. An Array of Model types. | ||
| * | ||
| * Simple Types | ||
| * ------------ | ||
| * Boolean | ||
| * String | ||
| * Number | ||
| * | ||
| * When a Model is nested (a property is a Model type or Array of Model types) the child | ||
| * Model is owned and initialized by the parent Model. | ||
| * | ||
| * When a structured schema should be enforced this class should be extended, the base class | ||
| * utilizing Properties with getters/setters that wrap getProperty and setProperty calls | ||
| * to the underlying data. | ||
| * | ||
| * When a more dynamic schema is needed, the MapModel class can be used, which bridges a | ||
| * Map and Model. | ||
| * | ||
| * Deserialization | ||
| * --------------- | ||
| * When deserializing a flat Model nothing specific is required. However if the Model | ||
| * is nested the createModelForProperty and/or createListForProperty needs to be implemented | ||
| * to aide in the deserialization process. | ||
| */ | ||
| export class Model implements IEventNotifier<IModelChangedHandler> { | ||
| /** | ||
| * A unique identifier for this model. | ||
| */ | ||
| get id(): string { | ||
| return this.getProperty<string>('id'); | ||
| } | ||
|
|
||
| set id(value: string) { | ||
| this.setProperty<string>('id', value); | ||
| } | ||
|
|
||
| protected data: Map<string, unknown> = new Map(); | ||
| private changeNotifier = new EventProducer<IModelChangedHandler>(); | ||
|
|
||
| /** | ||
| * | ||
| * @param _parentModel The optional parent model. When specified this model is a child model, any changes | ||
| * to this model will *also* be propagated up to it's parent for notification. When | ||
| * this is specified, must also specify _parentProperty | ||
| * @param _parentProperty The optional parent model property that references this model. When this is | ||
| * specified, must also specify _parentModel | ||
| */ | ||
| constructor( | ||
| private _parentModel: Model | null = null, | ||
| private readonly _parentProperty: string | null = null, | ||
| ) { | ||
| if ((_parentModel == null) !== (_parentProperty == null)) { | ||
| throw new Error( | ||
| 'Parent model and parent property must both be set or both be null.', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Initialize this model from a JSON object. Each key-value-pair in the JSON object | ||
| * will be deserialized into this model, recursively if needed. | ||
| * | ||
| * @param object The JSON object to initialize this model from. | ||
| */ | ||
| initializeFromJson(object: object): void { | ||
| this.data.clear(); | ||
| this.data = new Map(Object.entries(object)); | ||
| } | ||
|
|
||
| /** | ||
| * Initialize this model from another Model. The model provided will be replicated | ||
| * within this model. | ||
| * | ||
| * @param id The id of the model to initialize to. | ||
| * @param model The model to initialize this model from. | ||
| */ | ||
| initializeFromModel(id: string | null, model: Model): void { | ||
| const newData = new Map<string, unknown>(); | ||
|
|
||
| model.data.forEach((value: unknown, key: string) => { | ||
| if (value instanceof Model) { | ||
| const childModel = value as Model; | ||
| childModel['_parentModel'] = this; | ||
| newData.set(key, childModel); | ||
| } else { | ||
| newData.set(key, value); | ||
| } | ||
| }); | ||
|
|
||
| if (id !== null) { | ||
| newData.set('id', id); | ||
| } | ||
|
|
||
| this.data.clear(); | ||
| this.data = newData; | ||
| } | ||
|
|
||
| /** | ||
| * Called via initializeFromJson when the property being initialized is a JSON object, | ||
| * indicating the property value should be set to a nested Model. The specific concrete | ||
| * class of Model for this property is determined by the implementor and should depend on | ||
| * the property provided. | ||
| * | ||
| * @param property The property that is to contain the Model created by this method. | ||
| * @param jsonObject The JSON object that the Model will be created/initialized from. | ||
| * | ||
| * @return The created Model, or null if the property should not be set. | ||
| */ | ||
| protected createModelForProperty( | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _property: string, | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _jsonObject: object, | ||
| ): Model | null { | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Called via initializeFromJson when the property being initialized is a JSON array, | ||
| * indicating the property value should be set to an Array. The specific concrete class | ||
| * inside the Array for this property is determined by the implementor and should depend | ||
| * on the property provided. | ||
| * | ||
| * @param property The property that is to contain the Array created by this method. | ||
| * @param jsonArray The JSON array that the Array will be created/initialized from. | ||
| * | ||
| * @return The created Array, or null if the property should not be set. | ||
| */ | ||
| protected createListForProperty( | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _property: string, | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _jsonArray: unknown[], | ||
| ): unknown[] | null { | ||
| return null; | ||
| } | ||
|
|
||
| setProperty<T>( | ||
| name: string, | ||
| value: T | null, | ||
| tag: string = ModelChangeTags.NORMAL, | ||
| forceChange = false, | ||
| ): void { | ||
| const oldValue = this.data.get(name); | ||
|
|
||
| if (oldValue === value && !forceChange) { | ||
| return; | ||
| } | ||
|
|
||
| if (value !== null && value !== undefined) { | ||
| this.data.set(name, value); | ||
| } else if (this.data.has(name)) { | ||
| this.data.delete(name); | ||
| } | ||
|
|
||
| this.notifyChanged(name, name, tag, oldValue, value); | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether the provided property is currently set in this model. | ||
| * | ||
| * @param name The name of the property to test for. | ||
| * | ||
| * @return True if this model has the provided property, false otherwise. | ||
| */ | ||
| hasProperty(name: string): boolean { | ||
| return this.data.has(name); | ||
| } | ||
|
|
||
| getProperty<T = unknown | null>(name: string): T { | ||
| const value = this.data.get(name); | ||
| return value as T; | ||
| } | ||
|
|
||
| private notifyChanged( | ||
| path: string, | ||
| property: string, | ||
| tag: string, | ||
| oldValue: unknown, | ||
| newValue: unknown, | ||
| ): void { | ||
| // if there are any changed listeners for this specific model, notify them. | ||
| const changeArgs: ModelChangedArgs = { | ||
| model: this, | ||
| path, | ||
| property, | ||
| oldValue, | ||
| newValue, | ||
| }; | ||
| this.changeNotifier.fire((handler) => handler.onChanged(changeArgs, tag)); | ||
|
|
||
| // if there is a parent model, propagate the change up to the parent for it's own processing. | ||
| if (this._parentModel !== null) { | ||
| const parentPath = `${this._parentProperty}.${path}`; | ||
| this._parentModel.notifyChanged( | ||
| parentPath, | ||
| property, | ||
| tag, | ||
| oldValue, | ||
| newValue, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Serialize this model to a JSON object, recursively if required. | ||
| * | ||
| * @return The resulting JSON object. | ||
| */ | ||
| toJSON(): object { | ||
| return Object.fromEntries(this.data.entries()); | ||
| } | ||
|
|
||
| subscribe(handler: IModelChangedHandler): void { | ||
| return this.changeNotifier.subscribe(handler); | ||
| } | ||
|
|
||
| unsubscribe(handler: IModelChangedHandler): void { | ||
| this.changeNotifier.unsubscribe(handler); | ||
| } | ||
|
|
||
| get hasSubscribers(): boolean { | ||
| return this.changeNotifier.hasSubscribers; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { IEventNotifier } from 'src/types/events'; | ||
|
|
||
| export class EventProducer<THandler> implements IEventNotifier<THandler> { | ||
| private subscribers: THandler[] = []; | ||
|
|
||
| get hasSubscribers(): boolean { | ||
| return this.subscribers.length > 0; | ||
| } | ||
|
|
||
| subscribe(handler: THandler): void { | ||
| this.subscribers.push(handler); | ||
| } | ||
|
|
||
| unsubscribe(handler: THandler): void { | ||
| const index = this.subscribers.indexOf(handler); | ||
| if (index !== -1) { | ||
| this.subscribers.splice(index, 1); | ||
| } | ||
| } | ||
|
|
||
| subscribeAll(from: EventProducer<THandler>): void { | ||
| for (const handler of from.subscribers) { | ||
| this.subscribe(handler); | ||
| } | ||
| } | ||
|
|
||
| fire(callback: (handler: THandler) => void): void { | ||
| for (const handler of this.subscribers) { | ||
| callback(handler); | ||
| } | ||
| } | ||
|
|
||
| async suspendingFire( | ||
| callback: (handler: THandler) => Promise<void>, | ||
| ): Promise<void> { | ||
| for (const handler of this.subscribers) { | ||
| await callback(handler); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /** | ||
| * A generic interface which indicates the implementer has the ability to notify events through the | ||
| * generic THandler interface specified. When implemented, any outside component may subscribe | ||
| * to the events being notified. When an event is to be raised, the implementor | ||
| * will call a method within THandler, the method(s) defined therein are entirely dependent on | ||
| * the implementor/definition. | ||
| * | ||
| * Unlike ICallbackNotifier, there can be one zero or more event subscribers at any given time. | ||
| * | ||
| * @template THandler The type that the implementor is expecting to raise events to. | ||
| */ | ||
| export interface IEventNotifier<THandler> { | ||
| /** | ||
| * Whether there are currently any subscribers. | ||
| */ | ||
| hasSubscribers: boolean; | ||
|
|
||
| /** | ||
| * Subscribe to listen for events. | ||
| * | ||
| * @param handler The handler that will be called when the event(s) occur. | ||
| */ | ||
| subscribe(handler: THandler): void; | ||
|
|
||
| /** | ||
| * Unsubscribe to no longer listen for events. | ||
| * | ||
| * @param handler The handler that was previous registered via subscribe. | ||
| */ | ||
| unsubscribe(handler: THandler): void; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll need to double check this, but I believe this line should return either the Model if it exists or
null. I understand that in the Java implementationcreateModelForPropertyis meant to be overriden but I'm not completely sure if that translates 1-to-1 to typescript.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it looks like the jsdoc mentions two parameters but none are passed in.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added them back in. createModelForProperty is overridable. See.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The override keyword is just a check if were actually overriding a base class method.