mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 08:25:32 +00:00
Merge branch 'lmlayer-init' into lmlayer-build-reorg
This commit is contained in:
commit
cf13caa98e
11 changed files with 521 additions and 488 deletions
|
|
@ -1,408 +1,61 @@
|
|||
Keyman/Language Modelling layer
|
||||
================================
|
||||
Language Modelling Layer (LMLayer)
|
||||
==================================
|
||||
|
||||
This document introduces the protocol for communicating between
|
||||
KeymanWeb and the language modeling layer (i.e., the configurable
|
||||
prediction and suggestion engine, (henceforth referred to as the
|
||||
"LMLayer").
|
||||
Provide predictions and corrections while you type!
|
||||
|
||||
Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
|
||||
See [Worker Communication Protocol](./docs/worker-communication-protocol.md) for a
|
||||
semi-formal specification on how the Worker and the main thread communicate.
|
||||
|
||||
> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
|
||||
> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
|
||||
> "OPTIONAL" in this document are to be interpreted as described in
|
||||
> [RFC 2119].
|
||||
System dependencies
|
||||
-------------------
|
||||
|
||||
[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
|
||||
You will need Bash and Node.js >= 6.0.
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
Communication protocol between keyboard and asynchronous worker
|
||||
---------------------------------------------------------------
|
||||
Run `build.sh`. This will also automatically install dependencies with `npm`.
|
||||
|
||||

|
||||
|
||||
We have decided that everything to the right of the `KeymanWeb` will be
|
||||
in a Web Worker. However, communication can happen only through
|
||||
[`postMessage(data)`][postMessage] commands,
|
||||
where `data` is a serializable object (via the [structured clone][]
|
||||
algorithm).
|
||||
|
||||
What serializable object can we send that will adhere to the [open-closed principle]?
|
||||
|
||||
### Messages
|
||||
|
||||
The idea is to use a [discriminated union][]. The protocol involves
|
||||
plain JavaScript objects with one property called `message` that takes
|
||||
a finite set of `string` values.
|
||||
|
||||
These string values indicate what message should be sent. The rest of
|
||||
the properties in the object are the parameters send with the message.
|
||||
|
||||
```javascript
|
||||
{
|
||||
message: 'predict',
|
||||
// message-specific properties here
|
||||
}
|
||||
```sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
See also: [XML-RPC][]
|
||||
### Two-stage compilation process
|
||||
|
||||
Messages are **not** methods. That is, there is no assumption that
|
||||
a client will receive a reply when a message is sent. However, some
|
||||
pairs of messages, such as `predict` and `suggestions`, lightly assume
|
||||
request/response semantics.
|
||||
Since the primary LMLayer code runs within a [Web Worker][], `build.sh` compiles the
|
||||
LMLayer in two stages:
|
||||
|
||||
[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
|
||||
[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
|
||||
[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
|
||||
[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
|
||||
1. Compile the inner worker code
|
||||
1. Compile the TypeScript sources for _only_ the Worker code.
|
||||
2. Wrap the Worker code as `embedded_worker.js`
|
||||
|
||||
2. Compile the top-level code
|
||||
1. Include `embedded_worker.js` verbatim using a TypeScript directive.
|
||||
2. Compile the top-level TypeScript code.
|
||||
3. Unwrap the Worker code at runtime.
|
||||
|
||||
### Tokens
|
||||
[Web Worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
|
||||
|
||||
Tokens uniquely identify an input event, such as a key press. Since
|
||||
Keyman should ask for an asynchronous prediction on most key presses, the
|
||||
token is intended to associate a prediction and its response with
|
||||
a particular input; Keyman is free to ignore prediction responses if
|
||||
they are for outdated input events.
|
||||
Test
|
||||
----
|
||||
|
||||
The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
|
||||
its contents; it simply uses it to identify a request and pass it back
|
||||
to Keyman. There are a few requirements on the concrete type of the
|
||||
`Token`:
|
||||
This will run both headless unit tests, and in-browser unit tests and integration
|
||||
tests:
|
||||
|
||||
1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
|
||||
2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
|
||||
3. Tokens **MUST** be unique across messages. That is, tokens **MUST
|
||||
NOT** be duplicated between different messages.
|
||||
|
||||
It is up to the keyboard to create unambiguous tokens that can be uniquely
|
||||
identified through the round-trip process.
|
||||
|
||||
In the following examples, I'll use the subset of `number` values that
|
||||
are interpretable as [31-bit signed integers][Smi].
|
||||
|
||||
[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
|
||||
[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
|
||||
[Tokens]: #tokens
|
||||
|
||||
### Example
|
||||
|
||||
An asynchronous message to predict after typing 'D':
|
||||
|
||||
```javascript
|
||||
{
|
||||
message: 'predict',
|
||||
|
||||
token: 1,
|
||||
transform: {
|
||||
insert: 'D',
|
||||
deleteLeft: 0,
|
||||
deleteRight: 0
|
||||
},
|
||||
context: {
|
||||
left: '',
|
||||
right: '',
|
||||
startOfBuffer: true,
|
||||
endOfBuffer: true
|
||||
},
|
||||
}
|
||||
```sh
|
||||
./build.sh -test
|
||||
```
|
||||
|
||||
Message types
|
||||
-------------
|
||||
### Test-Driven Development
|
||||
|
||||
Currently there are four message types:
|
||||
I like to use [entr]() to automatically build and re-run the unit tests anytime I
|
||||
change a source code file. Here's the command I run in separate window:
|
||||
|
||||
Message | Direction | Parameters | Expected reply | Uses token
|
||||
--------------|--------------------|---------------------|---------------------|---------------
|
||||
`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
|
||||
`ready` | LMLayer → keyboard | configuration | No | No
|
||||
`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
|
||||
`suggestions` | LMLayer → keyboard | suggestions | No | Yes
|
||||
|
||||
|
||||
### Message: `initialize`
|
||||
|
||||
Must be sent from the keyboard to the LMLayer so that the LMLayer
|
||||
initializes a model. It will send `initialization` which is a plain
|
||||
JavaScript object specify the path to the model, as well configurations
|
||||
and platform restrictions.
|
||||
|
||||
The keyboard **MUST NOT** send any messages to the LMLayer prior to
|
||||
sending `initialize`. The keyboard **SHOULD NOT** send another message
|
||||
to the keyboard until it receives `ready` message from the LMLayer
|
||||
before sending another message.
|
||||
|
||||
These are the configurations, and platform restrictions sent to
|
||||
initialize the LMLayer and its model.
|
||||
|
||||
```typescript
|
||||
interface InitializeMessage {
|
||||
message: 'initialize';
|
||||
|
||||
/**
|
||||
* Path to the model. There are no concrete restrictions on the path
|
||||
* to the model, so long as the LMLayer can successfully use it to
|
||||
* initialize the model.
|
||||
*/
|
||||
model: string;
|
||||
|
||||
configuration: {
|
||||
/**
|
||||
* Whether the platform supports right contexts.
|
||||
* The absence of this rule implies false.
|
||||
*/
|
||||
supportsRightContexts?: false,
|
||||
|
||||
/**
|
||||
* Whether the platform supports deleting to the right.
|
||||
* The absence of this rule implies false.
|
||||
*/
|
||||
supportsDeleteRight?: false,
|
||||
|
||||
/**
|
||||
* The maximum amount of UTF-16 code units that the keyboard will
|
||||
* provide to the left of the cursor, as an integer.
|
||||
*/
|
||||
maxLeftContextCodeUnits: number,
|
||||
|
||||
/**
|
||||
* The maximum amount of code units that the keyboard will provide to
|
||||
* the right of the cursor, as an integer. The absence of this rule
|
||||
* implies 0.
|
||||
* See also, [[supportsRightContexts]].
|
||||
*/
|
||||
maxRightContextCodeUnits?: number,
|
||||
}
|
||||
}
|
||||
```sh
|
||||
git ls-files | entr -c ./build.sh -tdd
|
||||
```
|
||||
|
||||
Importantly, `./build.sh -tdd` skips running the in-browser tests, and skips
|
||||
downloading/updating `npm` dependencies.
|
||||
|
||||
### Message: `ready`
|
||||
|
||||
Must be sent from the LMLayer to the keyboard when the LMLayer's model
|
||||
as a response to `initialize`. It will send `configuration`, which is
|
||||
a plain JavaScript object requesting configuration from the keyboard.
|
||||
|
||||
There are only two options defined so far:
|
||||
|
||||
```typescript
|
||||
interface ReadyMessage {
|
||||
message: 'ready';
|
||||
configuration: {
|
||||
/**
|
||||
* How many UTF-16 code units maximum to send as the context to the
|
||||
* left of the cursor ("left" in the Unicode character stream).
|
||||
*
|
||||
* Affects the `context` property sent in `predict` messages.
|
||||
*
|
||||
* While the left context MUST NOT bisect surrogate pairs, they MAY
|
||||
* bisect graphical clusters.
|
||||
*/
|
||||
leftContextCodeUnits: number,
|
||||
|
||||
/**
|
||||
* How many UTF-16 code units maximum to send as the context to the
|
||||
* right of the cursor ("right" in the Unicode character stream).
|
||||
*
|
||||
* Affects the `context` property sent in `predict` messages.
|
||||
*
|
||||
* While the left context MUST NOT bisect surrogate pairs, they MAY
|
||||
* bisect graphical clusters.
|
||||
*/
|
||||
rightContextCodeUnits: number,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Message: `predict`
|
||||
|
||||
Sent from the keyboard to the LMLayer whenever a new prediction should
|
||||
be generated. This is typically initiated by a key press event. The
|
||||
keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
|
||||
token **MUST** be unique across all prediction events. The LMLayer
|
||||
**SHOULD** respond to each `predict` message with a `suggestions`
|
||||
message. The `suggestions` message **MUST** contain the corresponding
|
||||
token as sent in the initial `predict` message.
|
||||
|
||||
The keyboard **MUST** send the `context` parameter. The keyboard
|
||||
**SHOULD** send the `transform` parameter. The keyboard **MUST** send
|
||||
a unique token.
|
||||
|
||||
The semantics of the `predict` message **MUST** be from the
|
||||
perspective of this sequence of events:
|
||||
|
||||
1. After the input event is received by the keyboard.
|
||||
2. Before the keyboard applies the associated `transform` to the buffer.
|
||||
|
||||
**NOTE**: The keyboard **MAY** apply the `transform` associated with the
|
||||
input event before receiving the corresponding `suggestions` message
|
||||
from the LMLayer. The intention is that once the suggestions are displayed,
|
||||
the typist may select one of the suggestions in the place of the effects
|
||||
of their original input.
|
||||
|
||||
**NOTE**: The keyboard **MAY** send the `predict` message after it has
|
||||
applied the `transform` associated with the input event to the buffer.
|
||||
Regardless of the actual sequence, the semantics **MUST** remain the
|
||||
same:the prediction happens from the perspective before the `transform`
|
||||
has been applied. Therefore, if the keyboard eagerly transforms the
|
||||
buffer before it has sent `predict` message, it must anticipate _undoing_
|
||||
the `transform` it has already applied if it is to apply a `transfrom`
|
||||
send in the `suggestions` message. The keyboard must act as if it has
|
||||
never applied the `transform` associated with the input event in the
|
||||
first place.
|
||||
|
||||
The context is the text surrounding the insertion point, _before_ the
|
||||
transform is applied to the buffer.
|
||||
|
||||
```typescript
|
||||
interface Context {
|
||||
/**
|
||||
* Up to maxLeftContextCodeUnits code units of Unicode scalar value
|
||||
* (i. e., characters) to the left of the insertion point in the
|
||||
* buffer. If there is nothing to the left of the buffer, this returns
|
||||
* an empty string.
|
||||
*/
|
||||
left: USVString;
|
||||
|
||||
/**
|
||||
* Up to maxRightContextCodeUnits code units of Unicode scalar value
|
||||
* (i. e., characters) to the right of the insertion point in the
|
||||
* buffer. If there is nothing to the right of the buffer, this returns
|
||||
* an empty string.
|
||||
*/
|
||||
right?: USVString;
|
||||
|
||||
/**
|
||||
* Whether the insertion point is at the start of the buffer.
|
||||
*/
|
||||
startOfBuffer: boolean;
|
||||
|
||||
/**
|
||||
* Whether the insertion point is at the end of the buffer.
|
||||
*/
|
||||
endOfBuffer: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
The transform parameter describes how the input event will change the
|
||||
buffer.
|
||||
|
||||
```typescript
|
||||
interface Transform {
|
||||
/**
|
||||
* The Unicode scalar values (i.e., characters) to be inserted at the
|
||||
* cursor position.
|
||||
*
|
||||
* Corresponds to `s` in com.keyman.KeyboardInterface.output.
|
||||
*/
|
||||
insert: USVString;
|
||||
|
||||
/**
|
||||
* The number of code units to delete to the left of the cursor.
|
||||
*
|
||||
* Corresponds to `dn` in com.keyman.KeyboardInterface.output.
|
||||
*/
|
||||
delete: number;
|
||||
|
||||
/**
|
||||
* The number of code units to delete to the right of the cursor.
|
||||
* Not available on all platforms.
|
||||
*/
|
||||
deleteRight?: number;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Message: `suggestions`
|
||||
|
||||
The `suggestions` message is sent from the LMLayer to the keyboard. This
|
||||
message sends a ranked array of suggestions, in descending order of
|
||||
probability (i.e., entry `0` is most likely, followed by entry `1`,
|
||||
etc.). This message **MUST** be in response to a `predict` message, and
|
||||
it **MUST** respond with the corresponding [token].
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* `suggestions` is an ordered array of suggestion objects.
|
||||
* Each suggestion is a transform bundled with a `displayAs` property.
|
||||
*/
|
||||
let suggestions = Suggestion[];
|
||||
|
||||
interface Suggestion {
|
||||
/**
|
||||
* Same object as an input event transform.
|
||||
* Note that the transform is applied AFTER the input event
|
||||
* transform.
|
||||
*/
|
||||
transform: Transform;
|
||||
/**
|
||||
* A string to display the suggestion to the typist.
|
||||
* This should aid the typist understand what the transform
|
||||
* will do to their text.
|
||||
*/
|
||||
displayAs: string;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
let suggestions = [
|
||||
{
|
||||
transform: {
|
||||
insert: 'teapot',
|
||||
deleteLeft: 1,
|
||||
deleteRight: 0
|
||||
},
|
||||
displayAs: '🍵'
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
#### Timing
|
||||
|
||||
Each suggestion provides a `transform`. This transform is applied
|
||||
_after_ the transform associated with the input event that initiated
|
||||
this prediction. That is, the suggested transform applies to the buffer
|
||||
after the transform associated with the input event.
|
||||
|
||||
Rephrased in somewhat mathematical terms:
|
||||
Let 𝑥 ∈ 𝐵 be the input text buffer. Let 𝑇<sub>𝑖</sub> be the transform
|
||||
associated with an input event that maps a text buffer 𝐵 → 𝐵. Let
|
||||
𝑇<sub>𝑠</sub> be a transform suggested through the LMLayer. 𝑦 ∈ 𝐵 is the
|
||||
text buffer after the suggestion transform has been applied. The
|
||||
correct sequence of applications should be as follows:
|
||||
|
||||
> 𝑦 = 𝑇<sub>𝑠</sub>(𝑇<sub>𝑖</sub>(𝑥))
|
||||
|
||||
|
||||
#### Late suggestions
|
||||
|
||||
Sometimes, a `suggestions` message may arrive after an input event has
|
||||
already invalidated its request. This is called a **"late" suggestion**.
|
||||
The LMLayer **MAY** send late suggestions. Consequently, the keyboard
|
||||
**MAY** discard the late suggestions. There is no requirement for the
|
||||
keyboard to acknowledge late suggestions, or for the LMLayer to avoid
|
||||
sending late `suggestions` messages. In either case, a `suggestions`
|
||||
message can be identified as appropriate or "late" via its `token`
|
||||
property.
|
||||
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
- [x] Document `suggestions`
|
||||
- [x] TypeScript!
|
||||
- [ ] make simple `index.html` that demos a dummy model
|
||||
- [x] Update class definitions in README from TypeScript sources.
|
||||
- [ ] Do word segmentation
|
||||
- [ ] Determine the exact arguments given to the model's `predict()`
|
||||
method.
|
||||
- [ ] Make an `error` initialization message.
|
||||
- [ ] Make a `cancel` message.
|
||||
- [ ] LOGLIKEIHOOD IN THE TRANSFORM!
|
||||
- [ ] `possibleTransforms` where `transform` is an alias for `possibleTransforms[0]`
|
||||
- [ ] Use [puppeteer](https://github.com/GoogleChrome/puppeteer)?
|
||||
[entr]: http://eradman.com/entrproject/
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@
|
|||
# Designed for optimal compatibility with the Keyman Suite.
|
||||
#
|
||||
|
||||
# Include some helper functions from resources
|
||||
. ../../resources/shellHelperFunctions.sh
|
||||
|
||||
LMLAYER_OUTPUT=build
|
||||
WORKER_OUTPUT=build/intermediate
|
||||
NAKED_WORKER=$WORKER_OUTPUT/index.js
|
||||
EMBEDDED_WORKER=$WORKER_OUTPUT/embedded_worker.js
|
||||
|
||||
|
||||
# Build the worker and the main script.
|
||||
build ( ) {
|
||||
# Build worker first; the main file depends on it.
|
||||
|
|
@ -53,21 +57,6 @@ display_usage ( ) {
|
|||
echo " -test runs unit and integration tests after building"
|
||||
}
|
||||
|
||||
# Prints a nice, common error message.
|
||||
fail ( ) {
|
||||
# TODO: source shellHelperFunctions.sh
|
||||
local ERROR_RED
|
||||
local NORMAL
|
||||
ERROR_RED="$(tput setaf 1)"
|
||||
NORMAL="$(tput sgr0)"
|
||||
FAILURE_MSG="$1"
|
||||
if [[ "$FAILURE_MSG" == "" ]]; then
|
||||
FAILURE_MSG="Unknown failure."
|
||||
fi
|
||||
echo "$0: ${ERROR_RED}$FAILURE_MSG${NORMAL}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Creates embedded_worker.js. Must be run after the worker is built for the
|
||||
# first time
|
||||
wrap-worker ( ) {
|
||||
|
|
|
|||
391
common/predictive-text/docs/worker-communication-protocol.md
Normal file
391
common/predictive-text/docs/worker-communication-protocol.md
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
Keyman/Language Modelling layer
|
||||
================================
|
||||
|
||||
This document introduces the protocol for communicating between
|
||||
KeymanWeb and the language modeling layer (i.e., the configurable
|
||||
prediction and suggestion engine, (henceforth referred to as the
|
||||
"LMLayer").
|
||||
|
||||
Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
|
||||
|
||||
> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
|
||||
> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
|
||||
> "OPTIONAL" in this document are to be interpreted as described in
|
||||
> [RFC 2119].
|
||||
|
||||
[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
|
||||
|
||||
|
||||
Communication protocol between keyboard and asynchronous worker
|
||||
---------------------------------------------------------------
|
||||
|
||||

|
||||
|
||||
We have decided that everything to the right of the `KeymanWeb` will be
|
||||
in a Web Worker. However, communication can happen only through
|
||||
[`postMessage(data)`][postMessage] commands,
|
||||
where `data` is a serializable object (via the [structured clone][]
|
||||
algorithm).
|
||||
|
||||
What serializable object can we send that will adhere to the [open-closed principle]?
|
||||
|
||||
### Messages
|
||||
|
||||
The idea is to use a [discriminated union][]. The protocol involves
|
||||
plain JavaScript objects with one property called `message` that takes
|
||||
a finite set of `string` values.
|
||||
|
||||
These string values indicate what message should be sent. The rest of
|
||||
the properties in the object are the parameters send with the message.
|
||||
|
||||
```javascript
|
||||
{
|
||||
message: 'predict',
|
||||
// message-specific properties here
|
||||
}
|
||||
```
|
||||
|
||||
See also: [XML-RPC][]
|
||||
|
||||
Messages are **not** methods. That is, there is no assumption that
|
||||
a client will receive a reply when a message is sent. However, some
|
||||
pairs of messages, such as `predict` and `suggestions`, lightly assume
|
||||
request/response semantics.
|
||||
|
||||
[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
|
||||
[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
|
||||
[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
|
||||
[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
||||
[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
|
||||
|
||||
|
||||
### Tokens
|
||||
|
||||
Tokens uniquely identify an input event, such as a key press. Since
|
||||
Keyman should ask for an asynchronous prediction on most key presses, the
|
||||
token is intended to associate a prediction and its response with
|
||||
a particular input; Keyman is free to ignore prediction responses if
|
||||
they are for outdated input events.
|
||||
|
||||
The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
|
||||
its contents; it simply uses it to identify a request and pass it back
|
||||
to Keyman. There are a few requirements on the concrete type of the
|
||||
`Token`:
|
||||
|
||||
1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
|
||||
2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
|
||||
3. Tokens **MUST** be unique across messages. That is, tokens **MUST
|
||||
NOT** be duplicated between different messages.
|
||||
|
||||
It is up to the keyboard to create unambiguous tokens that can be uniquely
|
||||
identified through the round-trip process.
|
||||
|
||||
In the following examples, I'll use the subset of `number` values that
|
||||
are interpretable as [31-bit signed integers][Smi].
|
||||
|
||||
[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
|
||||
[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
|
||||
[Tokens]: #tokens
|
||||
|
||||
### Example
|
||||
|
||||
An asynchronous message to predict after typing 'D':
|
||||
|
||||
```javascript
|
||||
{
|
||||
message: 'predict',
|
||||
|
||||
token: 1,
|
||||
transform: {
|
||||
insert: 'D',
|
||||
deleteLeft: 0,
|
||||
deleteRight: 0
|
||||
},
|
||||
context: {
|
||||
left: '',
|
||||
right: '',
|
||||
startOfBuffer: true,
|
||||
endOfBuffer: true
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Message types
|
||||
-------------
|
||||
|
||||
Currently there are four message types:
|
||||
|
||||
Message | Direction | Parameters | Expected reply | Uses token
|
||||
--------------|--------------------|---------------------|---------------------|---------------
|
||||
`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
|
||||
`ready` | LMLayer → keyboard | configuration | No | No
|
||||
`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
|
||||
`suggestions` | LMLayer → keyboard | suggestions | No | Yes
|
||||
|
||||
|
||||
### Message: `initialize`
|
||||
|
||||
Must be sent from the keyboard to the LMLayer so that the LMLayer
|
||||
initializes a model. It will send `initialization` which is a plain
|
||||
JavaScript object specify the path to the model, as well configurations
|
||||
and platform restrictions.
|
||||
|
||||
The keyboard **MUST NOT** send any messages to the LMLayer prior to
|
||||
sending `initialize`. The keyboard **SHOULD NOT** send another message
|
||||
to the keyboard until it receives `ready` message from the LMLayer
|
||||
before sending another message.
|
||||
|
||||
These are the configurations, and platform restrictions sent to
|
||||
initialize the LMLayer and its model.
|
||||
|
||||
```typescript
|
||||
interface InitializeMessage {
|
||||
message: 'initialize';
|
||||
|
||||
/**
|
||||
* Path to the model. There are no concrete restrictions on the path
|
||||
* to the model, so long as the LMLayer can successfully use it to
|
||||
* initialize the model.
|
||||
*/
|
||||
model: string;
|
||||
|
||||
configuration: {
|
||||
/**
|
||||
* Whether the platform supports right contexts.
|
||||
* The absence of this rule implies false.
|
||||
*/
|
||||
supportsRightContexts?: false,
|
||||
|
||||
/**
|
||||
* Whether the platform supports deleting to the right.
|
||||
* The absence of this rule implies false.
|
||||
*/
|
||||
supportsDeleteRight?: false,
|
||||
|
||||
/**
|
||||
* The maximum amount of UTF-16 code units that the keyboard will
|
||||
* provide to the left of the cursor, as an integer.
|
||||
*/
|
||||
maxLeftContextCodeUnits: number,
|
||||
|
||||
/**
|
||||
* The maximum amount of code units that the keyboard will provide to
|
||||
* the right of the cursor, as an integer. The absence of this rule
|
||||
* implies 0.
|
||||
* See also, [[supportsRightContexts]].
|
||||
*/
|
||||
maxRightContextCodeUnits?: number,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Message: `ready`
|
||||
|
||||
Must be sent from the LMLayer to the keyboard when the LMLayer's model
|
||||
as a response to `initialize`. It will send `configuration`, which is
|
||||
a plain JavaScript object requesting configuration from the keyboard.
|
||||
|
||||
There are only two options defined so far:
|
||||
|
||||
```typescript
|
||||
interface ReadyMessage {
|
||||
message: 'ready';
|
||||
configuration: {
|
||||
/**
|
||||
* How many UTF-16 code units maximum to send as the context to the
|
||||
* left of the cursor ("left" in the Unicode character stream).
|
||||
*
|
||||
* Affects the `context` property sent in `predict` messages.
|
||||
*
|
||||
* While the left context MUST NOT bisect surrogate pairs, they MAY
|
||||
* bisect graphical clusters.
|
||||
*/
|
||||
leftContextCodeUnits: number,
|
||||
|
||||
/**
|
||||
* How many UTF-16 code units maximum to send as the context to the
|
||||
* right of the cursor ("right" in the Unicode character stream).
|
||||
*
|
||||
* Affects the `context` property sent in `predict` messages.
|
||||
*
|
||||
* While the left context MUST NOT bisect surrogate pairs, they MAY
|
||||
* bisect graphical clusters.
|
||||
*/
|
||||
rightContextCodeUnits: number,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Message: `predict`
|
||||
|
||||
Sent from the keyboard to the LMLayer whenever a new prediction should
|
||||
be generated. This is typically initiated by a key press event. The
|
||||
keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
|
||||
token **MUST** be unique across all prediction events. The LMLayer
|
||||
**SHOULD** respond to each `predict` message with a `suggestions`
|
||||
message. The `suggestions` message **MUST** contain the corresponding
|
||||
token as sent in the initial `predict` message.
|
||||
|
||||
The keyboard **MUST** send the `context` parameter. The keyboard
|
||||
**SHOULD** send the `transform` parameter. The keyboard **MUST** send
|
||||
a unique token.
|
||||
|
||||
The semantics of the `predict` message **MUST** be from the
|
||||
perspective of this sequence of events:
|
||||
|
||||
1. After the input event is received by the keyboard.
|
||||
2. Before the keyboard applies the associated `transform` to the buffer.
|
||||
|
||||
**NOTE**: The keyboard **MAY** apply the `transform` associated with the
|
||||
input event before receiving the corresponding `suggestions` message
|
||||
from the LMLayer. The intention is that once the suggestions are displayed,
|
||||
the typist may select one of the suggestions in the place of the effects
|
||||
of their original input.
|
||||
|
||||
**NOTE**: The keyboard **MAY** send the `predict` message after it has
|
||||
applied the `transform` associated with the input event to the buffer.
|
||||
Regardless of the actual sequence, the semantics **MUST** remain the
|
||||
same:the prediction happens from the perspective before the `transform`
|
||||
has been applied. Therefore, if the keyboard eagerly transforms the
|
||||
buffer before it has sent `predict` message, it must anticipate _undoing_
|
||||
the `transform` it has already applied if it is to apply a `transfrom`
|
||||
send in the `suggestions` message. The keyboard must act as if it has
|
||||
never applied the `transform` associated with the input event in the
|
||||
first place.
|
||||
|
||||
The context is the text surrounding the insertion point, _before_ the
|
||||
transform is applied to the buffer.
|
||||
|
||||
```typescript
|
||||
interface Context {
|
||||
/**
|
||||
* Up to maxLeftContextCodeUnits code units of Unicode scalar value
|
||||
* (i. e., characters) to the left of the insertion point in the
|
||||
* buffer. If there is nothing to the left of the buffer, this returns
|
||||
* an empty string.
|
||||
*/
|
||||
left: USVString;
|
||||
|
||||
/**
|
||||
* Up to maxRightContextCodeUnits code units of Unicode scalar value
|
||||
* (i. e., characters) to the right of the insertion point in the
|
||||
* buffer. If there is nothing to the right of the buffer, this returns
|
||||
* an empty string.
|
||||
*/
|
||||
right?: USVString;
|
||||
|
||||
/**
|
||||
* Whether the insertion point is at the start of the buffer.
|
||||
*/
|
||||
startOfBuffer: boolean;
|
||||
|
||||
/**
|
||||
* Whether the insertion point is at the end of the buffer.
|
||||
*/
|
||||
endOfBuffer: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
The transform parameter describes how the input event will change the
|
||||
buffer.
|
||||
|
||||
```typescript
|
||||
interface Transform {
|
||||
/**
|
||||
* The Unicode scalar values (i.e., characters) to be inserted at the
|
||||
* cursor position.
|
||||
*
|
||||
* Corresponds to `s` in com.keyman.KeyboardInterface.output.
|
||||
*/
|
||||
insert: USVString;
|
||||
|
||||
/**
|
||||
* The number of code units to delete to the left of the cursor.
|
||||
*
|
||||
* Corresponds to `dn` in com.keyman.KeyboardInterface.output.
|
||||
*/
|
||||
delete: number;
|
||||
|
||||
/**
|
||||
* The number of code units to delete to the right of the cursor.
|
||||
* Not available on all platforms.
|
||||
*/
|
||||
deleteRight?: number;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Message: `suggestions`
|
||||
|
||||
The `suggestions` message is sent from the LMLayer to the keyboard. This
|
||||
message sends a ranked array of suggestions, in descending order of
|
||||
probability (i.e., entry `0` is most likely, followed by entry `1`,
|
||||
etc.). This message **MUST** be in response to a `predict` message, and
|
||||
it **MUST** respond with the corresponding [token].
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* `suggestions` is an ordered array of suggestion objects.
|
||||
* Each suggestion is a transform bundled with a `displayAs` property.
|
||||
*/
|
||||
let suggestions = Suggestion[];
|
||||
|
||||
interface Suggestion {
|
||||
/**
|
||||
* Same object as an input event transform.
|
||||
* Note that the transform is applied AFTER the input event
|
||||
* transform.
|
||||
*/
|
||||
transform: Transform;
|
||||
/**
|
||||
* A string to display the suggestion to the typist.
|
||||
* This should aid the typist understand what the transform
|
||||
* will do to their text.
|
||||
*/
|
||||
displayAs: string;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
let suggestions = [
|
||||
{
|
||||
transform: {
|
||||
insert: 'teapot',
|
||||
deleteLeft: 1,
|
||||
deleteRight: 0
|
||||
},
|
||||
displayAs: '🍵'
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
#### Timing
|
||||
|
||||
Each suggestion provides a `transform`. This transform is applied
|
||||
_after_ the transform associated with the input event that initiated
|
||||
this prediction. That is, the suggested transform applies to the buffer
|
||||
after the transform associated with the input event.
|
||||
|
||||
Rephrased in somewhat mathematical terms:
|
||||
Let 𝑥 ∈ 𝐵 be the input text buffer. Let 𝑇<sub>𝑖</sub> be the transform
|
||||
associated with an input event that maps a text buffer 𝐵 → 𝐵. Let
|
||||
𝑇<sub>𝑠</sub> be a transform suggested through the LMLayer. 𝑦 ∈ 𝐵 is the
|
||||
text buffer after the suggestion transform has been applied. The
|
||||
correct sequence of applications should be as follows:
|
||||
|
||||
> 𝑦 = 𝑇<sub>𝑠</sub>(𝑇<sub>𝑖</sub>(𝑥))
|
||||
|
||||
|
||||
#### Late suggestions
|
||||
|
||||
Sometimes, a `suggestions` message may arrive after an input event has
|
||||
already invalidated its request. This is called a **"late" suggestion**.
|
||||
The LMLayer **MAY** send late suggestions. Consequently, the keyboard
|
||||
**MAY** discard the late suggestions. There is no requirement for the
|
||||
keyboard to acknowledge late suggestions, or for the LMLayer to avoid
|
||||
sending late `suggestions` messages. In either case, a `suggestions`
|
||||
message can be identified as appropriate or "late" via its `token`
|
||||
property.
|
||||
4
common/predictive-text/embedded_worker.d.ts
vendored
4
common/predictive-text/embedded_worker.d.ts
vendored
|
|
@ -21,7 +21,9 @@
|
|||
*/
|
||||
|
||||
// Include the code intended to run WITHIN the Web Worker.
|
||||
// The worker code must be compiled before this file is compiled.
|
||||
// The worker code MUST be compiled before this file is compiled.
|
||||
// If you see a 'File: embedded_worker.js not found.' error message, please
|
||||
// compile the worker first (stage one).
|
||||
/// <reference path="build/intermediate/embedded_worker.js" />
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -32,20 +32,55 @@
|
|||
*/
|
||||
type USVString = string;
|
||||
|
||||
// TODO: document
|
||||
/**
|
||||
* Top-level interface to the Language Modelling layer, or "LMLayer" for short.
|
||||
*
|
||||
* The Language Modelling layer provides a way for keyboards to offer prediction and
|
||||
* correction functionalities. The LMLayer proper runs within a Web Worker, however,
|
||||
* this class is intended to run in the main thread, and automatically spawn a Web
|
||||
* Worker, capable of offering predictions.
|
||||
*
|
||||
* Since the Worker runs in a different thread, the public methods of this class are
|
||||
* asynchronous. Methods of note include:
|
||||
*
|
||||
* - #initialize() -- initialize the LMLayer with a configuration and language model
|
||||
* - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for
|
||||
* the input event
|
||||
*
|
||||
* The top-level LMLayer will automatically starts up its own Web Worker.
|
||||
*/
|
||||
class LMLayer {
|
||||
/**
|
||||
* The underlying worker instance. By default, this is the LMLayerWorker.
|
||||
*/
|
||||
private _worker: Worker;
|
||||
|
||||
/**
|
||||
* Construct the top-level LMLayer interface. This also starts the underlying Worker.
|
||||
* Make sure to call .initialize() when using the default Worker.
|
||||
*
|
||||
* @param uri URI of the underlying LMLayer worker code. This will usually be a blob:
|
||||
* or file: URI. If uri is not provided, this will start the default Worker.
|
||||
*/
|
||||
constructor(uri?: string) {
|
||||
this._worker = new Worker(uri || LMLayer.asBlobURI(LMLayerWorkerCode));
|
||||
}
|
||||
|
||||
// TODO: asynchronous initialize() method, based on
|
||||
// https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L42-L62
|
||||
|
||||
// TODO: asynchronous predict() method, based on
|
||||
// https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L64-L80
|
||||
|
||||
// TODO: asynchronous close() method.
|
||||
// Worker code must recognize message and call self.close().
|
||||
|
||||
/**
|
||||
* Given a function, this utility returns the source code within it.
|
||||
* @param fn
|
||||
* Given a function, this utility returns the source code within it, as a string.
|
||||
* This is intended to unwrap the "wrapped" source code created in the LMLayerWorker
|
||||
* build process.
|
||||
*
|
||||
* @param fn The function whose body will be returned.
|
||||
*/
|
||||
static unwrap(fn: Function): string {
|
||||
let wrapper = fn.toString();
|
||||
|
|
@ -74,7 +109,7 @@ class LMLayer {
|
|||
}
|
||||
}
|
||||
|
||||
// Let LMLayer be available both in browser and in Node.
|
||||
// Let LMLayer be available both in the browser and in Node.
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = LMLayer;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos)
|
||||
* Copyright (c) 2018 SIL International
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a test model that simply returns "teapot" when given the context of
|
||||
* «I'm a little ».
|
||||
*/
|
||||
registerModel(function () {
|
||||
return {
|
||||
predict(context, transform) {
|
||||
return [
|
||||
{
|
||||
transform: {
|
||||
insert: 'teapot',
|
||||
delete: transform.insert.length,
|
||||
deleteRight: 0,
|
||||
},
|
||||
displayAs: '🍵',
|
||||
weight: 0.00,
|
||||
}
|
||||
];
|
||||
|
||||
/* TODO:
|
||||
if (context.wordsLeft === ["I'm", "a", "little"] &&
|
||||
transform.insert === 't') {
|
||||
}
|
||||
|
||||
return [];
|
||||
*/
|
||||
},
|
||||
|
||||
configuration: {
|
||||
}
|
||||
};
|
||||
});
|
||||
/*global registerModel*/
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
var assert = require('chai').assert;
|
||||
var sinon = require('sinon');
|
||||
|
||||
let LMLayer = require('../../build/');
|
||||
let LMLayer = require('../../build');
|
||||
|
||||
// Test the top-level LMLayer interface.
|
||||
// Note: these tests can only be run after BOTH stages of compilation are completed.
|
||||
describe('LMLayer', function() {
|
||||
describe('[[constructor]]', function () {
|
||||
it.skip('should be take a URI to instantiate', function () {
|
||||
|
|
@ -10,17 +12,16 @@ describe('LMLayer', function() {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* .unwrap() is a static function that unwraps some function code.
|
||||
*/
|
||||
// Since the Blob API is browser-specific, look for those tests
|
||||
// for .asBlobURI() in the in_browser tests.
|
||||
describe('.unwrap', function () {
|
||||
// Since the Blob API is DOM-specific, look for those tests
|
||||
// in the in_browser tests.
|
||||
it('should return the inner code of a function', function () {
|
||||
// Create a multi-line function body we can match in a RegExp.
|
||||
let text = LMLayer.unwrap(function hello() {
|
||||
var hello;
|
||||
var world;
|
||||
});
|
||||
// Unwrap should give us back ONLY the body. Whitespace isn't really important.
|
||||
assert.match(text, /^\s*var\s+hello;\s*var\s+world;\s*$/);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,8 +3,12 @@ var sinon = require('sinon');
|
|||
|
||||
let LMLayerWorker = require('../../build/intermediate');
|
||||
|
||||
// Unit tests for instantiating and initializing the LMLayer Worker in isolation.
|
||||
//
|
||||
// Although the LMLayerWorker expected to be used inside a DedicatedWorkerGlobalScope,
|
||||
// these unit tests DO NOT run inside a Worker, and instead use Sinon fakes to assert
|
||||
// behavior.
|
||||
describe('LMLayerWorker', function() {
|
||||
|
||||
describe('#constructor()', function() {
|
||||
it('should allow for the mocking of postMessage()', function () {
|
||||
var fakePostMessage = sinon.fake();
|
||||
|
|
@ -147,6 +151,7 @@ describe('LMLayerWorker', function() {
|
|||
});
|
||||
});
|
||||
|
||||
// TODO: move these tests to a different file.
|
||||
describe('Message: predict', function () {
|
||||
it.skip('should predict from a local model', function () {
|
||||
// will need import scripts figured out
|
||||
|
|
@ -161,6 +166,11 @@ describe('LMLayerWorker', function() {
|
|||
return { data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecation warning: Soon, models will not be requierd to be passed as source code;
|
||||
* when this happens, tests should refrain from sending source code for the model
|
||||
* parameter.
|
||||
*/
|
||||
function dummyModelCode() {
|
||||
return 'return {model: {}, configuration: {}}';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ describe('LMLayer', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#asBlobURI', function () {
|
||||
describe('#asBlobURI()', function () {
|
||||
// #asBlobURI() requires browser APIs, hence why it cannot be tested headless in Node.
|
||||
it('should take a function and convert it into a blob function', function (done) {
|
||||
let uri = LMLayer.asBlobURI(function dummyHandler() {
|
||||
// Post something weird, so we can be reasonably certain it's not a fluke.
|
||||
// WARNING: Do NOT factor out this string as a variable.
|
||||
// It MUST remain a string in this function body, because it gets stringified!
|
||||
// Post something weird, so we can be reasonably certain the Web Worker is...
|
||||
// well, working.
|
||||
// WARNING: Do NOT refactor this string as a variable. It **MUST** remain a string
|
||||
// in this function body, because the code in this function's body gets
|
||||
// stringified!
|
||||
postMessage('fhqwhgads');
|
||||
});
|
||||
assert.match(uri, /^blob:/);
|
||||
|
|
@ -20,33 +20,37 @@
|
|||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The signature of self.postMessage(), so that unit tests can mock it.
|
||||
*/
|
||||
type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage;
|
||||
|
||||
/**
|
||||
* The valid outgoing message types.
|
||||
*/
|
||||
type OutgoingMessageKind = 'ready' | 'suggestions';
|
||||
|
||||
/**
|
||||
* The structure of an initialization message. It should include the model (either in
|
||||
* source code or parameter form), as well as the keyboard's capabilities.
|
||||
*/
|
||||
interface InitializeMessage {
|
||||
/**
|
||||
* Source code of the model.
|
||||
* TODO: write a description of what this source code should look like.
|
||||
* The model, and its configuration.
|
||||
* TODO: write a description of what this actually is!
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* The configuration that the keyboard can offer to the model.
|
||||
*/
|
||||
configuration: RequestedConfiguration;
|
||||
}
|
||||
|
||||
interface InitializeMessage {
|
||||
/**
|
||||
* Source code of the model.
|
||||
* TODO: write a description of what this source code should look like.
|
||||
*/
|
||||
model: string;
|
||||
model: any;
|
||||
|
||||
/**
|
||||
* The configuration that the keyboard can offer to the model.
|
||||
*/
|
||||
// TODO: rename to capabilities? They **are** the capabilities of the keyboard's platform.
|
||||
configuration: RequestedConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* The structure of the message back to the keyboard.
|
||||
*/
|
||||
interface ReadyMessage {
|
||||
configuration: ModelConfiguration;
|
||||
}
|
||||
|
|
@ -104,7 +108,7 @@ interface ModelConfiguration {
|
|||
rightContextCodeUnits: number;
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// TODO: define what valid values of the model are.
|
||||
interface Model {};
|
||||
|
||||
/**
|
||||
|
|
@ -222,7 +226,7 @@ class LMLayerWorker {
|
|||
}
|
||||
}
|
||||
|
||||
// Let LMLayerWorker be available both in browser and in Node.
|
||||
// Let LMLayerWorker be available both in the browser and in Node.
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = LMLayerWorker;
|
||||
} else if (typeof self !== 'undefined' && 'postMessage' in self) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue