-
-
Notifications
You must be signed in to change notification settings - Fork 772
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
Improve Actor
typings
#3225
Improve Actor
typings
#3225
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,43 @@ | ||
import {isWorker, isSafari} from './util'; | ||
import {serialize, deserialize} from './web_worker_transfer'; | ||
import {isWorker} from './util'; | ||
import {serialize, deserialize, Serialized} from './web_worker_transfer'; | ||
import {ThrottledInvoker} from './throttled_invoker'; | ||
|
||
import type {Transferable} from '../types/transferable'; | ||
import type {Cancelable} from '../types/cancelable'; | ||
import type {WorkerSource} from '../source/worker_source'; | ||
|
||
export interface ActorTarget { | ||
addEventListener: typeof window.addEventListener; | ||
removeEventListener: typeof window.removeEventListener; | ||
postMessage: typeof window.postMessage; | ||
terminate?: () => void; | ||
} | ||
|
||
export interface WorkerSourceProvider { | ||
getWorkerSource(mapId: string, sourceType: string, sourceName: string): WorkerSource; | ||
} | ||
|
||
export type MessageType = '<response>' | '<cancel>' | | ||
'geojson.getClusterExpansionZoom' | 'geojson.getClusterChildren' | 'geojson.getClusterLeaves' | 'geojson.loadData' | | ||
'removeSource' | 'loadWorkerSource' | 'loadDEMTile' | 'removeDEMTile' | | ||
'removeTile' | 'reloadTile' | 'abortTile' | 'loadTile' | 'getTile' | | ||
'getGlyphs' | 'getImages' | 'setImages' | | ||
'syncRTLPluginState' | 'setReferrer' | 'setLayers' | 'updateLayers'; | ||
|
||
export type MessageData = { | ||
id: string; | ||
type: MessageType; | ||
data?: Serialized; | ||
targetMapId?: string | null; | ||
mustQueue?: boolean; | ||
error?: Serialized | null; | ||
hasCallback?: boolean; | ||
sourceMapId: string | null; | ||
} | ||
|
||
export type Message = { | ||
data: MessageData; | ||
} | ||
|
||
/** | ||
* An implementation of the [Actor design pattern](http://en.wikipedia.org/wiki/Actor_model) | ||
|
@@ -12,36 +46,30 @@ import type {Cancelable} from '../types/cancelable'; | |
* owned by the styles | ||
*/ | ||
export class Actor { | ||
target: any; | ||
parent: any; | ||
target: ActorTarget; | ||
parent: WorkerSourceProvider; | ||
mapId: string | null; | ||
callbacks: { | ||
number: any; | ||
}; | ||
callbacks: { [x: number]: Function}; | ||
name: string; | ||
tasks: { | ||
number: any; | ||
}; | ||
taskQueue: Array<number>; | ||
cancelCallbacks: { | ||
number: Cancelable; | ||
}; | ||
tasks: { [x: number]: MessageData }; | ||
taskQueue: Array<string>; | ||
cancelCallbacks: { [x: number]: () => void }; | ||
invoker: ThrottledInvoker; | ||
globalScope: any; | ||
globalScope: ActorTarget; | ||
|
||
/** | ||
* @param target - The target | ||
* @param parent - The parent | ||
* @param mapId - A unique identifier for the Map instance using this Actor. | ||
*/ | ||
constructor(target: any, parent: any, mapId?: string) { | ||
constructor(target: ActorTarget, parent: WorkerSourceProvider, mapId?: string) { | ||
this.target = target; | ||
this.parent = parent; | ||
this.mapId = mapId; | ||
this.callbacks = {} as { number: any }; | ||
this.tasks = {} as { number: any }; | ||
this.callbacks = {}; | ||
this.tasks = {}; | ||
this.taskQueue = []; | ||
this.cancelCallbacks = {} as { number: Cancelable }; | ||
this.cancelCallbacks = {}; | ||
this.invoker = new ThrottledInvoker(this.process); | ||
this.target.addEventListener('message', this.receive, false); | ||
this.globalScope = isWorker() ? target : window; | ||
|
@@ -55,7 +83,7 @@ export class Actor { | |
* @param targetMapId - A particular mapId to which to send this message. | ||
*/ | ||
send( | ||
type: string, | ||
type: MessageType, | ||
data: unknown, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it in scope for this PR to make data argument typed so you know you are passing the right data into it? Here's what I landed on in maplibre-contour: Something like: type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
export type MessageType = KeysOfType<WorkerType, (a: any, b: any, c: WorkerTileCallback) => void>;
send(
type: MessageType,
data: Parameters<WorkerType[MessageType]>[1],
callback?: Function | null,
targetMapId?: string | null,
mustQueue: boolean = false
): Cancelable { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is OK for this PR, I'm thinking about creating a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense - the typing gets pretty gnarly to make that work, makes sense to set up the typing we want on the new clean-slate implementation and clean things up as we migrate to it. |
||
callback?: Function | null, | ||
targetMapId?: string | null, | ||
|
@@ -69,40 +97,36 @@ export class Actor { | |
if (callback) { | ||
this.callbacks[id] = callback; | ||
} | ||
const buffers: Array<Transferable> = isSafari(this.globalScope) ? undefined : []; | ||
this.target.postMessage({ | ||
const buffers: Array<Transferable> = []; | ||
const message: MessageData = { | ||
id, | ||
type, | ||
hasCallback: !!callback, | ||
targetMapId, | ||
mustQueue, | ||
sourceMapId: this.mapId, | ||
data: serialize(data, buffers) | ||
}, buffers); | ||
}; | ||
|
||
this.target.postMessage(message, {transfer: buffers}); | ||
return { | ||
cancel: () => { | ||
if (callback) { | ||
// Set the callback to null so that it never fires after the request is aborted. | ||
delete this.callbacks[id]; | ||
} | ||
this.target.postMessage({ | ||
const cancelMessage: MessageData = { | ||
id, | ||
type: '<cancel>', | ||
targetMapId, | ||
sourceMapId: this.mapId | ||
}); | ||
}; | ||
this.target.postMessage(cancelMessage); | ||
} | ||
}; | ||
} | ||
|
||
receive = (message: { | ||
data: { | ||
id: number; | ||
type: string; | ||
data: unknown; | ||
targetMapId?: string | null; | ||
mustQueue: boolean; | ||
};}) => { | ||
receive = (message: Message) => { | ||
const data = message.data; | ||
const id = data.id; | ||
|
||
|
@@ -164,7 +188,7 @@ export class Actor { | |
this.processTask(id, task); | ||
}; | ||
|
||
processTask(id: number, task: any) { | ||
processTask(id: string, task: MessageData) { | ||
if (task.type === '<response>') { | ||
// The done() function in the counterpart has been called, and we are now | ||
// firing the callback in the originating actor, if there is one. | ||
|
@@ -180,30 +204,31 @@ export class Actor { | |
} | ||
} else { | ||
let completed = false; | ||
const buffers: Array<Transferable> = isSafari(this.globalScope) ? undefined : []; | ||
const buffers: Array<Transferable> = []; | ||
const done = task.hasCallback ? (err: Error, data?: any) => { | ||
completed = true; | ||
delete this.cancelCallbacks[id]; | ||
this.target.postMessage({ | ||
const responseMessage: MessageData = { | ||
id, | ||
type: '<response>', | ||
sourceMapId: this.mapId, | ||
error: err ? serialize(err) : null, | ||
data: serialize(data, buffers) | ||
}, buffers); | ||
}; | ||
this.target.postMessage(responseMessage, {transfer: buffers}); | ||
} : (_) => { | ||
completed = true; | ||
}; | ||
|
||
let callback = null; | ||
const params = (deserialize(task.data) as any); | ||
let callback: Cancelable = null; | ||
const params = deserialize(task.data); | ||
if (this.parent[task.type]) { | ||
// task.type == 'loadTile', 'removeTile', etc. | ||
callback = this.parent[task.type](task.sourceMapId, params, done); | ||
} else if (this.parent.getWorkerSource) { | ||
// task.type == sourcetype.method | ||
const keys = task.type.split('.'); | ||
const scope = (this.parent as any).getWorkerSource(task.sourceMapId, keys[0], params.source); | ||
const scope = this.parent.getWorkerSource(task.sourceMapId, keys[0], (params as any).source); | ||
callback = scope[keys[1]](params, done); | ||
} else { | ||
// No function was found. | ||
|
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.
I think you can also do
keyof Worker
here instead of enumerating these to get them automatically from the methods defined inworker.ts
.The
geojson.*
ones wouldn't come for free, but we could probably either do some fancy typescript work to create them automatically from a list of WorkerSource types, or just make explicit methods on workergeojsonGetClusterLeaves
andgetjsonLoadData
since there's only 2 of them.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.
I agree, but this is not the way I would like to move forward, I don't think that a message type should be a method name, it's not refactoring-safe, what I would like to do going forward is "register" an even type with a specific method, much like pub-sub and introduce promises to this entire mess.
I think this would be the first step toward reducing the number of callbacks from the code.
But I want this initial step to have more clarity on how to change things and how to proceed.
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 geojson part seems like a hack I'll be removing once this will be refactored to something that is easy to read and maintain.
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.
It might help to see some pseudocode of the end state you're looking for. Is it something like:
?
I think it's possible to get the existing "method of worker" approach to work with promises, but you've got a good point that we've got a chance to rethink this now so don't need to feel bound by the old implementation.
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.
Yes, something similar to what you wrote, I still don't have a good picture of how this will look as I need to understand most of the places this is used and how to best define it.
For example I leaned that
Style
class is registering itself to providegetGlyphs
method to the workers. It took me too long to figure out why and where the registration occures and how this works.It shouldn't be that complicated...
So I need to map the events and see how I can slowly progress and to it bit by bit...