├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── README.md ├── asconfig.json ├── assembly ├── FrameRequestCallback.ts ├── Promise.ts ├── PromiseActions-js.ts ├── PromiseActions.ts ├── PromiseExecutor.ts ├── console.ts ├── defer.ts ├── globals.ts ├── index.ts ├── requestAnimationFrame.ts ├── setInterval.ts ├── setTimeout.ts ├── tsconfig.json └── utils.ts ├── example ├── .gitignore ├── asconfig.json ├── assembly │ ├── index-js.ts │ ├── index-wasm.ts │ ├── log.d.ts │ ├── log.js │ ├── log.js.ts │ └── tsconfig.json ├── index.js ├── package.json ├── tests-js │ └── index.js └── tests-wasm │ └── index.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Specificty of the following rules matters. 2 | 3 | # Ignore everything, 4 | /**/* 5 | 6 | # but include these. 7 | !/assembly/**/* 8 | !/index.js 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | semi: false, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | bracketSpacing: false, 7 | printWidth: 120, 8 | arrowParens: 'avoid', 9 | 10 | overrides: [{files: '*.md', options: {tabWidth: 2}}], 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ECMAssembly 2 | 3 | (Some) JavaScript APIs brought to AssemblyScript. 4 | 5 | So far: 6 | 7 | - `requestAnimationFrame` 8 | - `setTimeout` 9 | - `Promise` (rudimentary initial implementation, still lacking things like promise chaining and static methods, etc) 10 | - `Console` 11 | 12 | > The name is a play on words: 13 | > `ECMAScript -> AssemblyScript -> ECMAssembly` 14 | 15 | Namely, this provides APIs that require task scheduling from the host 16 | environment's (JavaScript's) event loop; APIs such as `setTimeout`, `Promise`, 17 | etc. AssemblyScript's stdlib currently only provides APIs that can be 18 | implemented entirely on their own in Wasm without scheduling (Wasm does not yet 19 | provide any APIs for scheduling) therefore without the need for bindings. 20 | 21 | > **Note** 22 | > For DOM APIs, see [`asdom`](https://github.com/lume/asdom). 23 | 24 | # Usage 25 | 26 | First: 27 | 28 | ```sh 29 | npm install ecmassembly @assemblyscript/loader 30 | ``` 31 | 32 | > **Note** > `@assemblyscript/loader` is needed, and your program will need to be manually 33 | > loaded with the [loader API](https://github.com/AssemblyScript/assemblyscript/tree/main/lib/loader), and not with AssemblyScript's new auto-bindings 34 | > in 0.20+, for now. 35 | 36 | On the JavaScript side pass required glue code to the Wasm module via imports: 37 | 38 | ```js 39 | import {ECMAssembly} from 'ecmassembly/index.js' 40 | import ASLoader from '@assemblyscript/loader' 41 | 42 | const es = new ECMAssembly() 43 | 44 | const imports = { 45 | ...es.wasmImports, 46 | /*...All your own imports...*/ 47 | } 48 | 49 | ASLoader.instantiateStreaming(fetch('path/to/module.wasm'), imports).then(wasmModule => { 50 | // After the Wasm module is created, you need to pass the exports back to the lib: 51 | es.wasmExports = wasmModule.exports 52 | 53 | // Then finally, run anything from the module that depends on setTimeout, Promise, etc: 54 | wasmModule.exports.runMyApp() 55 | }) 56 | ``` 57 | 58 | For example, see the `example/`'s [Wasm entrypoint](./example/index.js). 59 | 60 | In your AssemblyScript project's `asconfig.json`, make sure the globals are included in `entries`, along with your own entry point: 61 | 62 | ```json 63 | { 64 | "entries": [ 65 | "./node_modules/ecmassembly/assembly/PromiseActions", 66 | "./node_modules/ecmassembly/assembly/globals.ts", 67 | "./path/to/your-entry-point.ts" 68 | ] 69 | } 70 | ``` 71 | 72 | For example, see the `example/`'s [asconfig.json](./example/asconfig.json). 73 | 74 | In your code you can now use the available APIs similar to in regular 75 | JavaScript, for example here is what `Promise` currently looks like: 76 | 77 | ```ts 78 | let actions: PromiseActions | null = null 79 | 80 | export function runMyApp() { 81 | const promise = new Promise(_actions => { 82 | // Temporary hack while AS does not yet support closures (no closing 83 | // over variable except those that are at the top-level of the module). 84 | actions = _actions 85 | 86 | // resolve after 1 second 87 | setTimeout(() => { 88 | actions!.resolve(true) 89 | }, 1000) 90 | }) 91 | 92 | promise.then(result => { 93 | console.log(result) 94 | // -- Console accepts amost anything. Numbers, Strings, Functions, Arrays. 95 | }) 96 | } 97 | ``` 98 | 99 | > **Note** 100 | > AssemblyScript does not support closures for non-top-level variables yet, so 101 | > a `Promise` constructor's executor function receives an object with `resolve` and 102 | > `reject` methods instead of two separate `resolve` and `reject` args, for the 103 | > time being, but this will be updated in the future once closures are supported. 104 | 105 | Here's what `requestAnimationFrame` looks like: 106 | 107 | ```ts 108 | // This is out here because there is no closure support for non-top-level variables yet. 109 | let loop: (time: number) => void = (t: number) => {} 110 | 111 | export function runMyApp() { 112 | // Make an infinite game loop: 113 | 114 | loop = (time: number) => { 115 | // ... render something based on the current elapsed time ... 116 | 117 | requestAnimationFrame(loop) 118 | } 119 | 120 | requestAnimationFrame(loop) 121 | } 122 | ``` 123 | 124 | For example, see `example/`'s [`index-wasm.ts`](./example/assembly/index-wasm.ts) where it exercises all the APIs. 125 | 126 | Finally, make sure when you compile your AS code you pass `--exportTable --exportRuntime` to the `asc` CLI. For example: 127 | 128 | ```sh 129 | asc --target release --exportTable --exportRuntime 130 | ``` 131 | 132 | # Portable mode 133 | 134 | If you plan to make your code compile to Wasm using AssemblyScript (with `asc`) _and_ plain JS using TypeScript (f.e. with `tsc`), then you should make a JS entry point that imports your AS entry point, like so: 135 | 136 | ```js 137 | // First import JS helpers 138 | import 'assemblyscript/std/portable/index' 139 | import type {} from 'ecmassembly/assembly/PromiseActions-js' // placeholder type for JS 140 | 141 | // Then import AS code so that it works in the JS target 142 | export * from './path/to/your-entry-point.js' 143 | ``` 144 | 145 | Then you will need to write some conditional branching in order to handle the 146 | difference between Wasm and JS runtimes when it comes to using `Promise`. The above `Promise` example would need to be updated like so: 147 | 148 | ```ts 149 | let actions: PromiseActions | null = null 150 | 151 | export function runMyApp() { 152 | const promise = new Promise((resolve, reject) => { 153 | // Temporary hack while AS does not yet support closures (no closing 154 | // over variable except those that are at the top-level of the module). 155 | // @ts-expect-error action object is for Wasm only 156 | actions = resolve 157 | 158 | // resolve after 1 second 159 | setTimeout(() => { 160 | if (ASC_TARGET == 0) resolve(time) 161 | else actions!.resolve(true) 162 | }, 1000) 163 | }) 164 | 165 | promise.then(result => { 166 | // this runs one second later, and `result` will be `true` here 167 | }) 168 | } 169 | ``` 170 | 171 | Where `ASC_TARGET == 0` means we're in a JS environment, otherwise the code will use the actions object when compiled to Wasm (`ASC_TARGET != 0`). 172 | 173 | > **Note** 174 | > When closures for non-top-level variables arrive in AssemblyScript, this 175 | > conditional checking will not be needed, and the `Promise` API will work exactly 176 | > the same way in either environment. 177 | 178 | # APIs so far 179 | 180 | - [x] `requestAnimationFrame`/`cancelAnimationFrame` 181 | - [x] `setTimeout`/`clearTimeout` 182 | - [x] `setInterval`/`clearInterval` 183 | - [x] `Promise` (rudimentary initial implementation, still lacking things like proper promise chaining and static methods, etc) 184 | - [ ] Complete the API, make it more to spec. 185 | - [ ] Remove `PromiseActions` after closure support. 186 | - [ ] `queueMicrotask` 187 | -------------------------------------------------------------------------------- /asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": ["assembly/index.ts"], 3 | "targets": { 4 | "debug": { 5 | "binaryFile": "build/untouched.wasm", 6 | "textFile": "build/untouched.wat", 7 | "sourceMap": true, 8 | "debug": true 9 | }, 10 | "release": { 11 | "binaryFile": "build/optimized.wasm", 12 | "textFile": "build/optimized.wat", 13 | "sourceMap": true, 14 | "optimizeLevel": 3, 15 | "shrinkLevel": 1, 16 | "converge": false, 17 | "noAssert": false 18 | } 19 | }, 20 | "options": {} 21 | } 22 | -------------------------------------------------------------------------------- /assembly/FrameRequestCallback.ts: -------------------------------------------------------------------------------- 1 | export type FrameRequestCallback = (time: number) => void 2 | -------------------------------------------------------------------------------- /assembly/Promise.ts: -------------------------------------------------------------------------------- 1 | import {defer, deferWithArg, _defer} from './defer' 2 | import {PromiseActions} from './PromiseActions' 3 | import {PromiseExecutor} from './PromiseExecutor' 4 | import {ptr} from './utils' 5 | 6 | /** 7 | * Creates a new Promise. 8 | * @param executor A callback used to initialize the promise. This callback is 9 | * passed an object with two methods: a resolve method used to resolve the 10 | * promise with a value or the result of another promise, and a reject method 11 | * used to reject the promise with a provided reason or error. When support for 12 | * closures in AS lands, we'll change this to be two callbacks instead of an 13 | * object, as per the Promise spec. 14 | */ 15 | @global 16 | export class Promise { 17 | private __ptr: usize = ptr(this) 18 | private __isSettled: boolean = false 19 | 20 | private __actions: PromiseActions = new PromiseActions(this) 21 | 22 | // Arrays are being used here in order to represent "value | undefined" 23 | // which is not yet possible in AS, where a length of 1 means the value is 24 | // defined, and a length of zero means the value is undefined. 25 | 26 | private __result: Array = [] 27 | private __error: Array = [] 28 | 29 | private __thenCallback: Array<(val: T) => void> = [] 30 | private __catchCallback: Array<(err: E) => void> = [] 31 | private __finallyCallback: Array<() => void> = [] 32 | 33 | constructor(private executor: PromiseExecutor) { 34 | this.executor(this.__actions, () => {}) 35 | } 36 | 37 | /** 38 | * Attaches callbacks for the resolution and/or rejection of the Promise. 39 | * @param onresolved The callback to execute when the Promise is resolved. 40 | * @returns A Promise for the completion of which ever callback is executed. 41 | */ 42 | then(onresolved: (v: T) => void): Promise { 43 | if (this.__thenCallback.length) throw new Error('Promise chaining or multiple then/catch calls not yet supported.') 44 | 45 | this.__thenCallback.push(onresolved) 46 | 47 | if (this.__result.length) this.__runThen() 48 | 49 | // TODO Promise.then should return a new Promise, not `this` 50 | return this 51 | } 52 | 53 | private __runThen(): void { 54 | if (!this.__result.length) throw new Error('This should not be possible.') 55 | 56 | // Run the then callbacks 57 | if (this.__thenCallback.length) { 58 | // XXX unable to pass methods as callbacks: 59 | // defer<(this: Promise) => void>(this.anyMethod) 60 | // _defer(ptr(this.anyMethod)) 61 | // _defer(ptr<(this: Promise) => void>(this.anyMethod)) 62 | 63 | // The goal here is to run the callback in the next microtask, as per Promise spec. 64 | deferWithArg((selfPtr: usize) => { 65 | const self = changetype>(selfPtr) 66 | const fn = self.__thenCallback[0] 67 | fn(self.__result[0]) 68 | }, this.__ptr) 69 | } 70 | 71 | // Run the finally callbacks 72 | if (this.__finallyCallback.length) { 73 | deferWithArg((selfPtr: usize) => { 74 | const self = changetype>(selfPtr) 75 | const fn = self.__finallyCallback[0] 76 | fn() 77 | }, this.__ptr) 78 | } 79 | } 80 | 81 | /** 82 | * Attaches a callback for only the rejection of the Promise. 83 | * @param onrejected The callback to execute when the Promise is rejected. 84 | * @returns A Promise for the completion of the callback. 85 | */ 86 | catch(onrejected: (err: E) => void): Promise { 87 | if (this.__catchCallback.length) throw new Error('Promise chaining or multiple then/catch calls not yet supported.') 88 | 89 | this.__catchCallback.push(onrejected) 90 | 91 | if (this.__error.length) this.__runCatch() 92 | 93 | // TODO Promise.catch should return a new Promise, not `this` 94 | return this 95 | } 96 | 97 | private __runCatch(): void { 98 | if (!this.__error.length) throw new Error('This should not be possible.') 99 | 100 | // Run the catch callbacks 101 | if (this.__catchCallback.length) { 102 | deferWithArg((selfPtr: usize) => { 103 | const self = changetype>(selfPtr) 104 | const fn = self.__catchCallback[0] 105 | fn(self.__error[0]) 106 | }, this.__ptr) 107 | } 108 | 109 | // Run the finally callbacks 110 | if (this.__finallyCallback.length) { 111 | deferWithArg((selfPtr: usize) => { 112 | const self = changetype>(selfPtr) 113 | const fn = self.__finallyCallback[0] 114 | fn() 115 | }, this.__ptr) 116 | } 117 | } 118 | 119 | /** 120 | * Returns a Promise that will be settled once the promise it is chained on 121 | * is resolved or rejected. When the promise settles, the callback passed to 122 | * finally() will be called without any arguments. This is useful for 123 | * running logic regardless if the promise this is chained on resolves 124 | * or rejects (like the finally block of a try-catch-finally 125 | * statement). 126 | * @param onfinally The callback to execute when the parent Promise is settled. 127 | * @returns A Promise for the settlement of the parent Promise. 128 | */ 129 | finally(onfinally: () => void): Promise { 130 | if (this.__finallyCallback.length) 131 | throw new Error('Promise chaining or multiple then/catch calls not yet supported.') 132 | 133 | this.__finallyCallback.push(onfinally) 134 | 135 | // TODO Promise.finally should return a new Promise, not `this` 136 | return this 137 | } 138 | 139 | /** 140 | * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved 141 | * or rejected, and having the value or error of the first resolved or rejected promise. 142 | * @param values An array of Promises. 143 | * @returns A new Promise. 144 | */ 145 | static race(values: Array>): Promise { 146 | return new Promise(actions => { 147 | actions.reject(new Error('Not Implemented')) 148 | }) 149 | } 150 | 151 | /** 152 | * Creates a Promise that is resolved with an array of results when all of the provided Promises 153 | * resolve, or rejected when any Promise is rejected. 154 | * @param values An array of Promises. 155 | * @returns A new Promise. 156 | */ 157 | static all(values: Array>): Promise, Error> { 158 | return new Promise, Error>(actions => { 159 | actions.reject(new Error('Not Implemented')) 160 | }) 161 | } 162 | 163 | /** 164 | * Creates a new promise that is already resolved with the given value. 165 | * @param value The value the promise should be resolved to. 166 | * @returns A resolved promise. 167 | */ 168 | static resolve(value: T): Promise { 169 | return new Promise(actions => { 170 | actions.resolve(value) 171 | }) 172 | } 173 | 174 | /** 175 | * Creates a new promise that is already rejected with the given error. 176 | * @param reason The error the promise should be rejected with. 177 | * @returns A rejected promise. 178 | */ 179 | static reject(reason: T): Promise { 180 | return new Promise(actions => { 181 | actions.reject(reason) 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /assembly/PromiseActions-js.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | // For now, for JS we don't need a class, just a placeholder 3 | type PromiseActions = { 4 | resolve(result: T): void 5 | reject(reason: E): void 6 | } 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /assembly/PromiseActions.ts: -------------------------------------------------------------------------------- 1 | import {Promise} from './Promise' 2 | 3 | // We shouldn't have to export this (this class should not even exist) once AS supports closures. 4 | 5 | @global 6 | export class PromiseActions { 7 | /*friend*/ constructor(private promise: Promise) {} 8 | 9 | resolve(result: T): void { 10 | // @ts-ignore, internal access 11 | if (this.promise.__isSettled) return 12 | // @ts-ignore, internal access 13 | this.promise.__isSettled = true 14 | 15 | // @ts-ignore, internal access 16 | this.promise.__result.push(result) 17 | 18 | // @ts-ignore, internal access 19 | this.promise.__runThen() 20 | } 21 | 22 | reject(reason: E): void { 23 | // @ts-ignore, internal access 24 | if (this.promise.__isSettled) return 25 | // @ts-ignore, internal access 26 | this.promise.__isSettled = true 27 | 28 | // @ts-ignore, internal access 29 | this.promise.__error.push(reason) 30 | 31 | // @ts-ignore, internal access 32 | this.promise.__runCatch() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assembly/PromiseExecutor.ts: -------------------------------------------------------------------------------- 1 | import {PromiseActions} from './PromiseActions' 2 | 3 | // TODO convert to callback form once closures are out. 4 | // type PromiseExecutor = (resolve: (result: T) => void, reject: (error: Error | null) => void) => void 5 | export type PromiseExecutor = (resRej: PromiseActions, rejectJSOnly: () => void) => void 6 | -------------------------------------------------------------------------------- /assembly/console.ts: -------------------------------------------------------------------------------- 1 | import { Promise } from "./Promise" 2 | 3 | declare function _log(data: string): void 4 | 5 | /* So Far, supports: 6 | - Strings 7 | - Numbers 8 | - UintArrays 9 | - IntArrays 10 | - Functions 11 | - ArrayBuffer 12 | - Map 13 | - Set 14 | - DataView 15 | - Promise 16 | - Array 17 | - StaticArray 18 | - Infinity 19 | Add more to the list if you think of any! 20 | */ 21 | 22 | export namespace console { 23 | export function log(data: T): void { 24 | 25 | // -- String 26 | if (data instanceof String) { 27 | _log(changetype(data)) 28 | } 29 | // -- Number 30 | else if (isFloat(data) || isInteger(data) || isSigned(data)) { 31 | _log(f64(data).toString()) 32 | } 33 | // -- Uint(8/16/32/64)Array 34 | else if (data instanceof Uint8Array || data instanceof Uint16Array || data instanceof Uint32Array || data instanceof Uint64Array || data instanceof Uint8ClampedArray) { 35 | _log(`UintArray(${data.length}) [${data.toString()}]`) 36 | } 37 | // -- Int(8/16/32/64)Array 38 | else if (data instanceof Int8Array || data instanceof Int16Array || data instanceof Int32Array || data instanceof Int64Array) { 39 | _log(`IntArray(${data.length}) [${data.toString()}]`) 40 | } 41 | // -- StaticArray 42 | else if (data instanceof StaticArray) { 43 | _log(data.toString()) 44 | } 45 | // -- Array 46 | else if (data instanceof Array) { 47 | _log(data.toString()) 48 | } 49 | // -- ArrayBuffer 50 | else if (data instanceof ArrayBuffer) { 51 | let array = Uint8Array.wrap(data) 52 | let hex = '' 53 | for (let i = 0; i < data.byteLength; i++) { 54 | if (i === data.byteLength - 1) { 55 | hex += array[i].toString(16) 56 | } else { 57 | hex += `${array[i].toString(16)} ` 58 | } 59 | } 60 | _log(`ArrayBuffer {\n [Uint8Contents]: <${hex}>,\n byteLength: ${data.byteLength}\n}`) 61 | } 62 | // -- Map 63 | else if (data instanceof Map) { 64 | _log('[object Map]') 65 | } 66 | // -- Set 67 | else if (data instanceof Set) { 68 | _log('[object Set]') 69 | } 70 | // -- Promise 71 | else if (data instanceof Promise) { 72 | _log('[object Promise]') 73 | } 74 | // -- Functions 75 | else if (typeof data === 'function') { 76 | _log('[Function: ' + 'Unknown' + ']') 77 | // Function.name not working... _log('[Function: ' + data.name + ']') 78 | } 79 | // -- DataView 80 | else if (data instanceof DataView) { 81 | let array = Uint8Array.wrap(data.buffer) 82 | let hex = '' 83 | for (let i = 0; i < data.byteLength; i++) { 84 | if (i === data.byteLength - 1) { 85 | hex += array[i].toString(16) 86 | } else { 87 | hex += `${array[i].toString(16)} ` 88 | } 89 | } 90 | _log(`DataView {\n byteLength: ${data.byteLength},\n byteOffset: ${data.byteOffset},\n buffer: ArrayBuffer {\n [Uint8Contents]: <${hex}>,\n byteLength: ${data.byteLength}\n }\n}`) 91 | } 92 | // -- Unknown 93 | else { 94 | _log('Unknown') 95 | } 96 | 97 | } 98 | } -------------------------------------------------------------------------------- /assembly/defer.ts: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////// 2 | 3 | declare function _defer(fn: usize): void 4 | 5 | export {_defer} 6 | 7 | export function defer(fn: () => void): void { 8 | _defer(fn.index) 9 | } 10 | 11 | ///////////////////////////////////////// 12 | 13 | declare function _deferWithArg(fn: usize, arg: usize): void 14 | 15 | export {_deferWithArg} 16 | 17 | export function deferWithArg(fn: T, arg: usize): void { 18 | if (!isFunction(fn)) { 19 | ERROR('Must pass a function with one parameter.') 20 | throw new Error('Must pass a function with one parameter.') 21 | } 22 | _deferWithArg(fn.index, arg) 23 | } 24 | -------------------------------------------------------------------------------- /assembly/globals.ts: -------------------------------------------------------------------------------- 1 | export * from './Promise' 2 | export * from './setTimeout' 3 | export * from './setInterval' 4 | export * from './requestAnimationFrame' 5 | -------------------------------------------------------------------------------- /assembly/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Promise' 2 | export * from './setTimeout' 3 | export * from './setInterval' 4 | export * from './requestAnimationFrame' 5 | export * from './console' -------------------------------------------------------------------------------- /assembly/requestAnimationFrame.ts: -------------------------------------------------------------------------------- 1 | import { FrameRequestCallback } from "./FrameRequestCallback" 2 | 3 | declare function _requestAnimationFrame(fn: usize): i32 4 | 5 | export {_requestAnimationFrame} 6 | 7 | // @ts-expect-error func decos 8 | @global 9 | export function requestAnimationFrame(fn: FrameRequestCallback): i32 { 10 | return _requestAnimationFrame(fn.index) 11 | } 12 | 13 | // @ts-expect-error func decos 14 | @global 15 | export declare function cancelAnimationFrame(id: i32): void 16 | -------------------------------------------------------------------------------- /assembly/setInterval.ts: -------------------------------------------------------------------------------- 1 | declare function _setInterval(fn: usize, milliseconds: f32): i32 2 | 3 | export {_setInterval} 4 | 5 | // @ts-expect-error func decos 6 | @global 7 | export function setInterval(fn: () => void, milliseconds: f32 = 0.0): i32 { 8 | return _setInterval(fn.index, milliseconds) 9 | } 10 | 11 | // @ts-expect-error func decos 12 | @global 13 | export declare function clearInterval(id: i32): void 14 | -------------------------------------------------------------------------------- /assembly/setTimeout.ts: -------------------------------------------------------------------------------- 1 | declare function _setTimeout(fn: usize, milliseconds: f32): i32 2 | 3 | export {_setTimeout} 4 | 5 | // @ts-expect-error function decorators 6 | @global 7 | export function setTimeout(fn: () => void, milliseconds: f32 = 0.0): i32 { 8 | return _setTimeout(fn.index, milliseconds) 9 | } 10 | 11 | // @ts-expect-error function decorators 12 | @global 13 | export declare function clearTimeout(id: i32): void 14 | -------------------------------------------------------------------------------- /assembly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/assembly.json", 3 | "include": [ 4 | "./**/*.ts" 5 | // , "../example/assembly/log.d.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /assembly/utils.ts: -------------------------------------------------------------------------------- 1 | export function fnPointerToIndex(fnPtr: usize): i32 { 2 | return load(fnPtr) 3 | } 4 | 5 | export function ptr(any: T): usize { 6 | return changetype(any) 7 | } 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | build-js/ 3 | package-lock.json -------------------------------------------------------------------------------- /example/asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | "./node_modules/ecmassembly/assembly/PromiseActions.ts", 4 | "./node_modules/ecmassembly/assembly/globals.ts", 5 | "./assembly/index-wasm.ts" 6 | ], 7 | "targets": { 8 | "debug": { 9 | "binaryFile": "build/untouched.wasm", 10 | "textFile": "build/untouched.wat", 11 | "sourceMap": true, 12 | "debug": true 13 | }, 14 | "release": { 15 | "binaryFile": "build/optimized.wasm", 16 | "textFile": "build/optimized.wat", 17 | "sourceMap": true, 18 | "optimizeLevel": 3, 19 | "shrinkLevel": 1, 20 | "converge": false, 21 | "noAssert": false 22 | } 23 | }, 24 | "options": {} 25 | } 26 | -------------------------------------------------------------------------------- /example/assembly/index-js.ts: -------------------------------------------------------------------------------- 1 | // First import JS type helpers 2 | import 'assemblyscript/std/portable/index' 3 | import type {} from 'ecmassembly/assembly/PromiseActions-js' 4 | 5 | // Then import AS code so that it works in the JS target 6 | export * from './index-wasm.js' 7 | -------------------------------------------------------------------------------- /example/assembly/index-wasm.ts: -------------------------------------------------------------------------------- 1 | import {FrameRequestCallback} from '../node_modules/ecmassembly/assembly/FrameRequestCallback' 2 | import './log.js' 3 | 4 | // User should not new PromiseActions directly, but we have to export it so 5 | // they can create a variable that will reference it because there are no 6 | // closures. Once closures land, this class will not be exported, and the end 7 | // user will use callbacks passed into new Promise executors. 8 | let actions: PromiseActions | null = null 9 | 10 | let timeout: i32 = 0 11 | let interval: i32 = 0 12 | 13 | export function testSetTimeout(): void { 14 | setTimeout(() => { 15 | log('Timeout one!') 16 | }, 1000) 17 | 18 | setTimeout(() => { 19 | log('Timeout two!') 20 | }) 21 | 22 | // We should not see the log from the cancelled timeout. 23 | timeout = setTimeout(() => { 24 | log('We should not see this!!!!!!!!!') 25 | }, 1000) 26 | 27 | setTimeout(() => { 28 | clearTimeout(timeout) 29 | }, 10) 30 | } 31 | 32 | export function testSetInterval(): void { 33 | interval = setInterval(() => { 34 | log('Interval one!') 35 | }, 750) 36 | 37 | setTimeout(() => { 38 | clearInterval(interval) 39 | 40 | interval = setInterval(() => { 41 | log('Interval two!') 42 | }) 43 | }, 2500) 44 | 45 | setTimeout(() => { 46 | clearInterval(interval) 47 | }, 3500) 48 | } 49 | 50 | export function testPromiseThen(): void { 51 | log((2.0).toString()) 52 | 53 | const p = new Promise(actionsOrResolve => { 54 | // @ts-expect-error for Wasm only 55 | actions = actionsOrResolve 56 | 57 | log((3.0).toString()) 58 | 59 | setTimeout(() => { 60 | if (ASC_TARGET == 0) { 61 | // JS 62 | actionsOrResolve(1000) 63 | } else { 64 | // Wasm 65 | // TODO once we have closures, remove this checking, as we'll 66 | // have plain resolve/reject functions. 67 | 68 | // We will not need any null assertion when closures allows us to 69 | // remove PromiseActions and therefore the user relies on the 70 | // resolve/reject functions that will be passed into here, but for 71 | // now they rely on actions object being passed in, and there's no 72 | // way to reference it inside the setTimeout callback except for 73 | // storing it on a global variable due to lacking closures. 74 | actions!.resolve(1000) 75 | } 76 | }, 2000) 77 | }) 78 | 79 | p.then((n: i32) => { 80 | if (n != 1000) throw new Error('It should have resolved with the correct value.') 81 | log(n.toString()) 82 | }).catch((e: Error) => { 83 | throw new Error('catch() should not run.') 84 | }) 85 | 86 | log((4.0).toString()) 87 | } 88 | 89 | let actions2: PromiseActions | null = null 90 | 91 | export function testPromiseCatch(): void { 92 | log((5.0).toString()) 93 | 94 | const p2 = new Promise((actionsOrResolve, reject) => { 95 | // @ts-expect-error for Wasm only 96 | actions2 = actionsOrResolve 97 | 98 | log((6.0).toString()) 99 | 100 | setTimeout(() => { 101 | log((7.0).toString()) 102 | log('CREATE ERROR 1') 103 | const e = new Error('rejected1') 104 | // const e = {message: 'rejected1', name: 'foo'} 105 | 106 | log('REJECT WITH ERROR 1') 107 | if (ASC_TARGET == 0) reject(e) 108 | else actions2!.reject(e) 109 | }, 2000) 110 | }) 111 | 112 | p2.then((n: i32) => { 113 | throw new Error('then() should not run.') 114 | }).catch(e => { 115 | if (e.message != 'rejected1') throw new Error('It should have rejected with the correct error') 116 | log((3000).toString()) 117 | }) 118 | 119 | log((8.0).toString()) 120 | } 121 | 122 | let actions3: PromiseActions | null = null 123 | 124 | export function testPromiseThenFinally(): void { 125 | log((8.1).toString()) 126 | 127 | const p2 = new Promise((actionsOrResolve, reject) => { 128 | // @ts-expect-error for Wasm only 129 | actions3 = actionsOrResolve 130 | 131 | log((8.2).toString()) 132 | 133 | setTimeout(() => { 134 | log((8.3).toString()) 135 | 136 | if (ASC_TARGET == 0) actionsOrResolve(3200) 137 | else actions3!.resolve(3200) 138 | }, 2000) 139 | }) 140 | 141 | p2.then((n: i32) => { 142 | if (n != 3200) throw new Error('Resolved with incorrect value.') 143 | log(n.toString()) 144 | }) 145 | .catch(e => { 146 | throw new Error('catch() should not run.') 147 | }) 148 | .finally(() => { 149 | log('then.finally') 150 | }) 151 | 152 | log((8.4).toString()) 153 | } 154 | 155 | let actions4: PromiseActions | null = null 156 | 157 | export function testPromiseCatchFinally(): void { 158 | log((8.5).toString()) 159 | 160 | const p2 = new Promise((actionsOrResolve, reject) => { 161 | // @ts-expect-error for Wasm only 162 | actions4 = actionsOrResolve 163 | 164 | log((8.6).toString()) 165 | 166 | setTimeout(() => { 167 | log((8.7).toString()) 168 | log('CREATE ERROR 2') 169 | const e = new Error('rejected2') 170 | 171 | log('REJECT WITH ERROR 2') 172 | if (ASC_TARGET == 0) reject(e) 173 | else actions4!.reject(e) 174 | }, 2000) 175 | }) 176 | 177 | p2.then((n: i32) => { 178 | throw new Error('then() should not run.') 179 | }) 180 | .catch(e => { 181 | if (e.message != 'rejected2') throw new Error('It should have rejected with the correct error') 182 | log('caught error 2') 183 | }) 184 | .finally(() => { 185 | log('catch.finally') 186 | }) 187 | 188 | log((8.8).toString()) 189 | } 190 | 191 | let actions5: PromiseActions | null = null 192 | let count: f32 = 0.0 193 | let loop: FrameRequestCallback = (t: number) => {} 194 | 195 | export function testRAF(): void { 196 | log((9.0).toString()) 197 | 198 | const p = new Promise((actionsOrResolve, reject) => { 199 | // @ts-expect-error for Wasm only 200 | actions5 = actionsOrResolve 201 | 202 | log((10.0).toString()) 203 | 204 | requestAnimationFrame(time => { 205 | if (ASC_TARGET == 0) actionsOrResolve(time) 206 | else actions5!.resolve(time) 207 | }) 208 | }) 209 | 210 | p.then((n: f64) => { 211 | log((4000).toString()) 212 | log(n.toString()) 213 | }) 214 | 215 | loop = (time: f64): void => { 216 | log(count.toString()) 217 | log(time.toString()) 218 | if (count++ < 100.0) requestAnimationFrame(loop) 219 | } 220 | 221 | requestAnimationFrame(loop) 222 | 223 | log((11.0).toString()) 224 | } 225 | -------------------------------------------------------------------------------- /example/assembly/log.d.ts: -------------------------------------------------------------------------------- 1 | declare function log(s: string): void 2 | -------------------------------------------------------------------------------- /example/assembly/log.js: -------------------------------------------------------------------------------- 1 | globalThis.log = console.log 2 | -------------------------------------------------------------------------------- /example/assembly/log.js.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error func decos 2 | @global @external('console', 'log') 3 | export declare function log(s: string): void 4 | -------------------------------------------------------------------------------- /example/assembly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "assemblyscript/std/portable.json", 3 | "compilerOptions": { 4 | "outDir": "../build-js", 5 | "lib": ["dom", "esnext"], 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "target": "es2020" 9 | }, 10 | "include": ["./**/*.ts", "log.d.ts", "log.js"] 11 | } 12 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import ASLoader from '@assemblyscript/loader' 4 | import {ECMAssembly} from 'ecmassembly/index.js' 5 | import raf from 'raf' 6 | 7 | // You must polyfill requestAnimationFrame in Node (or else the ecmassembly lib throws a helpful error that user needs to do so). 8 | raf.polyfill(globalThis) 9 | 10 | const es = new ECMAssembly() 11 | 12 | const imports = { 13 | ...es.wasmImports, 14 | console: { 15 | log: s => console.log(wasmModule.exports.__getString(s)), 16 | }, 17 | env: { 18 | abort(message, fileName, line, column) { 19 | console.error('--------- Error message from AssemblyScript ---------') 20 | console.error(' ' + wasmModule.exports.__getString(message)) 21 | console.error(' In file "' + wasmModule.exports.__getString(fileName) + '"') 22 | console.error(` on line ${line}, column ${column}.`) 23 | console.error('-----------------------------------------------------') 24 | }, 25 | }, 26 | } 27 | 28 | function dirname(url) { 29 | const parts = url.split(path.sep) 30 | parts.pop() 31 | return parts.join(path.sep).replace('file://', '') 32 | } 33 | 34 | const wasmModule = ASLoader.instantiateSync( 35 | fs.readFileSync(dirname(import.meta.url) + '/build/untouched.wasm'), 36 | imports, 37 | ) 38 | 39 | // Before doing anything, give the exports to ECMAssembly 40 | es.wasmExports = wasmModule.exports 41 | 42 | // Now run anything (in this case, the example's tests/index.js file will call the exported functions). 43 | export default wasmModule.exports 44 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "scripts": { 4 | "start": "npm i && npm run dev", 5 | "build": "npm run asbuild", 6 | "dev": "npm run asbuild:untouched && npm test && echo TODO: watch mode", 7 | "asbuild:untouched": "asc --target debug --exportTable --exportRuntime", 8 | "asbuild:optimized": "asc --target release --exportTable --exportRuntime", 9 | "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized", 10 | "build:js": "tsc -p assembly/tsconfig.json", 11 | "clean:wasm": "rimraf build", 12 | "clean:js": "rimraf build-js", 13 | "test:wasm": "npm run clean:wasm && npm run asbuild && node tests-wasm", 14 | "test:js": "npm run clean:js && npm run build:js && node tests-js", 15 | "test": "npm i && npm run test:wasm && npm run test:js" 16 | }, 17 | "dependencies": { 18 | "@assemblyscript/loader": "^0.20.0", 19 | "ecmassembly": "file:../", 20 | "raf": "^3.4.1" 21 | }, 22 | "devDependencies": { 23 | "assemblyscript": "^0.18.11", 24 | "typescript": "^4.7.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/tests-js/index.js: -------------------------------------------------------------------------------- 1 | import * as moduleExports from '../build-js/index-js.js' 2 | 3 | import raf from 'raf' 4 | raf.polyfill() 5 | 6 | moduleExports.testSetTimeout() 7 | moduleExports.testSetInterval() 8 | moduleExports.testPromiseThen() 9 | moduleExports.testPromiseCatch() 10 | moduleExports.testPromiseThenFinally() 11 | moduleExports.testPromiseCatchFinally() 12 | 13 | setTimeout(() => moduleExports.testRAF(), 3500) 14 | 15 | console.log('ok') 16 | -------------------------------------------------------------------------------- /example/tests-wasm/index.js: -------------------------------------------------------------------------------- 1 | import moduleExports from '../index.js' 2 | 3 | moduleExports.testSetTimeout() 4 | moduleExports.testSetInterval() 5 | moduleExports.testPromiseThen() 6 | moduleExports.testPromiseCatch() 7 | moduleExports.testPromiseThenFinally() 8 | moduleExports.testPromiseCatchFinally() 9 | 10 | setTimeout(() => moduleExports.testRAF(), 3500) 11 | 12 | console.log('ok') 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | export class ECMAssembly { 4 | table 5 | __pin 6 | __unpin 7 | 8 | get wasmExports() { 9 | return this._exports 10 | } 11 | set wasmExports(e) { 12 | this.table = e.table 13 | this.__pin = e.__pin 14 | this.__unpin = e.__unpin 15 | this._exports = e 16 | } 17 | 18 | _exports = null 19 | 20 | __deferWithArg_pinnedRefCount = new Map() 21 | 22 | wasmImports = { 23 | console: { 24 | _log: (data) => { 25 | console.log(this._exports.__getString(data)) 26 | } 27 | }, 28 | requestAnimationFrame: { 29 | _requestAnimationFrame: fnIndex => { 30 | return requestAnimationFrame(time => { 31 | this.getFn(fnIndex)(time) 32 | }) 33 | }, 34 | 35 | // _cancelAnimationFrame: id => { 36 | // cancelAnimationFrame(id) 37 | // }, 38 | cancelAnimationFrame, 39 | }, 40 | 41 | setTimeout: { 42 | _setTimeout: (fnIndex, ms) => { 43 | return setTimeout(this.getFn(fnIndex), ms) 44 | }, 45 | clearTimeout, 46 | }, 47 | 48 | setInterval: { 49 | _setInterval: (fnIndex, ms) => { 50 | return setInterval(this.getFn(fnIndex), ms) 51 | }, 52 | clearInterval, 53 | }, 54 | 55 | defer: { 56 | _defer: callbackIndex => { 57 | Promise.resolve().then(this.getFn(callbackIndex)) 58 | }, 59 | _deferWithArg: (callbackIndex, argPtr) => { 60 | let refCount = this.__deferWithArg_pinnedRefCount.get(argPtr) 61 | refCount ?? this.__deferWithArg_pinnedRefCount.set(argPtr, (refCount = 0)) 62 | 63 | // Prevent the thing pointed to by argPtr from being collectd, because the callback needs it later. 64 | if (refCount++ === 0) this.__pin(argPtr) 65 | this.__deferWithArg_pinnedRefCount.set(argPtr, refCount) 66 | 67 | Promise.resolve().then(() => { 68 | // At this point, is the callback collected? Did we need to 69 | // __pin the callback too? Does it currently works by 70 | // accident? 71 | 72 | this.getFn(callbackIndex)(argPtr) 73 | 74 | let refCount = this.__deferWithArg_pinnedRefCount.get(argPtr) 75 | if (refCount == null) throw new Error('We should always have a ref count at this point!') 76 | 77 | if (refCount-- === 0) { 78 | this.__unpin(argPtr) 79 | this.__deferWithArg_pinnedRefCount.delete(argPtr) 80 | } else { 81 | this.__deferWithArg_pinnedRefCount.set(argPtr, refCount) 82 | } 83 | }) 84 | }, 85 | }, 86 | } 87 | 88 | getFn(fnIndex) { 89 | if (!this.wasmExports) 90 | throw new Error( 91 | 'Make sure you set .wasmExports after instantiating the Wasm module but before running the Wasm module.', 92 | ) 93 | return this.table.get(fnIndex) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecmassembly", 3 | "version": "0.3.0-beta.0", 4 | "type": "module", 5 | "homepage": "https://github.com/aspkg/ecmassembly", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:aspkg/ecmassembly.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/aspkg/ecmassembly/issues" 12 | }, 13 | "scripts": { 14 | "start": "npm run dev", 15 | "build": "npm run asbuild", 16 | "dev": "npm run asbuild:untouched && npm test && echo TODO: watch mode", 17 | "asbuild:untouched": "asc --target debug --exportTable --exportRuntime", 18 | "asbuild:optimized": "asc --target release --exportTable --exportRuntime", 19 | "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized", 20 | "clean": "rimraf build", 21 | "test": "npm run clean && npm run asbuild && cd example && npm test", 22 | "release:patch": "npm version patch -m 'v%s' && npm publish && git push --follow-tags", 23 | "release:minor": "npm version minor -m 'v%s' && npm publish && git push --follow-tags", 24 | "release:major": "npm version major -m 'v%s' && npm publish && git push --follow-tags" 25 | }, 26 | "devDependencies": { 27 | "assemblyscript": "^0.20.0", 28 | "prettier": "^2.2.1", 29 | "rimraf": "^3.0.2" 30 | }, 31 | "keywords": [ 32 | "assemblyscript", 33 | "webassembly", 34 | "wasm", 35 | "promise", 36 | "requestanimationframe", 37 | "raf", 38 | "settimeout" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------